Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • mediatek/site-interludes
  • aeltheos/site-kwei
  • mediatek/site-kwei
3 results
Show changes
Showing
with 1029 additions and 318 deletions
from django.conf import settings
from django.contrib import messages
from django.core.mail import mail_admins, send_mass_mail
from django.db.models import Count
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse, reverse_lazy
from django.views.generic import FormView, RedirectView, TemplateView
from accounts.models import EmailUser
from home import models
from home.views import get_planning_context
from site_settings.models import Colors, SiteSettings
from shared.views import CSVWriteView, SuperuserRequiredMixin
from interludes import settings as site_settings
from admin_pages.forms import Recipients, SendEmailForm
# ==============================
# Main Admin views
# ==============================
class AdminView(SuperuserRequiredMixin, TemplateView):
template_name = "admin.html"
def get_metrics(self):
registered = models.ParticipantModel.objects.filter(
is_registered=True, user__is_active=True
)
acts = models.ActivityModel.objects.all()
slots_in = models.SlotModel.objects.all()
wishes = models.ActivityChoicesModel.objects.filter(
participant__is_registered=True, participant__user__is_active=True
)
class metrics:
participants = registered.count()
ulm = registered.filter(school=models.ParticipantModel.ENS.ENS_ULM).count()
lyon = registered.filter(school=models.ParticipantModel.ENS.ENS_LYON).count()
rennes = registered.filter(school=models.ParticipantModel.ENS.ENS_RENNES).count()
saclay = registered.filter(school=models.ParticipantModel.ENS.ENS_CACHAN).count()
non_registered = EmailUser.objects.filter(is_active=True).count() - participants
# mugs = registered.filter(mug=True).count()
sleeps = registered.filter(sleeps=True).count()
paid = registered.filter(paid=True).count()
meal1 = registered.filter(meal_friday_evening=True).count()
meal2 = registered.filter(meal_saturday_morning=True).count()
meal3 = registered.filter(meal_saturday_midday=True).count()
meal4 = registered.filter(meal_saturday_evening=True).count()
meal5 = registered.filter(meal_sunday_morning=True).count()
meal6 = registered.filter(meal_sunday_midday=True).count()
meal7 = registered.filter(meal_sunday_evening=True).count()
meals = meal1 + meal2 + meal3 + meal4 + meal5 + meal6
activites = acts.count()
displayed = acts.filter(display=True).count()
act_ins = acts.filter(display=True, must_subscribe=True).count()
communicate = acts.filter(communicate_participants=True).count()
st_present = acts.filter(display=True, status=models.ActivityModel.Status.PRESENT).count()
st_distant = acts.filter(display=True, status=models.ActivityModel.Status.DISTANT).count()
st_both = acts.filter(display=True, status=models.ActivityModel.Status.BOTH).count()
slots = slots_in.count()
true_ins = slots_in.filter(subscribing_open=True).count()
wish = wishes.count()
granted = wishes.filter(accepted=True).count()
malformed = models.ActivityChoicesModel.objects.filter(slot__subscribing_open=False).count()
return metrics
def validate_activity_participant_nb(self):
""" Vérifie que le nombre de participant inscrit
à chaque activité est compris entre le min et le max"""
slots = models.SlotModel.objects.filter(subscribing_open=True)
min_fails = ""
max_fails = ""
for slot in slots:
total = models.ActivityChoicesModel.objects.filter(
slot=slot, accepted=True, participant__is_registered=True,
participant__user__is_active=True
).aggregate(total=Count("id"))["total"]
max = slot.activity.max_participants
min = slot.activity.min_participants
if max != 0 and max < total:
max_fails += "<br> &bullet;&ensp;{}: {} inscrits (maximum {})".format(
slot, total, max
)
if min > total:
min_fails += "<br> &bullet;&ensp;{}: {} inscrits (minimum {})".format(
slot, total, min
)
message = ""
if min_fails:
message += '<li class="error">Activités en sous-effectif : {}</li>'.format(min_fails)
else:
message += '<li class="success">Aucune activité en sous-effectif</li>'
if max_fails:
message += '<li class="error">Activités en sur-effectif : {}</li>'.format(max_fails)
else:
message += '<li class="success">Aucune activité en sur-effectif</li>'
return message
def validate_activity_conflicts(self):
"""Vérifie que personne n'est inscrit à des activités simultanées"""
slots = models.SlotModel.objects.filter(subscribing_open=True)
conflicts = []
for i, slot_1 in enumerate(slots):
for slot_2 in slots[i+1:]:
if slot_1.conflicts(slot_2):
conflicts.append((slot_1, slot_2))
base_qs = models.ActivityChoicesModel.objects.filter(
accepted=True, participant__is_registered=True,
participant__user__is_active=True
)
errors = ""
for slot_1, slot_2 in conflicts:
participants_1 = {x.participant for x in base_qs.filter(slot=slot_1)}
participants_2 = {x.participant for x in base_qs.filter(slot=slot_2)}
intersection = participants_1.intersection(participants_2)
if intersection:
errors += '<br> &bullet;&ensp; {} participe à la fois à "{}" et à "{}"'.format(
", ".join(str(x) for x in intersection), slot_1, slot_2
)
if errors:
return '<li class="error">Des participants ont plusieurs activités au même moment :{}</li>'.format(
errors
)
return '<li class="success">Aucun inscrit à plusieurs activités simultanées</li>'
def validate_slot_less(self):
"""verifie que toutes les activité demandant une liste de participant ont un créneaux"""
activities = models.ActivityModel.objects.filter(communicate_participants=True)
errors = ""
for activity in activities:
count = models.SlotModel.objects.filter(activity=activity).count()
if count == 0:
errors += "<br> &bullet;&ensp; {}".format(activity.title)
if errors:
return '<li class="error">Certaines activités demandant une liste de participants n\'ont pas de créneaux :{}<br>Leurs orgas vont recevoir un mail inutile.</li>'.format(
errors
)
return '<li class="success">Toutes les activités demandant une liste de participants ont au moins un créneau</li>'
def validate_multiple_similar_inscription(self):
"""verifie que personne n'est inscrit à la même activité plusieurs fois"""
slots = models.SlotModel.objects.filter(subscribing_open=True)
conflicts = []
for i, slot_1 in enumerate(slots):
for slot_2 in slots[i+1:]:
if slot_1.activity == slot_2.activity:
conflicts.append((slot_1, slot_2))
base_qs = models.ActivityChoicesModel.objects.filter(
accepted=True, participant__is_registered=True,
participant__user__is_active=True
)
errors = ""
for slot_1, slot_2 in conflicts:
participants_1 = {x.participant for x in base_qs.filter(slot=slot_1)}
participants_2 = {x.participant for x in base_qs.filter(slot=slot_2)}
intersection = participants_1.intersection(participants_2)
if intersection:
errors += '<br> &bullet;&ensp; {} inscrit aux créneaux "{}" et "{}" de l\'activité "{}"'.format(
", ".join(str(x) for x in intersection), slot_1, slot_2, slot_1.activity
)
if errors:
return '<li class="error">Des participants sont inscrits plusieurs fois à la même activité :{}</li>'.format(
errors
)
return '<li class="success">Aucun inscrit plusieurs fois à une même activité</li>'
def planning_validation(self):
"""Vérifie que toutes les activités ont le bon nombre de créneaux
dans le planning"""
errors = ""
activities = models.ActivityModel.objects.all()
for activity in activities:
nb_wanted = activity.desired_slot_nb
nb_got = activity.slots.count()
if nb_wanted != nb_got:
errors += '<br> &bullet;&ensp; "{}" souhaite {} crénaux mais en a {}.'.format(
activity.title, nb_wanted, nb_got
)
if errors:
return '<li class="error">Certaines activités ont trop/pas assez de crénaux :{}</li>'.format(
errors
)
return '<li class="success">Toutes les activités ont le bon nombre de crénaux</li>'
def validate_activity_allocation(self):
settings = SiteSettings.load()
validations = '<ul class="messagelist">'
# validate global settings
if not settings.inscriptions_open:
validations += '<li class="success">Les inscriptions sont fermées</li>'
else:
validations += '<li class="error">Les inscriptions sont encores ouvertes</li>'
if settings.activities_allocated:
validations += '<li class="success">La répartition est marquée comme effectuée</li>'
else:
validations += '<li class="error">La répartition n\'est pas marquée comme effectuée</li>'
# longer validations
validations += self.validate_activity_participant_nb()
validations += self.validate_activity_conflicts()
validations += self.validate_multiple_similar_inscription()
validations += self.validate_slot_less()
if settings.discord_link:
validations += '<li class="success">Le lien du discord est renseigné</li>'
else:
validations += '<li class="error">Le lien du discord n\'est pas renseigné</li>'
validations += '</ul>'
user_email_nb = models.ParticipantModel.objects.filter(
is_registered=True, user__is_active=True
).count()
orga_email_nb = models.ActivityModel.objects.filter(
communicate_participants=True
).count()
return {
"validations": validations,
"user_email_nb": user_email_nb,
"orga_email_nb": orga_email_nb,
"validation_errors": '<li class="error">' in validations,
"planning_validation": self.planning_validation(),
}
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["metrics"] = self.get_metrics()
context.update(get_planning_context())
context.update(self.validate_activity_allocation())
return context
# ==============================
# DB Export Views
# ==============================
class ExportActivities(SuperuserRequiredMixin, CSVWriteView):
filename = "activites_interludes"
model = models.ActivityModel
fields = [
# The key is "host_id" but listed as "host" in auto-found field names
# which leads to an error...
'id', 'display', 'title', 'act_type', 'game_type', 'description',
'desc_as_html', 'host_id', 'host_name', 'host_email', 'host_info', 'show_email',
'must_subscribe', 'communicate_participants', 'max_participants',
'min_participants', 'duration', 'desired_slot_nb',
'available_friday_evening', 'available_friday_night',
'available_saturday_morning', 'available_saturday_afternoon',
'available_saturday_evening', 'available_saturday_night',
'available_sunday_morning', 'available_sunday_afternoon',
'constraints', 'status', 'needs', 'comments'
]
class ExportSlots(SuperuserRequiredMixin, CSVWriteView):
filename = "créneaux_interludes"
headers = [
"Titre", "Début", "Salle",
"Ouverte aux inscriptions", "Affiché sur le planning", "Affiché sur l'activité",
"Couleur", "Durée", "Durée activité",
]
def get_rows(self):
slots = models.SlotModel.objects.all()
rows = []
for slot in slots:
rows.append([
str(slot), slot.start, slot.room,
slot.subscribing_open, slot.on_planning, slot.on_activity,
Colors(slot.color).name, slot.duration, slot.activity.duration,
])
return rows
class ExportParticipants(SuperuserRequiredMixin, CSVWriteView):
filename = "participants_interludes"
headers = [
"id", "mail", "prénom", "nom", "ENS", "Dors sur place", #"Tasse",
"Repas vendredi", "Repas S matin", "Repas S midi", "Repas S soir",
"Repas D matin", "Repas D midi", "Reaps D soir", "Payé⋅e", "Prix",
"Montant payé"
]
def get_rows(self):
profiles = models.ParticipantModel.objects.filter(
is_registered=True,user__is_active=True
).all()
rows = []
for profile in profiles:
rows.append([
profile.user.id,
profile.user.email,
profile.user.first_name,
profile.user.last_name,
profile.school,
profile.sleeps,
# profile.mug,
profile.meal_friday_evening,
profile.meal_saturday_morning,
profile.meal_saturday_midday,
profile.meal_saturday_evening,
profile.meal_sunday_morning,
profile.meal_sunday_midday,
profile.meal_sunday_evening,
profile.paid,
profile.cost,
profile.amount_paid
])
return rows
class ExportActivityChoices(SuperuserRequiredMixin, CSVWriteView):
filename = "choix_activite_interludes"
model = models.ActivityChoicesModel
headers = ["id_participant", "nom_participant", "mail_participant", "priorité", "obtenu", "nom_créneau", "id_créneau"]
def get_rows(self):
activities = models.ActivityChoicesModel.objects.all()
rows = []
for act in activities:
if act.participant.is_registered and act.participant.user.is_active:
rows.append([
act.participant.id, str(act.participant), act.participant.user.email, act.priority,
act.accepted, str(act.slot), act.slot.id
])
return rows
# ==============================
# Send email views
# ==============================
class SendEmailBase(SuperuserRequiredMixin, RedirectView):
"""Classe abstraite pour l'envoie d'un groupe d'emails"""
pattern_name = "admin_pages:index"
from_address = None
def send_emails(self):
raise NotImplementedError("{}.send_emails isn't implemented".format(self.__class__.__name__))
def get_redirect_url(self, *args, **kwargs):
settings = SiteSettings.load()
if settings.allow_mass_mail:
self.send_emails()
else:
messages.error(self.request, "L'envoi de mail de masse est désactivé dans les réglages")
return reverse(self.pattern_name)
class SendUserEmail(SendEmailBase):
"""Envoie aux utilisateurs leur repartition d'activité"""
def get_emails(self):
"""genere les mails a envoyer"""
participants = models.ParticipantModel.objects.filter(
is_registered=True, user__is_active=True
)
emails = []
settings = SiteSettings.load()
for participant in participants:
my_choices = models.ActivityChoicesModel.objects.filter(participant=participant)
message = render_to_string("email/user.html", {
"user": participant.user,
"settings": settings,
"requested_activities_nb": my_choices.count(),
"my_choices": my_choices.filter(accepted=True),
})
emails.append((
site_settings.USER_EMAIL_SUBJECT_PREFIX + "Vos activités", # subject
message,
self.from_address, # From:
[participant.user.email], # To:
))
return emails
def send_emails(self):
settings = SiteSettings.load()
if settings.user_notified:
messages.error(self.request, "Les participants ont déjà reçu un mail annonçant la répartition. Modifiez les réglages pour en envoyer un autre")
return
settings.user_notified = True
settings.save()
emails = self.get_emails()
nb_sent = send_mass_mail(emails, fail_silently=False)
mail_admins(
"Emails de répartition envoyés aux participants",
"Les participants ont reçu un mail leur communiquant la répartition des activités\n"
"Nombre total de mail envoyés: {}\n\n"
"{}".format(nb_sent, site_settings.EMAIL_SIGNATURE)
)
messages.success(self.request, "{} mails envoyés aux utilisateurs".format(nb_sent))
class SendOrgaEmail(SendEmailBase):
"""
Envoie aux organisateur leur communiquant les nom/mail des inscrits
à leurs activités
"""
def get_emails(self):
"""genere les mails a envoyer"""
activities = models.ActivityModel.objects.filter(communicate_participants=True)
emails = []
settings = SiteSettings.load()
for activity in activities:
slots = models.SlotModel.objects.filter(activity=activity)
message = render_to_string("email/orga.html", {
"activity": activity,
"settings": settings,
"slots": slots,
})
emails.append((
site_settings.USER_EMAIL_SUBJECT_PREFIX +
"Liste d'inscrits à votre activité {}".format(activity.title), # subject
message,
self.from_address, # From:
[activity.host_email] # To:
))
return emails
def send_emails(self):
settings = SiteSettings.load()
if settings.orga_notified:
messages.error(self.request, "Les orgas ont déjà reçu un mail avec leur listes d'inscrits. Modifiez les réglages pour en envoyer un autre")
return
settings.orga_notified = True
settings.save()
emails = self.get_emails()
nb_sent = send_mass_mail(emails, fail_silently=False)
mail_admins(
"Listes d'inscrits envoyés aux orgas",
"Les mails communiquant aux organisateurs leur listes d'inscrit ont été envoyés\n"
"Nombre total de mail envoyés: {}\n\n"
"{}".format(nb_sent, site_settings.EMAIL_SIGNATURE)
)
messages.success(self.request, "{} mails envoyés aux orgas".format(nb_sent))
class NewEmail(SuperuserRequiredMixin, FormView):
"""Créer un nouveau mail"""
template_name = "send_email.html"
form_class = SendEmailForm
success_url = reverse_lazy("admin_pages:index")
from_address = None
def get_emails(self, selection):
"""return the list of destination emails"""
if selection == Recipients.ALL:
users = EmailUser.objects.filter(is_active=True)
return [u.email for u in users]
elif selection == Recipients.REGISTERED:
participants = models.ParticipantModel.objects.filter(
is_registered=True, user__is_active=True
)
return [p.user.email for p in participants]
else:
raise ValueError("Invalid selection specifier\n")
@staticmethod
def sending_allowed():
"""Checks if sending mass emails is allowed"""
settings = SiteSettings.load()
return settings.allow_mass_mail
def form_valid(self, form):
# This method is called when valid form data has been POSTed.
# It should return an HttpResponse.
if not self.sending_allowed():
messages.error(request, "L'envoi de mail de masse est désactivé dans les réglages")
else:
dest = form.cleaned_data["dest"]
subject = form.cleaned_data["subject"]
text = form.cleaned_data["text"]
emails = []
for to_addr in self.get_emails(dest):
emails.append([
subject,
text,
self.from_address,
[to_addr]
])
nb_sent = send_mass_mail(emails, fail_silently=False)
mail_admins(
"Email envoyé",
"Un email a été envoyé à {}.\n"
"Nombre total de mail envoyés: {}\n\n"
"Sujet : {}\n\n"
"{}\n\n"
"{}".format(
Recipients(dest).label, nb_sent, subject, text,
site_settings.EMAIL_SIGNATURE
)
)
messages.success(self.request, "{} mails envoyés".format(nb_sent))
return super().form_valid(form)
def get_context_data(self, *args, **kwargs):
"""ajoute l'email d'envoie aux données contextuelles"""
context = super().get_context_data(*args, **kwargs)
context["from_email"] = self.from_address if self.from_address else settings.DEFAULT_FROM_EMAIL
context["registered_nb"] = models.ParticipantModel.objects.filter(
is_registered = True, user__is_active=True
).count()
context["accounts_nb"] = EmailUser.objects.filter(is_active=True).count()
return context
def get(self, request, *args, **kwargs):
if self.sending_allowed():
return super().get(request, *args, **kwargs)
messages.error(request, "L'envoi de mail de masse est désactivé dans les réglages")
return HttpResponseRedirect(self.get_success_url())
...@@ -9,8 +9,8 @@ admin.site.site_header = "Administration site interludes" ...@@ -9,8 +9,8 @@ admin.site.site_header = "Administration site interludes"
admin.site.site_title = "Admin Interludes" admin.site.site_title = "Admin Interludes"
@admin.register(models.InterludesActivity) @admin.register(models.ActivityModel)
class InterludesActivityAdmin(ExportCsvMixin, admin.ModelAdmin): class ActivityModelAdmin(ExportCsvMixin, admin.ModelAdmin):
"""option d'affichage des activités dans la vue django admin""" """option d'affichage des activités dans la vue django admin"""
filename = "export_activites.csv" filename = "export_activites.csv"
list_display = ("title", "host_name", "display", "must_subscribe",) list_display = ("title", "host_name", "display", "must_subscribe",)
...@@ -18,44 +18,83 @@ class InterludesActivityAdmin(ExportCsvMixin, admin.ModelAdmin): ...@@ -18,44 +18,83 @@ class InterludesActivityAdmin(ExportCsvMixin, admin.ModelAdmin):
ordering = ("title", "host_name",) ordering = ("title", "host_name",)
list_editable = ("display",) list_editable = ("display",)
fields = ( fields = (
"title", "title", "display",
("host_name", "host_email"), ("host_name", "host_email"), "show_email",
"status", "act_type", "duration", "host_info",
"act_type", "game_type",
"description", "desc_as_html",
("min_participants", "max_participants"), ("min_participants", "max_participants"),
"must_subscribe", "must_subscribe",
"communicate_participants", "communicate_participants",
"description", "desc_as_html", ("duration", "desired_slot_nb"),
"display", (
"notes", "available_friday_evening",
"available_friday_night",
"available_saturday_morning",
"available_saturday_afternoon",
"available_saturday_evening",
"available_saturday_night",
"available_sunday_morning",
"available_sunday_afternoon"
),
"constraints",
"status", "needs",
"comments",
) )
list_per_page = 100 list_per_page = 100
csv_export_fields = [
# The key is "host_id" but listed as "host" in auto-found field names
# which leads to an error...
'id', 'display', 'title', 'act_type', 'game_type', 'description',
'desc_as_html', 'host_id', 'host_name', 'host_email', 'show_email', 'host_info',
'must_subscribe', 'communicate_participants', 'max_participants',
'min_participants', 'duration', 'desired_slot_nb',
'available_friday_evening', 'available_friday_night',
'available_saturday_morning', 'available_saturday_afternoon',
'available_saturday_evening', 'available_saturday_night',
'available_sunday_morning', 'available_sunday_afternoon',
'constraints', 'status', 'needs', 'comments'
]
@admin.register(models.InterludesSlot) @admin.register(models.SlotModel)
class InterludesSlotAdmin(ExportCsvMixin, admin.ModelAdmin): class SlotModelAdmin(ExportCsvMixin, admin.ModelAdmin):
"""option d'affichage des créneaux dans la vue d'admin""" """option d'affichage des créneaux dans la vue d'admin"""
filename = "export_slots.csv" filename = "export_slots.csv"
list_display = ("__str__", "start", "room", "subscribing_open", "on_planning",) csv_export_fields = (
list_filter = ("subscribing_open", "on_planning", "activity__display",) "activity_id", "title",
list_editable = ("subscribing_open", "on_planning",) "start", "duration", "room",
"on_planning", "on_activity", "color",
)
list_display = ("__str__", "start", "room", "subscribing_open", "on_planning", "on_activity",)
list_filter = ("subscribing_open", "on_planning", "on_activity", "activity__display",)
list_editable = ("subscribing_open", "on_planning", "on_activity",)
ordering = ("activity", "title", "start",) ordering = ("activity", "title", "start",)
@admin.register(models.InterludesParticipant) @admin.register(models.ParticipantModel)
class InterludesParticipantAdmin(ExportCsvMixin, admin.ModelAdmin): class ParticipantModelAdmin(ExportCsvMixin, admin.ModelAdmin):
"""option d'affichage des participant dans la vue django admin""" """option d'affichage des participant dans la vue django admin"""
filename = "export_participants.csv" filename = "export_participants.csv"
list_display = ("user", "school", "is_registered") fields = (
"user", "school", "is_registered",
("meal_friday_evening", "meal_saturday_morning", "meal_saturday_midday",
"meal_saturday_evening", "meal_sunday_morning", "meal_sunday_midday",
"meal_sunday_evening"),
"sleeps", "nb_murder", "paid", "amount_paid", "comment"
)
list_display = ("user", "school", "is_registered", "comment")
list_filter = ( list_filter = (
"school", "is_registered", "sleeps", "school", "is_registered", "sleeps",
"meal_friday_evening", "meal_saturday_morning", "meal_saturday_midday", "meal_friday_evening", "meal_saturday_morning", "meal_saturday_midday",
"meal_saturday_evening", "meal_sunday_morning", "meal_sunday_midday", "meal_saturday_evening", "meal_sunday_morning", "meal_sunday_midday",
"meal_sunday_evening", "nb_murder", "paid"
) )
ordering = ("user",) ordering = ("user",)
list_per_page = 200 list_per_page = 200
@admin.register(models.InterludesActivityChoices) @admin.register(models.ActivityChoicesModel)
class ActivityListAdmin(ExportCsvMixin, admin.ModelAdmin): class ActivityListAdmin(ExportCsvMixin, admin.ModelAdmin):
"""option d'affichage des choix d'activités dans la vue django admin""" """option d'affichage des choix d'activités dans la vue django admin"""
filename = "export_choix_activite.csv" filename = "export_choix_activite.csv"
......
...@@ -8,18 +8,20 @@ from shared.forms import FormRenderMixin ...@@ -8,18 +8,20 @@ from shared.forms import FormRenderMixin
class InscriptionForm(FormRenderMixin, forms.ModelForm): class InscriptionForm(FormRenderMixin, forms.ModelForm):
class Meta: class Meta:
model = models.InterludesParticipant model = models.ParticipantModel
fields = ( fields = (
"school", "sleeps", # "mug", "school", "sleeps", # "mug",
"meal_friday_evening", "meal_saturday_morning", "meal_saturday_midday", "meal_friday_evening", "meal_saturday_morning", "meal_saturday_midday",
"meal_saturday_evening", "meal_sunday_morning", "meal_sunday_midday", "meal_saturday_evening", "meal_sunday_morning", "meal_sunday_midday", "meal_sunday_evening",
"paid","nb_murder", "comment"
) )
field_groups = [["school"], ["sleeps"], #["mug"], field_groups = [["school"], ["sleeps"], #["mug"],
[ [
"meal_friday_evening", "meal_saturday_morning", "meal_saturday_midday", "meal_friday_evening", "meal_saturday_morning", "meal_saturday_midday",
"meal_saturday_evening", "meal_sunday_morning", "meal_sunday_midday", "meal_saturday_evening", "meal_sunday_morning", "meal_sunday_midday", "meal_sunday_evening"
] ],
["paid"],["nb_murder"], ["comment"]
] ]
def save(self, *args, commit=True, **kwargs): def save(self, *args, commit=True, **kwargs):
...@@ -31,13 +33,13 @@ class InscriptionForm(FormRenderMixin, forms.ModelForm): ...@@ -31,13 +33,13 @@ class InscriptionForm(FormRenderMixin, forms.ModelForm):
class ActivityForm(FormRenderMixin, forms.ModelForm): class ActivityForm(FormRenderMixin, forms.ModelForm):
class Meta: class Meta:
model = models.InterludesActivityChoices model = models.ActivityChoicesModel
fields = ("slot",) fields = ("slot",)
labels = {"slot":""} labels = {"slot":""}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ActivityForm, self).__init__(*args, **kwargs) super(ActivityForm, self).__init__(*args, **kwargs)
slots = models.InterludesSlot.objects.filter(subscribing_open=True) slots = models.SlotModel.objects.filter(subscribing_open=True)
self.fields['slot'].queryset = slots self.fields['slot'].queryset = slots
class BaseActivityFormSet(forms.BaseFormSet): class BaseActivityFormSet(forms.BaseFormSet):
...@@ -57,3 +59,53 @@ class BaseActivityFormSet(forms.BaseFormSet): ...@@ -57,3 +59,53 @@ class BaseActivityFormSet(forms.BaseFormSet):
if activity in activities: if activity in activities:
raise ValidationError("Vous ne pouvez pas sélectionner une même activtté plusieurs fois") raise ValidationError("Vous ne pouvez pas sélectionner une même activtté plusieurs fois")
activities.append(activity) activities.append(activity)
class ActivitySubmissionForm(FormRenderMixin, forms.ModelForm):
class Meta:
model = models.ActivityModel
fields = (
"title", "act_type", "game_type", "description",
"host_name", "host_email", "host_info",
"must_subscribe", "communicate_participants",
"max_participants", "min_participants",
"duration", "desired_slot_nb",
"available_friday_evening",
"available_friday_night",
"available_saturday_morning",
"available_saturday_afternoon",
"available_saturday_evening",
"available_saturday_night",
"available_sunday_morning",
"available_sunday_afternoon",
"constraints",
#"status",
"needs",
"comments",
)
def clean(self):
cleaned_data = super().clean()
maxi = cleaned_data.get("max_participants")
mini = cleaned_data.get("min_participants")
if maxi != 0 and mini > maxi:
raise forms.ValidationError(
"Le nombre minimal de participants est supérieur au nombre maximal",
code="invalid_order"
)
return cleaned_data
def save(self, *args, commit=True, **kwargs):
"""Enregistre l'activité dans la base de données"""
activity = models.ActivityModel(
**self.cleaned_data,
)
if commit:
activity.save()
return activity
# Generated by Django 3.0.8 on 2021-03-21 17:30 # Generated by Django 3.2.7 on 2021-10-05 18:45
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import home.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
...@@ -15,32 +16,64 @@ class Migration(migrations.Migration): ...@@ -15,32 +16,64 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='InterludesActivity', name='ActivityModel',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('display', models.BooleanField(default=False, help_text="Si vrai, s'affiche sur la page activités", verbose_name='afficher dans la liste')),
('show_email', models.BooleanField(default=True, help_text="Si l'affichage d'email global et cette case sont vrai, affiche l'email de l'orga", verbose_name="afficher l'email de l'orga")),
('title', models.CharField(max_length=200, verbose_name='Titre')), ('title', models.CharField(max_length=200, verbose_name='Titre')),
('status', models.CharField(choices=[('P', 'En présentiel uniquement'), ('D', 'En distanciel uniquement'), ('2', 'Les deux')], max_length=1, verbose_name='Présentiel/distanciel')), ('act_type', models.CharField(choices=[('1 partie', 'Une partie'), ('2+ parties', 'Quelques parties'), ('Tournoi', 'Tournoi'), ('freeplay', 'Freeplay'), ('other', 'Autre')], max_length=12, verbose_name="Type d'activité")),
('act_type', models.CharField(choices=[('Tournoi', 'Tournoi'), ('partie', 'Une partie'), ('parties', 'Quelques parties'), ('freeplay', 'Freeplay'), ('jeu cartes', 'Jeu de cartes'), ('jeu plateau', 'Jeu de société'), ('table RPG', 'Jeu de rôle sur table'), ('large RPG', 'Jeu de rôle grandeur nature'), ('videogame', 'Jeu vidéo'), ('partygame', 'Party game'), ('puzzle', 'Puzzle ou analogue'), ('secret roles', 'Jeu à rôles secrets'), ('coop', 'Jeu coopératif'), ('other', 'Autre')], max_length=12, verbose_name='Type')), ('game_type', models.CharField(choices=[('jeu cartes', 'Jeu de cartes'), ('jeu plateau', 'Jeu de société'), ('table RPG', 'Jeu de rôle sur table'), ('large RPG', 'Jeu de rôle grandeur nature'), ('videogame', 'Jeu vidéo'), ('partygame', 'Party game'), ('puzzle', 'Puzzle ou analogue'), ('secret roles', 'Jeu à rôles secrets'), ('coop', 'Jeu coopératif'), ('other', 'Autre')], max_length=12, verbose_name='Type de jeu')),
('duration', models.DurationField(help_text='format hh:mm:ss', verbose_name='Durée')), ('description', models.TextField(help_text='Texte ou html selon la valeur de "Description HTML".\n', max_length=10000, verbose_name='description')),
('max_participants', models.PositiveIntegerField(help_text='0 pour illimité', verbose_name='Nombre maximum de participants')), ('desc_as_html', models.BooleanField(default=False, help_text='Assurer vous que le texte est bien formaté, cette option peut casser la page activités.', verbose_name='Description au format HTML')),
('min_participants', models.PositiveIntegerField(verbose_name='Nombre minimum de participants')), ('host_name', models.CharField(blank=True, help_text='Peut-être laissé vide pour des simples activités sans orga', max_length=50, null=True, verbose_name="nom de l'organisateur")),
('host_email', models.EmailField(help_text='Utilisé pour communiquer la liste des participants si demandé', max_length=254, verbose_name="email de l'organisateur")),
('host_info', models.TextField(blank=True, max_length=1000, null=True, verbose_name='Autre orgas/contacts')),
('must_subscribe', models.BooleanField(default=False, help_text="Informatif, il faut utiliser les créneaux pour ajouter dans la liste d'inscription", verbose_name='sur inscription')),
('communicate_participants', models.BooleanField(verbose_name="communiquer la liste des participants à l'orga avant l'événement")), ('communicate_participants', models.BooleanField(verbose_name="communiquer la liste des participants à l'orga avant l'événement")),
('display', models.BooleanField(default=False, verbose_name="afficher dans la liste d'activités")), ('max_participants', models.PositiveIntegerField(default=0, help_text='0 pour illimité', verbose_name='Nombre maximum de participants')),
('must_subscribe', models.BooleanField(default=False, help_text="Une activité doit être affichée dans la liste également pour que l'on puisse si inscrire", verbose_name='sur inscription')), ('min_participants', models.PositiveIntegerField(default=0, verbose_name='Nombre minimum de participants')),
('host_name', models.CharField(max_length=50, verbose_name="nom de l'organisateur")), ('duration', models.DurationField(help_text='format hh:mm:ss', verbose_name='Durée')),
('host_email', models.EmailField(max_length=254, verbose_name="email de l'organisateur")), ('desired_slot_nb', models.PositiveIntegerField(default=1, validators=[home.models.validate_nonzero], verbose_name='Nombre de créneaux souhaités')),
('description', models.TextField(max_length=2000, verbose_name='description')), ('available_friday_evening', models.CharField(choices=[('0', 'Idéal'), ('1', 'Acceptable'), ('2', 'Indisponible')], default='1', max_length=1, verbose_name='Crénau vendredi soir')),
('on_planning', models.BooleanField(default=False, help_text='Nécessite de salle et heure de début non vide', verbose_name='afficher sur le planning')), ('available_friday_night', models.CharField(choices=[('0', 'Idéal'), ('1', 'Acceptable'), ('2', 'Indisponible')], default='1', max_length=1, verbose_name='Crénau vendredi nuit')),
('start', models.DateTimeField(blank=True, null=True, verbose_name='début')), ('available_saturday_morning', models.CharField(choices=[('0', 'Idéal'), ('1', 'Acceptable'), ('2', 'Indisponible')], default='1', max_length=1, verbose_name='Crénau samedi matin')),
('room', models.CharField(blank=True, max_length=100, null=True, verbose_name='salle')), ('available_saturday_afternoon', models.CharField(choices=[('0', 'Idéal'), ('1', 'Acceptable'), ('2', 'Indisponible')], default='1', max_length=1, verbose_name='Crénau samedi après-midi')),
('notes', models.TextField(blank=True, max_length=2000, verbose_name='Notes privées')), ('available_saturday_evening', models.CharField(choices=[('0', 'Idéal'), ('1', 'Acceptable'), ('2', 'Indisponible')], default='1', max_length=1, verbose_name='Crénau samedi soir')),
('available_saturday_night', models.CharField(choices=[('0', 'Idéal'), ('1', 'Acceptable'), ('2', 'Indisponible')], default='1', max_length=1, verbose_name='Crénau samedi nuit')),
('available_sunday_morning', models.CharField(choices=[('0', 'Idéal'), ('1', 'Acceptable'), ('2', 'Indisponible')], default='1', max_length=1, verbose_name='Crénau dimanche matin')),
('available_sunday_afternoon', models.CharField(choices=[('0', 'Idéal'), ('1', 'Acceptable'), ('2', 'Indisponible')], default='1', max_length=1, verbose_name='Crénau dimanche après-midi')),
('constraints', models.TextField(blank=True, max_length=2000, null=True, verbose_name='Contraintes particulières')),
('status', models.CharField(choices=[('P', 'En présentiel uniquement'), ('D', 'En distanciel uniquement'), ('2', 'Les deux')], max_length=1, verbose_name='Présentiel/distanciel')),
('needs', models.TextField(blank=True, max_length=2000, null=True, verbose_name='Besoin particuliers')),
('comments', models.TextField(blank=True, max_length=2000, null=True, verbose_name='Commentaires')),
('host', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Organisateur')),
], ],
options={ options={
'verbose_name': 'activité', 'verbose_name': 'activité',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='InterludesParticipant', name='SlotModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(default='{act_title}', help_text="Utilisez '{act_title}' pour insérer le titre de l'activité correspondante", max_length=200, verbose_name='Titre')),
('start', models.DateTimeField(verbose_name='début')),
('duration', models.DurationField(blank=True, help_text="Format 00:00:00. Laisser vide pour prendre la durée de l'activité correspondante", null=True, verbose_name='durée')),
('room', models.CharField(blank=True, max_length=100, null=True, verbose_name='salle')),
('on_planning', models.BooleanField(default=True, verbose_name='afficher sur le planning')),
('on_activity', models.BooleanField(default=True, verbose_name="afficher dans la description de l'activité")),
('subscribing_open', models.BooleanField(default=False, help_text="Si vrai, apparaît dans la liste du formulaire d'inscription", verbose_name='ouvert aux inscriptions')),
('color', models.CharField(choices=[('a', 'Rouge'), ('b', 'Orange'), ('c', 'Jaune'), ('d', 'Vert'), ('e', 'Bleu'), ('f', 'Bleu foncé'), ('g', 'Noir')], default='a', help_text='La légende des couleurs est modifiable dans les paramètres', max_length=1, verbose_name='Couleur')),
('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.activitymodel', verbose_name='Activité')),
],
options={
'verbose_name': 'créneau',
'verbose_name_plural': 'créneaux',
},
),
migrations.CreateModel(
name='ParticipantModel',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('school', models.CharField(choices=[('U', 'ENS Ulm'), ('L', 'ENS Lyon'), ('R', 'ENS Rennes'), ('C', 'ENS Paris Saclay')], max_length=1, verbose_name='ENS de rattachement')), ('school', models.CharField(choices=[('U', 'ENS Ulm'), ('L', 'ENS Lyon'), ('R', 'ENS Rennes'), ('C', 'ENS Paris Saclay')], max_length=1, verbose_name='ENS de rattachement')),
...@@ -59,19 +92,19 @@ class Migration(migrations.Migration): ...@@ -59,19 +92,19 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='ActivityList', name='ActivityChoicesModel',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('priority', models.PositiveIntegerField(verbose_name='priorité')), ('priority', models.PositiveIntegerField(verbose_name='priorité')),
('accepted', models.BooleanField(default=False, verbose_name='Obtenue')), ('accepted', models.BooleanField(default=False, verbose_name='Obtenue')),
('activity', models.ForeignKey(db_column='activité', on_delete=django.db.models.deletion.CASCADE, to='home.InterludesActivity')), ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.participantmodel', verbose_name='participant')),
('participant', models.ForeignKey(db_column='participant', on_delete=django.db.models.deletion.CASCADE, to='home.InterludesParticipant')), ('slot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.slotmodel', verbose_name='créneau')),
], ],
options={ options={
'verbose_name': "choix d'activités", 'verbose_name': "choix d'activités",
'verbose_name_plural': "choix d'activités", 'verbose_name_plural': "choix d'activités",
'ordering': ('participant', 'priority'), 'ordering': ('participant', 'priority'),
'unique_together': {('participant', 'activity'), ('priority', 'participant')}, 'unique_together': {('participant', 'slot'), ('priority', 'participant')},
}, },
), ),
] ]
# Generated by Django 3.0.8 on 2021-03-29 14:46
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('home', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='interludesactivity',
name='canonical',
field=models.ForeignKey(blank=True, help_text="Si plusieurs copie d'une activité existe (pour plusieurs crénaux), et une seule est affichée, sélectionner là dans les copie pour réparer les liens du planning vers la description", null=True, on_delete=django.db.models.deletion.SET_NULL, to='home.InterludesActivity', verbose_name='Représentant canonique'),
),
migrations.AddField(
model_name='interludesactivity',
name='desc_as_html',
field=models.BooleanField(default=False, help_text='Assurer vous que le texte est bien formaté, cette option peut casser la page activités.', verbose_name='Description au format HTML'),
),
migrations.AddField(
model_name='interludesactivity',
name='subscribing_open',
field=models.BooleanField(default=False, help_text="Si vrai, apparaît dans la liste du formulaire d'inscription", verbose_name='ouverte aux inscriptions'),
),
migrations.AlterField(
model_name='interludesactivity',
name='description',
field=models.TextField(help_text='Texte ou html selon la valeur de "Description HTML".\n', max_length=2000, verbose_name='description'),
),
migrations.AlterField(
model_name='interludesactivity',
name='display',
field=models.BooleanField(default=False, help_text="Si vrai, s'affiche sur la page activités", verbose_name='afficher dans la liste'),
),
migrations.AlterField(
model_name='interludesactivity',
name='must_subscribe',
field=models.BooleanField(default=False, help_text="Informatif, il faut utiliser 'ouverte aux inscriptions' pour ajouter dans la liste d'inscription", verbose_name='sur inscription'),
),
]
# Generated by Django 3.2.16 on 2022-11-08 18:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='participantmodel',
name='comment',
field=models.TextField(blank=True, max_length=2000, null=True, verbose_name='Commentaire'),
),
migrations.AddField(
model_name='participantmodel',
name='nb_murder',
field=models.PositiveIntegerField(default=0, verbose_name='Nombre de murder réalisées'),
),
]
# Generated by Django 3.0.8 on 2021-04-25 13:39 # Generated by Django 3.2.16 on 2022-11-15 09:18
import datetime
from django.db import migrations, models from django.db import migrations, models
from django.utils.timezone import utc
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('home', '0004_auto_20210424_1735'), ('home', '0002_auto_20221108_1943'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='interludesslot', model_name='activitymodel',
name='start', name='status',
field=models.DateTimeField(default=datetime.datetime(2021, 5, 8, 11, 0, tzinfo=utc), verbose_name='début'), field=models.CharField(blank=True, choices=[('P', 'En présentiel uniquement'), ('D', 'En distanciel uniquement'), ('2', 'Les deux')], default='P', max_length=1, verbose_name='Présentiel/distanciel'),
preserve_default=False,
), ),
] ]
# Generated by Django 3.0.8 on 2021-04-07 12:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('home', '0002_auto_20210329_1646'),
]
operations = [
migrations.CreateModel(
name='InterludesActivityChoices',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('priority', models.PositiveIntegerField(verbose_name='priorité')),
('accepted', models.BooleanField(default=False, verbose_name='Obtenue')),
('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.InterludesParticipant', verbose_name='participant')),
],
options={
'verbose_name': "choix d'activités",
'verbose_name_plural': "choix d'activités",
'ordering': ('participant', 'priority'),
},
),
migrations.CreateModel(
name='InterludesSlot',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(default='{act_title}', help_text="Utilisez '{act_title}' pour insérer le titre de l'activité correspondante", max_length=200, verbose_name='Titre')),
('start', models.DateTimeField(blank=True, null=True, verbose_name='début')),
('room', models.CharField(blank=True, max_length=100, null=True, verbose_name='salle')),
('on_planning', models.BooleanField(default=False, help_text='Nécessite de salle et heure de début non vide', verbose_name='afficher sur le planning')),
('subscribing_open', models.BooleanField(default=False, help_text="Si vrai, apparaît dans la liste du formulaire d'inscription", verbose_name='ouvert aux inscriptions')),
],
options={
'verbose_name': 'créneau',
'verbose_name_plural': 'créneaux',
},
),
migrations.RemoveField(
model_name='interludesactivity',
name='canonical',
),
migrations.RemoveField(
model_name='interludesactivity',
name='on_planning',
),
migrations.RemoveField(
model_name='interludesactivity',
name='room',
),
migrations.RemoveField(
model_name='interludesactivity',
name='start',
),
migrations.RemoveField(
model_name='interludesactivity',
name='subscribing_open',
),
migrations.AlterField(
model_name='interludesactivity',
name='must_subscribe',
field=models.BooleanField(default=False, help_text="Informatif, il faut utiliser les créneaux pour ajouter dans la liste d'inscription", verbose_name='sur inscription'),
),
migrations.DeleteModel(
name='ActivityList',
),
migrations.AddField(
model_name='interludesslot',
name='activity',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.InterludesActivity', verbose_name='Activité'),
),
migrations.AddField(
model_name='interludesactivitychoices',
name='slot',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.InterludesSlot', verbose_name='créneau'),
),
migrations.AlterUniqueTogether(
name='interludesactivitychoices',
unique_together={('participant', 'slot'), ('priority', 'participant')},
),
]
# Generated by Django 3.0.8 on 2021-04-24 15:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0003_auto_20210407_1420'),
]
operations = [
migrations.AddField(
model_name='interludesslot',
name='color',
field=models.CharField(choices=[('a', 'Bleu foncé'), ('b', 'Rouge'), ('c', 'Jaune'), ('d', 'Bleu'), ('e', 'Vert'), ('f', 'Noir'), ('g', 'Orange')], default='a', max_length=1, verbose_name='Couleur'),
),
migrations.AlterField(
model_name='interludesactivity',
name='description',
field=models.TextField(help_text='Texte ou html selon la valeur de "Description HTML".\n', max_length=10000, verbose_name='description'),
),
migrations.AlterField(
model_name='interludesactivity',
name='host_email',
field=models.EmailField(help_text='Utilisé pour communiquer la liste des participants si demandé', max_length=254, verbose_name="email de l'organisateur"),
),
migrations.AlterField(
model_name='interludesactivity',
name='host_name',
field=models.CharField(blank=True, help_text='Peut-être laissé vide pour des simples activités sans orga', max_length=50, null=True, verbose_name="nom de l'organisateur"),
),
]
# Generated by Django 3.2.16 on 2022-11-17 20:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0003_alter_activitymodel_status'),
]
operations = [
migrations.AddField(
model_name='participantmodel',
name='paid',
field=models.BooleanField(default=False, verbose_name='payé(e)'),
),
]
# Generated by Django 3.2.16 on 2022-12-09 09:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0004_participantmodel_paid'),
]
operations = [
migrations.AddField(
model_name='participantmodel',
name='meal_sunday_evening',
field=models.BooleanField(default=False, verbose_name='repas de dimanche soir'),
),
migrations.AlterField(
model_name='participantmodel',
name='meal_sunday_midday',
field=models.BooleanField(default=False, verbose_name='repas de dimanche midi'),
),
]
# Generated by Django 3.2.16 on 2022-12-09 10:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0005_auto_20221209_1028'),
]
operations = [
migrations.AddField(
model_name='participantmodel',
name='amount_paid',
field=models.PositiveIntegerField(default=0, verbose_name='Montant payé'),
),
]
import datetime import datetime
from django.db import models from django.db import models
from django.forms import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
from accounts.models import EmailUser from accounts.models import EmailUser
from site_settings.models import SiteSettings from site_settings.models import Colors, SiteSettings
class InterludesActivity(models.Model): def validate_nonzero(value):
"""Make a positive integer field non-zero"""
if value == 0:
raise ValidationError(
_('Cette valeur doit-être non-nulle'),
)
class ActivityModel(models.Model):
"""une activité des interludes (i.e. JDR, murder)...""" """une activité des interludes (i.e. JDR, murder)..."""
class Status(models.TextChoices): class Status(models.TextChoices):
...@@ -16,12 +24,16 @@ class InterludesActivity(models.Model): ...@@ -16,12 +24,16 @@ class InterludesActivity(models.Model):
DISTANT = "D", _("En distanciel uniquement") DISTANT = "D", _("En distanciel uniquement")
BOTH = "2", _("Les deux") BOTH = "2", _("Les deux")
class Types(models.TextChoices): class ActivityTypes(models.TextChoices):
"""types d'activités""" """quantité d'activité"""
GAME = "1 partie", _("Une partie")
GAMES = "2+ parties", _("Quelques parties")
TOURNAMENT = "Tournoi", _("Tournoi") TOURNAMENT = "Tournoi", _("Tournoi")
GAME = "partie", _("Une partie")
GAMES = "parties", _("Quelques parties")
FREEPLAY = "freeplay", _("Freeplay") FREEPLAY = "freeplay", _("Freeplay")
OTHER = "other", _("Autre")
class GameTypes(models.TextChoices):
"""types de jeu"""
CARD_GAME = "jeu cartes", _("Jeu de cartes") CARD_GAME = "jeu cartes", _("Jeu de cartes")
BOARD_GAME = "jeu plateau", _("Jeu de société") BOARD_GAME = "jeu plateau", _("Jeu de société")
TABLETOP_RPG = "table RPG", _("Jeu de rôle sur table") TABLETOP_RPG = "table RPG", _("Jeu de rôle sur table")
...@@ -33,25 +45,35 @@ class InterludesActivity(models.Model): ...@@ -33,25 +45,35 @@ class InterludesActivity(models.Model):
COOP = "coop", _("Jeu coopératif") COOP = "coop", _("Jeu coopératif")
OTHER = "other", _("Autre") OTHER = "other", _("Autre")
title = models.CharField("Titre", max_length=200) class Availability(models.TextChoices):
"""Diponibilité à un moment donné"""
IDEAL = "0", _("Idéal")
POSSIBLE = "1", _("Acceptable")
UNAVAILABLE = "2", _("Indisponible")
status = models.CharField("Présentiel/distanciel", choices=Status.choices, max_length=1) display = models.BooleanField("afficher dans la liste", default=False,
act_type = models.CharField("Type", choices=Types.choices, max_length=12) help_text="Si vrai, s'affiche sur la page activités"
duration = models.DurationField("Durée", help_text="format hh:mm:ss")
max_participants = models.PositiveIntegerField(
"Nombre maximum de participants", help_text="0 pour illimité"
) )
min_participants = models.PositiveIntegerField( show_email = models.BooleanField("afficher l'email de l'orga", default=True,
"Nombre minimum de participants" help_text="Si l'affichage d'email global et cette case sont vrai, affiche l'email de l'orga"
) )
communicate_participants = models.BooleanField("communiquer la liste des participants à l'orga avant l'événement")
display = models.BooleanField("afficher dans la liste", default=False, title = models.CharField("Titre", max_length=200)
help_text="Si vrai, s'affiche sur la page activités"
act_type = models.CharField("Type d'activité", choices=ActivityTypes.choices, max_length=12)
game_type = models.CharField("Type de jeu", choices=GameTypes.choices, max_length=12)
description = models.TextField(
"description", max_length=10000,
help_text='Texte ou html selon la valeur de "Description HTML".\n'
) )
must_subscribe = models.BooleanField("sur inscription", default=False, desc_as_html = models.BooleanField("Description au format HTML", default=False,
help_text="Informatif, il faut utiliser les créneaux pour ajouter dans la liste d'inscription" help_text="Assurer vous que le texte est bien formaté, cette option peut casser la page activités."
)
host = models.ForeignKey(
EmailUser, on_delete=models.SET_NULL, verbose_name="Organisateur",
blank=True, null=True
) )
host_name = models.CharField( host_name = models.CharField(
"nom de l'organisateur", max_length=50, null=True, blank=True, "nom de l'organisateur", max_length=50, null=True, blank=True,
...@@ -61,15 +83,76 @@ class InterludesActivity(models.Model): ...@@ -61,15 +83,76 @@ class InterludesActivity(models.Model):
"email de l'organisateur", "email de l'organisateur",
help_text="Utilisé pour communiquer la liste des participants si demandé" help_text="Utilisé pour communiquer la liste des participants si demandé"
) )
description = models.TextField( host_info = models.TextField(
"description", max_length=10000, "Autre orgas/contacts", max_length=1000, blank=True, null=True
help_text='Texte ou html selon la valeur de "Description HTML".\n'
) )
desc_as_html = models.BooleanField("Description au format HTML", default=False,
help_text="Assurer vous que le texte est bien formaté, cette option peut casser la page activités." must_subscribe = models.BooleanField("sur inscription", default=False,
help_text="Informatif, il faut utiliser les créneaux pour ajouter dans la liste d'inscription"
)
communicate_participants = models.BooleanField("communiquer la liste des participants à l'orga avant l'événement")
max_participants = models.PositiveIntegerField(
"Nombre maximum de participants", help_text="0 pour illimité", default=0
)
min_participants = models.PositiveIntegerField(
"Nombre minimum de participants", default=0
) )
notes = models.TextField("Notes privées", max_length=2000, blank=True) ## Information fournies par le respo
duration = models.DurationField("Durée", help_text="format hh:mm:ss")
desired_slot_nb = models.PositiveIntegerField(
"Nombre de créneaux souhaités", default=1,
validators=[validate_nonzero]
)
available_friday_evening = models.CharField(
"Crénau vendredi soir", choices=Availability.choices, max_length=1,
default=Availability.POSSIBLE,
)
available_friday_night = models.CharField(
"Crénau vendredi nuit", choices=Availability.choices, max_length=1,
default=Availability.POSSIBLE,
)
available_saturday_morning = models.CharField(
"Crénau samedi matin", choices=Availability.choices, max_length=1,
default=Availability.POSSIBLE,
)
available_saturday_afternoon = models.CharField(
"Crénau samedi après-midi", choices=Availability.choices, max_length=1,
default=Availability.POSSIBLE,
)
available_saturday_evening = models.CharField(
"Crénau samedi soir", choices=Availability.choices, max_length=1,
default=Availability.POSSIBLE,
)
available_saturday_night = models.CharField(
"Crénau samedi nuit", choices=Availability.choices, max_length=1,
default=Availability.POSSIBLE,
)
available_sunday_morning = models.CharField(
"Crénau dimanche matin", choices=Availability.choices, max_length=1,
default=Availability.POSSIBLE,
)
available_sunday_afternoon = models.CharField(
"Crénau dimanche après-midi", choices=Availability.choices, max_length=1,
default=Availability.POSSIBLE,
)
constraints = models.TextField(
"Contraintes particulières", max_length=2000, blank=True, null=True
)
status = models.CharField(
"Présentiel/distanciel", choices=Status.choices, max_length=1,
default=Status.PRESENT, blank=True
)
needs = models.TextField(
"Besoin particuliers", max_length=2000, blank=True, null=True
)
comments = models.TextField(
"Commentaires", max_length=2000, blank=True, null=True
)
@property @property
def nb_participants(self) -> str: def nb_participants(self) -> str:
...@@ -91,15 +174,9 @@ class InterludesActivity(models.Model): ...@@ -91,15 +174,9 @@ class InterludesActivity(models.Model):
@property @property
def pretty_type(self) -> str: def pretty_type(self) -> str:
type = self.Types(self.act_type).label type = self.ActivityTypes(self.act_type).label
return type game = self.GameTypes(self.game_type).label
# status = self.Status(self.status) return "{}, {}".format(game, type.lower())
# status_repr = "présentiel ou distanciel"
# if status == self.Status.DISTANT:
# status_repr = "distanciel"
# elif status == self.Status.PRESENT:
# status_repr = "présentiel"
# return "{} ({})".format(type, status_repr)
@property @property
def slug(self) -> str: def slug(self) -> str:
...@@ -109,7 +186,7 @@ class InterludesActivity(models.Model): ...@@ -109,7 +186,7 @@ class InterludesActivity(models.Model):
@property @property
def slots(self): def slots(self):
"""Returns a list of slots related to self""" """Returns a list of slots related to self"""
return InterludesSlot.objects.filter(activity=self, on_planning=True).order_by("start") return SlotModel.objects.filter(activity=self, on_activity=True).order_by("start")
def __str__(self): def __str__(self):
return self.title return self.title
...@@ -118,53 +195,51 @@ class InterludesActivity(models.Model): ...@@ -118,53 +195,51 @@ class InterludesActivity(models.Model):
verbose_name = "activité" verbose_name = "activité"
class InterludesSlot(models.Model): class SlotModel(models.Model):
"""Crénaux indiquant ou une activité se place dans le planning """Crénaux indiquant ou une activité se place dans le planning
Dans une table à part car un activité peut avoir plusieurs créneaux. Dans une table à part car un activité peut avoir plusieurs créneaux.
Les inscriptions se font à des créneaux et non des activités""" Les inscriptions se font à des créneaux et non des activités"""
class Colors(models.TextChoices):
"""Couleur d'affichage dans le planning
Leur code HTML est hardcodé dans la template "_planning.html"."""
DARK_BLUE = "a", "Bleu foncé"
RED = "b", "Rouge"
YELLOW = "c", "Jaune"
BLUE = "d", "Bleu"
GREEN = "e", "Vert"
BLACK = "f", "Noir"
ORANGE = "g", "Orange"
TITLE_SPECIFIER = "{act_title}" TITLE_SPECIFIER = "{act_title}"
activity = models.ForeignKey(InterludesActivity, on_delete=models.CASCADE, verbose_name="Activité") activity = models.ForeignKey(ActivityModel, on_delete=models.CASCADE, verbose_name="Activité")
title = models.CharField( title = models.CharField(
"Titre", max_length=200, default=TITLE_SPECIFIER, "Titre", max_length=200, default=TITLE_SPECIFIER,
help_text="Utilisez '{}' pour insérer le titre de l'activité correspondante".format( help_text="Utilisez '{}' pour insérer le titre de l'activité correspondante".format(
TITLE_SPECIFIER), TITLE_SPECIFIER),
) )
start = models.DateTimeField("début") start = models.DateTimeField("début")
duration = models.DurationField(
"durée", blank=True, null=True,
help_text="Format 00:00:00. Laisser vide pour prendre la durée de l'activité correspondante"
)
room = models.CharField("salle", max_length=100, null=True, blank=True) room = models.CharField("salle", max_length=100, null=True, blank=True)
on_planning = models.BooleanField( on_planning = models.BooleanField(
"afficher sur le planning", default=False, "afficher sur le planning", default=True,
help_text="Nécessite de salle et heure de début non vide", )
on_activity = models.BooleanField(
"afficher dans la description de l'activité", default=True,
) )
subscribing_open = models.BooleanField("ouvert aux inscriptions", default=False, subscribing_open = models.BooleanField("ouvert aux inscriptions", default=False,
help_text="Si vrai, apparaît dans la liste du formulaire d'inscription" help_text="Si vrai, apparaît dans la liste du formulaire d'inscription"
) )
color = models.CharField( color = models.CharField(
"Couleur", choices=Colors.choices, max_length=1, default=Colors.DARK_BLUE "Couleur", choices=Colors.choices, max_length=1, default=Colors.RED,
help_text="La légende des couleurs est modifiable dans les paramètres"
) )
@property @property
def participants(self): def participants(self):
return InterludesActivityChoices.objects.filter(slot=self, accepted=True) return ActivityChoicesModel.objects.filter(slot=self, accepted=True)
@property @property
def end(self): def end(self):
"""Heure de fin du créneau""" """Heure de fin du créneau"""
if self.duration:
return self.start + self.duration
return self.start + self.activity.duration return self.start + self.activity.duration
def conflicts(self, other: "InterludesSlot") -> bool: def conflicts(self, other: "SlotModel") -> bool:
"""Check whether these slots overlap""" """Check whether these slots overlap"""
if self.start <= other.start: if self.start <= other.start:
return other.start <= self.end return other.start <= self.end
...@@ -192,7 +267,7 @@ class InterludesSlot(models.Model): ...@@ -192,7 +267,7 @@ class InterludesSlot(models.Model):
if settings.date_start: if settings.date_start:
time = date.timetz() time = date.timetz()
offset = datetime.timedelta(0) offset = datetime.timedelta(0)
if time.hour <= 4: if time.hour < 4:
offset = datetime.timedelta(days=1) offset = datetime.timedelta(days=1)
return timezone.datetime.combine( return timezone.datetime.combine(
settings.date_start + offset, settings.date_start + offset,
...@@ -216,10 +291,7 @@ class InterludesSlot(models.Model): ...@@ -216,10 +291,7 @@ class InterludesSlot(models.Model):
@property @property
def planning_end(self) -> int: def planning_end(self) -> int:
end = self.fake_date(self.end) return self.fake_date(self.end)
if end and end <= self.planning_start:
end += datetime.timedelta(days = 1)
return end
def __str__(self) -> str: def __str__(self) -> str:
return self.title.replace(self.TITLE_SPECIFIER, self.activity.title) return self.title.replace(self.TITLE_SPECIFIER, self.activity.title)
...@@ -229,7 +301,7 @@ class InterludesSlot(models.Model): ...@@ -229,7 +301,7 @@ class InterludesSlot(models.Model):
verbose_name_plural = "créneaux" verbose_name_plural = "créneaux"
class InterludesParticipant(models.Model): class ParticipantModel(models.Model):
"""un participant aux interludes""" """un participant aux interludes"""
class ENS(models.TextChoices): class ENS(models.TextChoices):
...@@ -249,12 +321,21 @@ class InterludesParticipant(models.Model): ...@@ -249,12 +321,21 @@ class InterludesParticipant(models.Model):
meal_saturday_midday = models.BooleanField("repas de samedi midi", default=False) meal_saturday_midday = models.BooleanField("repas de samedi midi", default=False)
meal_saturday_evening = models.BooleanField("repas de samedi soir", default=False) meal_saturday_evening = models.BooleanField("repas de samedi soir", default=False)
meal_sunday_morning = models.BooleanField("repas de dimanche matin", default=False) meal_sunday_morning = models.BooleanField("repas de dimanche matin", default=False)
meal_sunday_midday = models.BooleanField("repas de dimanche soir", default=False) meal_sunday_midday = models.BooleanField("repas de dimanche midi", default=False)
meal_sunday_evening = models.BooleanField("repas de dimanche soir", default=False)
sleeps = models.BooleanField("dormir sur place", default=False) sleeps = models.BooleanField("dormir sur place", default=False)
paid = models.BooleanField("payé(e)", default=False)
# mug = models.BooleanField("commander une tasse", default=False) # mug = models.BooleanField("commander une tasse", default=False)
nb_murder = models.PositiveIntegerField("Nombre de murder réalisées", default=0)
comment = models.TextField("Commentaire", max_length=2000, blank=True, null=True)
amount_paid = models.PositiveIntegerField("Montant payé", default=0)
def __str__(self) -> str: def __str__(self) -> str:
school = self.ENS(self.school).label.replace("ENS ", "") if self.school else "" school = self.ENS(self.school).label.replace("ENS ", "") if self.school else ""
return "{} {} ({})".format(self.user.first_name, self.user.last_name, school) return "{} {} ({})".format(self.user.first_name, self.user.last_name, school)
...@@ -263,22 +344,28 @@ class InterludesParticipant(models.Model): ...@@ -263,22 +344,28 @@ class InterludesParticipant(models.Model):
def nb_meals(self) -> int: def nb_meals(self) -> int:
return ( return (
self.meal_friday_evening + self.meal_saturday_evening + self.meal_saturday_midday + self.meal_friday_evening + self.meal_saturday_evening + self.meal_saturday_midday +
self.meal_saturday_morning + self.meal_sunday_midday + self.meal_sunday_morning self.meal_saturday_morning + self.meal_sunday_midday + self.meal_sunday_morning +
self.meal_sunday_evening
)
@property
def cost(self) -> int:
return (
(self.is_registered*2 + self.nb_meals) * (2+self.paid) - (self.paid*self.meal_sunday_evening)
) )
class Meta: class Meta:
verbose_name = "participant" verbose_name = "participant"
class InterludesActivityChoices(models.Model): class ActivityChoicesModel(models.Model):
"""liste d'activités souhaitée de chaque participant, """liste d'activités souhaitée de chaque participant,
avec un order de priorité""" avec un order de priorité"""
priority = models.PositiveIntegerField("priorité") priority = models.PositiveIntegerField("priorité")
participant = models.ForeignKey( participant = models.ForeignKey(
InterludesParticipant, on_delete=models.CASCADE, verbose_name="participant", ParticipantModel, on_delete=models.CASCADE, verbose_name="participant",
) )
slot = models.ForeignKey( slot = models.ForeignKey(
InterludesSlot, on_delete=models.CASCADE, verbose_name="créneau", SlotModel, on_delete=models.CASCADE, verbose_name="créneau",
) )
accepted = models.BooleanField("Obtenue", default=False) accepted = models.BooleanField("Obtenue", default=False)
...@@ -289,4 +376,4 @@ class InterludesActivityChoices(models.Model): ...@@ -289,4 +376,4 @@ class InterludesActivityChoices(models.Model):
verbose_name = "choix d'activités" verbose_name = "choix d'activités"
verbose_name_plural = "choix d'activités" verbose_name_plural = "choix d'activités"
EmailUser.profile = property(lambda user: InterludesParticipant.objects.get_or_create(user=user)[0]) EmailUser.profile = property(lambda user: ParticipantModel.objects.get_or_create(user=user)[0])
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
--color_bg_2: #26263c; --color_bg_2: #26263c;
--color_bg_3: #39395c; --color_bg_3: #39395c;
--color_sep: #eb811b; --color_sep: #eb811b;
--color_header: #e4e4e4;
} }
html, body { html, body {
...@@ -40,7 +41,7 @@ header { ...@@ -40,7 +41,7 @@ header {
} }
header > * { header > * {
color: #ddd; color: var(--color_header);
} }
header h1, header h1 a, header h1 a:visited, header h1 a:active { header h1, header h1 a, header h1 a:visited, header h1 a:active {
...@@ -50,6 +51,14 @@ header h1, header h1 a, header h1 a:visited, header h1 a:active { ...@@ -50,6 +51,14 @@ header h1, header h1 a, header h1 a:visited, header h1 a:active {
margin: 10px 0; margin: 10px 0;
flex: 1; flex: 1;
} }
header h1 a {
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 img {
height: 80px;
}
header #head_main_infos { header #head_main_infos {
display: flex; display: flex;
...@@ -63,12 +72,15 @@ header #head_main_infos { ...@@ -63,12 +72,15 @@ header #head_main_infos {
header h1, header h1 a { header h1, header h1 a {
font-size: 50px; font-size: 50px;
} }
header h1 img {
height: 60px;
}
header #head_main_infos { header #head_main_infos {
font-size: 15pt; font-size: 15pt;
} }
} }
@media (max-width: 600px) { @media (max-width: 700px) {
header { header, header h1 a {
flex-direction: column; flex-direction: column;
} }
header h1, header h1 a { header h1, header h1 a {
...@@ -83,6 +95,15 @@ header #head_main_infos { ...@@ -83,6 +95,15 @@ header #head_main_infos {
} }
} }
div.easter_egg {
display: inline-block;
}
#circle {
-webkit-clip-path: circle(50% at 50% 50%);
clip-path: circle(50% at 50% 50%)
}
/* =========================== /* ===========================
// Navbar // Navbar
...@@ -93,7 +114,7 @@ nav { ...@@ -93,7 +114,7 @@ nav {
display: flex; display: flex;
justify-content: center space-around; justify-content: center space-around;
background-color: var(--color_bg_2); background-color: var(--color_bg_2);
color: white; color: var(--color_header);
margin: 0; margin: 0;
padding: 0 20px; padding: 0 20px;
border-bottom: 6px solid var(--color_bg_1); border-bottom: 6px solid var(--color_bg_1);
...@@ -102,7 +123,7 @@ nav { ...@@ -102,7 +123,7 @@ nav {
nav a { nav a {
padding: 5px; padding: 5px;
font-size: 20px; font-size: 20px;
color: white; color: var(--color_header);
text-decoration: none; text-decoration: none;
text-emphasis: bold; text-emphasis: bold;
width: 100%; width: 100%;
...@@ -114,6 +135,10 @@ nav a:hover { ...@@ -114,6 +135,10 @@ nav a:hover {
background-color: var(--color_bg_3); background-color: var(--color_bg_3);
transition-duration: 0.5s; transition-duration: 0.5s;
} }
nav a:focus {
background-color: var(--color_bg_3);
}
nav a.current { nav a.current {
background-color: var(--color_bg_1); background-color: var(--color_bg_1);
...@@ -203,6 +228,10 @@ main p { ...@@ -203,6 +228,10 @@ main p {
strong { strong {
font-weight: bold; font-weight: bold;
} }
.underline {
text-decoration: underline;
}
main a:link { main a:link {
text-decoration: underline; text-decoration: underline;
...@@ -422,40 +451,54 @@ ul.messagelist li.info:before { ...@@ -422,40 +451,54 @@ ul.messagelist li.info:before {
height: 35px; height: 35px;
margin: 0 5px 0 0; margin: 0 5px 0 0;
} }
#transport-metro-icon, #transport-ratp-metro-icon,
#transport-metro-stop, #transport-ratp-metro-stop,
#transport-bus-1-icon, #transport-ratp-bus-1-icon,
#transport-bus-1-stop { #transport-ratp-bus-1-stop,
#transport-tcl-metro,
#transport-tcl-bus-1 {
grid-row: 1; grid-row: 1;
} }
#transport-rer-icon, #transport-ratp-rer-icon,
#transport-rer-stop, #transport-ratp-rer-stop,
#transport-bus-2-icon, #transport-ratp-bus-2-icon,
#transport-bus-2-stop { #transport-ratp-bus-2-stop,
#transport-tcl-tram-1,
#transport-tcl-bus-2,
#transport-tcl-stop {
grid-row: 2; grid-row: 2;
} }
#transport-noctilien-icon, #transport-ratp-noctilien-icon,
#transport-noctilien-stop { #transport-ratp-noctilien-stop,
#transport-tcl-tram-2,
#transport-tcl-bus-3 {
grid-row: 3; grid-row: 3;
} }
#transport-metro-icon, #transport-ratp-metro-icon,
#transport-rer-icon { #transport-ratp-rer-icon{
grid-column: 1; grid-column: 1;
justify-self: end; justify-self: end;
} }
#transport-metro-stop, #transport-ratp-metro-stop,
#transport-rer-stop { #transport-ratp-rer-stop,
#transport-tcl-stop {
grid-column: 2; grid-column: 2;
} }
#transport-bus-1-icon, #transport-ratp-bus-1-icon,
#transport-bus-2-icon, #transport-ratp-bus-2-icon,
#transport-noctilien-icon { #transport-ratp-noctilien-icon,
#transport-tcl-metro,
#transport-tcl-tram-1,
#transport-tcl-tram-2 {
grid-column: 3; grid-column: 3;
justify-self: end; justify-self: end;
} }
#transport-bus-1-stop, #transport-ratp-bus-1-stop,
#transport-bus-2-stop, #transport-ratp-bus-2-stop,
#transport-noctilien-stop { #transport-ratp-noctilien-stop,
#transport-tcl-bus-1,
#transport-tcl-bus-2,
#transport-tcl-bus-3 {
grid-column: 4; grid-column: 4;
} }
...@@ -468,39 +511,39 @@ ul.messagelist li.info:before { ...@@ -468,39 +511,39 @@ ul.messagelist li.info:before {
grid-template-rows: auto auto auto auto auto; grid-template-rows: auto auto auto auto auto;
align-items: center; align-items: center;
} }
#transport-metro-icon, #transport-ratp-metro-icon,
#transport-metro-stop { #transport-ratp-metro-stop {
grid-row: 1; grid-row: 1;
} }
#transport-rer-icon, #transport-ratp-rer-icon,
#transport-rer-stop { #transport-ratp-rer-stop {
grid-row: 2; grid-row: 2;
} }
#transport-bus-1-icon, #transport-ratp-bus-1-icon,
#transport-bus-1-stop { #transport-ratp-bus-1-stop {
grid-row: 3; grid-row: 3;
} }
#transport-bus-2-icon, #transport-ratp-bus-2-icon,
#transport-bus-2-stop { #transport-ratp-bus-2-stop {
grid-row: 4; grid-row: 4;
} }
#transport-noctilien-icon, #transport-ratp-noctilien-icon,
#transport-noctilien-stop { #transport-ratp-noctilien-stop {
grid-row: 5; grid-row: 5;
} }
#transport-metro-icon, #transport-ratp-metro-icon,
#transport-rer-icon, #transport-ratp-rer-icon,
#transport-bus-1-icon, #transport-ratp-bus-1-icon,
#transport-bus-2-icon, #transport-ratp-bus-2-icon,
#transport-noctilien-icon { #transport-ratp-noctilien-icon {
grid-column: 1; grid-column: 1;
justify-self: end; justify-self: end;
} }
#transport-metro-stop, #transport-ratp-metro-stop,
#transport-rer-stop, #transport-ratp-rer-stop,
#transport-bus-1-stop, #transport-ratp-bus-1-stop,
#transport-bus-2-stop, #transport-ratp-bus-2-stop,
#transport-noctilien-stop { #transport-ratp-noctilien-stop {
grid-column: 2; grid-column: 2;
} }
} }
...@@ -523,7 +566,7 @@ footer { ...@@ -523,7 +566,7 @@ footer {
bottom: 0; bottom: 0;
width: 100%; width: 100%;
text-align: center; text-align: center;
color: white; color: var(--color_header);
display: flex; display: flex;
background-color: var(--color_bg_1); background-color: var(--color_bg_1);
border-top: 5px solid var(--color_bg_2); border-top: 5px solid var(--color_bg_2);
......
home/static/imgs/2021/favicon.ico

177 KiB

home/static/imgs/2021/logo.png

184 KiB

home/static/imgs/2021/logo_easter_egg.png

207 KiB

home/static/imgs/2021/logo_grey.png

183 KiB