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 1388 additions and 127 deletions
{% autoescape off %}
Bonjour {{ user.first_name }} {{ user.last_name }},
{% if settings.discord_link %}
Vous pouvez rejoindre notre serveur discord avec le lien suivant : {{ settings.discord_link }}
{% endif %}
Les inscriptions aux Interludes sont fermées et la répartition des activités à été effectuée.
{% if requested_activities_nb %}
Vous avez obtenu {{ my_choices|length }} activité(s) (sur {{ requested_activities_nb }} souhaitée(s)).
{% for choice in my_choices %}
- {{ choice.slot }}{% if choice.slot.on_planning %} (le {{ choice.slot.start|date:"l à H:i" }}){% endif %}{% endfor %}{% if activities %}
Cette liste est également disponible sur la page "Mon compte" du site: {% url "profile" %}.
Votre adresse email sera communiquée aux orgas pour les activités nécessitant préparation avant l'événement.{% endif %}
{% else %}
Vous n'aviez demandé aucune activité.
{% endif %}
--
L'équipe Interludes
{% if settings.contact_email %}Pour nous contacter, envoyer un email à {{ settings.contact_email }}{% endif %}
{% endautoescape %}
{% extends "base.html" %}
{% load static %}
{% block "content" %}
<h2>Envoyer un email</h2>
<p>
Ce formulaire permet d'envoyer un mail à tous les comptes du site
(ou seulement aux comptes actuellements inscrits).
À utiliser avec modération.
</p>
<ul>
<li>Nombre de comptes : {{ accounts_nb }}</li>
<li>Nombre d'inscrits : {{ registered_nb }}</li>
</ul>
<form method="post" action="{% url 'admin_pages:email_new' %}">
{% csrf_token %}
<table>
<tr><td><strong>De :</strong></td><td>{{ from_email }}</td></tr>
<tr><td><strong>Envoyer à : <strong></td><td>{{ form.dest }}</td></tr>
<tr><td><strong>Sujet : <strong></td><td>{{ form.subject }}</td></tr>
</table>
{{ form.text }}
<br>
<div class="flex">
<input type="submit" value="Envoyer">
<a class="button" href="{% url 'admin_pages:index' %}">Annuler</a>
</div>
</form>
{% endblock %}
from django.test import TestCase
# Create your tests here.
from django.urls import path, include
from admin_pages import views
urlpatterns = [
path('', views.AdminView.as_view(), name="index"),
path('export/activities/', views.ExportActivities.as_view(), name="activities.csv"),
path('export/slots/', views.ExportSlots.as_view(), name="slots.csv"),
path('export/participants/', views.ExportParticipants.as_view(), name="participants.csv"),
path('export/activity_choices/', views.ExportActivityChoices.as_view(), name="activity_choices.csv"),
path('email/send_user_emails_0564946523/', views.SendUserEmail.as_view(), name="email_users"),
path('email/send_orga_emails_5682480453/', views.SendOrgaEmail.as_view(), name="email_orgas"),
path('email/new_email/', views.NewEmail.as_view(), name="email_new"),
]
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 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()
saclay = registered.filter(school=models.ParticipantModel.ENS.ENS_CACHAN).count()
exterieur = registered.filter(school=models.ParticipantModel.ENS.EXTERIEUR).count()
non_registered = EmailUser.objects.filter(is_active=True).count() - participants
# mugs = registered.filter(mug=True).count()
sleeps = registered.filter(sleeps=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()
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 soir"
]
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,
])
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, participant__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((
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, 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((
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, 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,
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())
from django.contrib import admin
from home.models import InterludesActivity, InterludesParticipant, ActivityList
from home import models
from shared.admin import ExportCsvMixin
# Titre de la vue (tag <h1>)
......@@ -9,40 +9,92 @@ admin.site.site_header = "Administration site interludes"
admin.site.site_title = "Admin Interludes"
@admin.register(InterludesActivity)
class InterludesActivityAdmin(ExportCsvMixin, admin.ModelAdmin):
@admin.register(models.ActivityModel)
class ActivityModelAdmin(ExportCsvMixin, admin.ModelAdmin):
"""option d'affichage des activités dans la vue django admin"""
list_display = ("title", "host_name", "display", "must_subscribe","on_planning")
list_filter = ("display", "must_subscribe", "on_planning")
filename = "export_activites.csv"
list_display = ("title", "host_name", "display", "must_subscribe",)
list_filter = ("display", "must_subscribe", "status",)
ordering = ("title", "host_name",)
list_editable = ("display",)
fields = (
"title",
("host_name", "host_email"),
"status", "act_type", "duration",
"title", "display",
("host_name", "host_email"), "show_email",
"host_info",
"act_type", "game_type",
"description", "desc_as_html",
("min_participants", "max_participants"),
"must_subscribe",
"description",
"display",
"room", "start",
"on_planning",
"notes"
"communicate_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",
)
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(InterludesParticipant)
class InterludesParticipantAdmin(ExportCsvMixin, admin.ModelAdmin):
@admin.register(models.SlotModel)
class SlotModelAdmin(ExportCsvMixin, admin.ModelAdmin):
"""option d'affichage des créneaux dans la vue d'admin"""
filename = "export_slots.csv"
csv_export_fields = (
"activity_id", "title",
"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",)
@admin.register(models.ParticipantModel)
class ParticipantModelAdmin(ExportCsvMixin, admin.ModelAdmin):
"""option d'affichage des participant dans la vue django admin"""
filename = "export_participants.csv"
list_display = ("user", "school", "is_registered")
list_filter = ("school", "is_registered")
list_filter = (
"school", "is_registered", "sleeps",
"meal_friday_evening", "meal_saturday_morning", "meal_saturday_midday",
"meal_saturday_evening", "meal_sunday_morning", "meal_sunday_midday",
)
ordering = ("user",)
list_per_page = 200
@admin.register(ActivityList)
@admin.register(models.ActivityChoicesModel)
class ActivityListAdmin(ExportCsvMixin, admin.ModelAdmin):
"""option d'affichage des choix d'activités dans la vue django admin"""
list_display = ("activity", "participant", "priority", "accepted")
list_filter = ("activity", "participant__is_registered", "participant")
filename = "export_choix_activite.csv"
list_display = ("slot", "participant", "priority", "accepted")
list_filter = (
"slot__activity", "participant__is_registered", "slot__activity__display",
"accepted", "slot__subscribing_open",
)
list_editable = ("accepted",)
ordering = ("activity", "priority", "participant",)
list_per_page = 200
ordering = ("slot", "priority", "participant",)
list_per_page = 400
from django import forms
from django.core.exceptions import ValidationError
from home.models import ActivityList, InterludesParticipant, InterludesActivity
from home import models
from shared.forms import FormRenderMixin
class InscriptionForm(FormRenderMixin, forms.ModelForm):
class Meta:
model = InterludesParticipant
model = models.ParticipantModel
fields = (
"school", "sleeps", # "mug",
"meal_friday_evening", "meal_saturday_morning", "meal_saturday_midday",
......@@ -31,14 +31,14 @@ class InscriptionForm(FormRenderMixin, forms.ModelForm):
class ActivityForm(FormRenderMixin, forms.ModelForm):
class Meta:
model = ActivityList
fields = ("activity",)
labels = {"activity":""}
model = models.ActivityChoicesModel
fields = ("slot",)
labels = {"slot":""}
def __init__(self, *args, **kwargs):
super(ActivityForm, self).__init__(*args, **kwargs)
activities = InterludesActivity.objects.filter(display=True, must_subscribe=True)
self.fields['activity'].queryset = activities
slots = models.SlotModel.objects.filter(subscribing_open=True)
self.fields['slot'].queryset = slots
class BaseActivityFormSet(forms.BaseFormSet):
"""Form set that fails if duplicate activities"""
......@@ -51,9 +51,59 @@ class BaseActivityFormSet(forms.BaseFormSet):
for form in self.forms:
if self.can_delete and self._should_delete_form(form):
continue
activity = form.cleaned_data.get('activity')
activity = form.cleaned_data.get('slot')
if activity is None:
continue
if activity in activities:
raise ValidationError("Vous ne pouvez pas sélectionner une même activtté plusieurs fois")
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.2.7 on 2021-10-05 18:45
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import home.models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ActivityModel',
fields=[
('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')),
('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é")),
('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')),
('description', models.TextField(help_text='Texte ou html selon la valeur de "Description HTML".\n', max_length=10000, verbose_name='description')),
('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')),
('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")),
('max_participants', models.PositiveIntegerField(default=0, help_text='0 pour illimité', verbose_name='Nombre maximum de participants')),
('min_participants', models.PositiveIntegerField(default=0, verbose_name='Nombre minimum de participants')),
('duration', models.DurationField(help_text='format hh:mm:ss', verbose_name='Durée')),
('desired_slot_nb', models.PositiveIntegerField(default=1, validators=[home.models.validate_nonzero], verbose_name='Nombre de créneaux souhaités')),
('available_friday_evening', models.CharField(choices=[('0', 'Idéal'), ('1', 'Acceptable'), ('2', 'Indisponible')], default='1', max_length=1, verbose_name='Crénau vendredi soir')),
('available_friday_night', models.CharField(choices=[('0', 'Idéal'), ('1', 'Acceptable'), ('2', 'Indisponible')], default='1', max_length=1, verbose_name='Crénau vendredi nuit')),
('available_saturday_morning', models.CharField(choices=[('0', 'Idéal'), ('1', 'Acceptable'), ('2', 'Indisponible')], default='1', max_length=1, verbose_name='Crénau samedi matin')),
('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')),
('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={
'verbose_name': 'activité',
},
),
migrations.CreateModel(
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=[
('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')),
('is_registered', models.BooleanField(default=False, verbose_name='est inscrit')),
('meal_friday_evening', models.BooleanField(default=False, verbose_name='repas de vendredi soir')),
('meal_saturday_morning', models.BooleanField(default=False, verbose_name='repas de samedi matin')),
('meal_saturday_midday', models.BooleanField(default=False, verbose_name='repas de samedi midi')),
('meal_saturday_evening', models.BooleanField(default=False, verbose_name='repas de samedi soir')),
('meal_sunday_morning', models.BooleanField(default=False, verbose_name='repas de dimanche matin')),
('meal_sunday_midday', models.BooleanField(default=False, verbose_name='repas de dimanche soir')),
('sleeps', models.BooleanField(default=False, verbose_name='dormir sur place')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='Utilisateur', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'participant',
},
),
migrations.CreateModel(
name='ActivityChoicesModel',
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.participantmodel', verbose_name='participant')),
('slot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.slotmodel', verbose_name='créneau')),
],
options={
'verbose_name': "choix d'activités",
'verbose_name_plural': "choix d'activités",
'ordering': ('participant', 'priority'),
'unique_together': {('participant', 'slot'), ('priority', 'participant')},
},
),
]
# Generated by Django 3.2.15 on 2022-08-21 13:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='activitymodel',
name='status',
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'),
),
migrations.AlterField(
model_name='participantmodel',
name='school',
field=models.CharField(choices=[('C', 'ENS Paris-Saclay'), ('E', 'Extérieur')], max_length=1, verbose_name='ENS Paris-Saclay ou extérieur'),
),
]
import datetime
from django.db import models
from django.forms import ValidationError
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from accounts.models import EmailUser
from site_settings.models import Colors, SiteSettings
def validate_nonzero(value):
"""Make a positive integer field non-zero"""
if value == 0:
raise ValidationError(
_('Cette valeur doit-être non-nulle'),
)
class InterludesActivity(models.Model):
class ActivityModel(models.Model):
"""une activité des interludes (i.e. JDR, murder)..."""
class Status(models.TextChoices):
......@@ -12,12 +24,16 @@ class InterludesActivity(models.Model):
DISTANT = "D", _("En distanciel uniquement")
BOTH = "2", _("Les deux")
class Types(models.TextChoices):
"""types d'activités"""
class ActivityTypes(models.TextChoices):
"""quantité d'activité"""
GAME = "1 partie", _("Une partie")
GAMES = "2+ parties", _("Quelques parties")
TOURNAMENT = "Tournoi", _("Tournoi")
GAME = "partie", _("Une partie")
GAMES = "parties", _("Quelques parties")
FREEPLAY = "freeplay", _("Freeplay")
OTHER = "other", _("Autre")
class GameTypes(models.TextChoices):
"""types de jeu"""
CARD_GAME = "jeu cartes", _("Jeu de cartes")
BOARD_GAME = "jeu plateau", _("Jeu de société")
TABLETOP_RPG = "table RPG", _("Jeu de rôle sur table")
......@@ -29,45 +45,121 @@ class InterludesActivity(models.Model):
COOP = "coop", _("Jeu coopératif")
OTHER = "other", _("Autre")
class Availability(models.TextChoices):
"""Diponibilité à un moment donné"""
IDEAL = "0", _("Idéal")
POSSIBLE = "1", _("Acceptable")
UNAVAILABLE = "2", _("Indisponible")
display = models.BooleanField("afficher dans la liste", default=False,
help_text="Si vrai, s'affiche sur la page activités"
)
show_email = models.BooleanField("afficher l'email de l'orga", default=True,
help_text="Si l'affichage d'email global et cette case sont vrai, affiche l'email de l'orga"
)
title = models.CharField("Titre", max_length=200)
status = models.CharField("Présentiel/distanciel", choices=Status.choices, max_length=1)
act_type = models.CharField("Type", choices=Types.choices, max_length=12)
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'
)
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."
)
duration = models.DurationField("Durée", help_text="format hh:mm:ss")
host = models.ForeignKey(
EmailUser, on_delete=models.SET_NULL, verbose_name="Organisateur",
blank=True, null=True
)
host_name = models.CharField(
"nom de l'organisateur", max_length=50, null=True, blank=True,
help_text="Peut-être laissé vide pour des simples activités sans orga"
)
host_email = models.EmailField(
"email de l'organisateur",
help_text="Utilisé pour communiquer la liste des participants si demandé"
)
host_info = models.TextField(
"Autre orgas/contacts", max_length=1000, blank=True, null=True
)
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é"
"Nombre maximum de participants", help_text="0 pour illimité", default=0
)
min_participants = models.PositiveIntegerField(
"Nombre minimum de participants"
"Nombre minimum de participants", default=0
)
display = models.BooleanField("afficher dans la liste d'activités", default=False)
must_subscribe = models.BooleanField("sur inscription", default=False,
help_text="Une activité doit être affichée dans la liste également pour que l'on puisse si inscrire"
## 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]
)
host_name = models.CharField("nom de l'organisateur", max_length=50)
host_email = models.EmailField("email de l'organisateur")
description = models.TextField("description", max_length=2000)
on_planning = models.BooleanField(
"afficher sur le planning", default=False,
help_text="Nécessite de salle et heure de début non vide"
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,
)
start = models.DateTimeField("début", null=True, blank=True)
room = models.CharField("salle", max_length=100, null=True, blank=True)
notes = models.TextField("Notes privées", max_length=2000, blank=True)
constraints = models.TextField(
"Contraintes particulières", max_length=2000, blank=True, null=True
)
@property
def end(self):
if (not self.start) or (not self.duration):
return None
return self.start + self.duration
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
def nb_participants(self) -> str:
if self.max_participants == 0:
ret = "Illimités"
elif self.max_participants == self.min_participants:
ret = "{}".format(self.min_participants)
else:
ret = "{} - {}".format(self.min_participants, self.max_participants)
if self.must_subscribe:
......@@ -82,15 +174,19 @@ class InterludesActivity(models.Model):
@property
def pretty_type(self) -> str:
return self.Types(self.act_type).label
type = self.ActivityTypes(self.act_type).label
game = self.GameTypes(self.game_type).label
return "{}, {}".format(game, type.lower())
def conflicts(self, other: "InterludesActivity") -> bool:
"""Check whether these activites overlap"""
if self.end is None or other.end is None:
return False
if self.start <= other.start:
return other.start <= self.end
return self.start <= other.end
@property
def slug(self) -> str:
"""Returns the planning/display slug for this activity"""
return "act-{}".format(self.id)
@property
def slots(self):
"""Returns a list of slots related to self"""
return SlotModel.objects.filter(activity=self, on_activity=True).order_by("start")
def __str__(self):
return self.title
......@@ -99,18 +195,125 @@ class InterludesActivity(models.Model):
verbose_name = "activité"
class InterludesParticipant(models.Model):
class SlotModel(models.Model):
"""Crénaux indiquant ou une activité se place dans le planning
Dans une table à part car un activité peut avoir plusieurs créneaux.
Les inscriptions se font à des créneaux et non des activités"""
TITLE_SPECIFIER = "{act_title}"
activity = models.ForeignKey(ActivityModel, on_delete=models.CASCADE, verbose_name="Activité")
title = models.CharField(
"Titre", max_length=200, default=TITLE_SPECIFIER,
help_text="Utilisez '{}' pour insérer le titre de l'activité correspondante".format(
TITLE_SPECIFIER),
)
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)
on_planning = models.BooleanField(
"afficher sur le planning", default=True,
)
on_activity = models.BooleanField(
"afficher dans la description de l'activité", default=True,
)
subscribing_open = models.BooleanField("ouvert aux inscriptions", default=False,
help_text="Si vrai, apparaît dans la liste du formulaire d'inscription"
)
color = models.CharField(
"Couleur", choices=Colors.choices, max_length=1, default=Colors.RED,
help_text="La légende des couleurs est modifiable dans les paramètres"
)
@property
def participants(self):
return ActivityChoicesModel.objects.filter(slot=self, accepted=True)
@property
def end(self):
"""Heure de fin du créneau"""
if self.duration:
return self.start + self.duration
return self.start + self.activity.duration
def conflicts(self, other: "SlotModel") -> bool:
"""Check whether these slots overlap"""
if self.start <= other.start:
return other.start <= self.end
return self.start <= other.end
@staticmethod
def relative_day(date: datetime.datetime) -> int:
"""Relative day to start.
- friday 04:00 -> 03:59 = day 0
- saturday 04:00 -> 03:59 = day 1
- sunday 04:00 -> 03:59 = day 2
returns 0 if no date_start is defined in settings"""
settings = SiteSettings.load()
if settings.date_start:
return (date - timezone.datetime.combine(
settings.date_start, datetime.time(hour=4), timezone.get_current_timezone()
)).days
else:
return 0
@staticmethod
def fake_date(date: datetime.datetime):
"""Fake day for display on the (single day planning)"""
settings = SiteSettings.load()
if settings.date_start:
time = date.timetz()
offset = datetime.timedelta(0)
if time.hour < 4:
offset = datetime.timedelta(days=1)
return timezone.datetime.combine(
settings.date_start + offset,
date.timetz()
)
return None
@property
def start_day(self) -> int:
"""returns a day (0-2)"""
return self.relative_day(self.start)
@property
def end_day(self) -> int:
"""returns a day (0-2)"""
return self.relative_day(self.end)
@property
def planning_start(self) -> int:
return self.fake_date(self.start)
@property
def planning_end(self) -> int:
return self.fake_date(self.end)
def __str__(self) -> str:
return self.title.replace(self.TITLE_SPECIFIER, self.activity.title)
class Meta:
verbose_name = "créneau"
verbose_name_plural = "créneaux"
class ParticipantModel(models.Model):
"""un participant aux interludes"""
class ENS(models.TextChoices):
"""enum representant les ENS"""
ENS_ULM = "U", _("ENS Ulm")
ENS_LYON = "L", _("ENS Lyon")
ENS_RENNES = "R", _("ENS Rennes")
ENS_CACHAN = "C", _("ENS Paris Saclay")
#ENS_ULM = "U", _("ENS Ulm")
#ENS_LYON = "L", _("ENS Lyon")
#ENS_RENNES = "R", _("ENS Rennes")
ENS_CACHAN = "C", _("ENS Paris-Saclay")
EXTERIEUR = "E", _("Extérieur")
user = models.OneToOneField(EmailUser, on_delete=models.CASCADE, related_name="Utilisateur")
school = models.CharField("ENS de rattachement", choices=ENS.choices, max_length=1)
school = models.CharField("ENS Paris-Saclay ou extérieur", choices=ENS.choices, max_length=1)
is_registered = models.BooleanField("est inscrit", default=False)
......@@ -126,7 +329,8 @@ class InterludesParticipant(models.Model):
# mug = models.BooleanField("commander une tasse", default=False)
def __str__(self) -> str:
return "{} {} ({})".format(self.user.first_name, self.user.last_name, self.school)
school = self.ENS(self.school).label if self.school else ""
return "{} {} ({})".format(self.user.first_name, self.user.last_name, school)
@property
def nb_meals(self) -> int:
......@@ -139,23 +343,23 @@ class InterludesParticipant(models.Model):
verbose_name = "participant"
class ActivityList(models.Model):
class ActivityChoicesModel(models.Model):
"""liste d'activités souhaitée de chaque participant,
avec un order de priorité"""
priority = models.PositiveIntegerField("priorité")
participant = models.ForeignKey(
InterludesParticipant, on_delete=models.CASCADE, db_column="participant"
ParticipantModel, on_delete=models.CASCADE, verbose_name="participant",
)
activity = models.ForeignKey(
InterludesActivity, on_delete=models.CASCADE, db_column="activité"
slot = models.ForeignKey(
SlotModel, on_delete=models.CASCADE, verbose_name="créneau",
)
accepted = models.BooleanField("Obtenue", default=False)
class Meta:
# couples uniques
unique_together = (("priority", "participant"), ("participant", "activity"))
unique_together = (("priority", "participant"), ("participant", "slot"))
ordering = ("participant", "priority")
verbose_name = "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 @@
--color_bg_2: #26263c;
--color_bg_3: #39395c;
--color_sep: #eb811b;
--color_header: #e4e4e4;
}
html, body {
......@@ -40,7 +41,7 @@ header {
}
header > * {
color: #ddd;
color: var(--color_header);
}
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;
flex: 1;
}
header h1 a {
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 img {
height: 80px;
}
header #head_main_infos {
display: flex;
......@@ -63,19 +72,38 @@ header #head_main_infos {
header h1, header h1 a {
font-size: 50px;
}
header h1 img {
height: 60px;
}
header #head_main_infos {
font-size: 10pt;
font-size: 15pt;
}
}
@media (max-width: 600px) {
@media (max-width: 700px) {
header, header h1 a {
flex-direction: column;
}
header h1, header h1 a {
font-size: 30px;
}
header #head_main_infos {
font-size: 8pt;
font-size: 12pt;
flex-direction: row;
}
header #head_main_infos div {
padding: 0 10px 10px;
}
}
div.easter_egg {
display: inline-block;
}
#circle {
-webkit-clip-path: circle(50% at 50% 50%);
clip-path: circle(50% at 50% 50%)
}
/* ===========================
// Navbar
......@@ -86,7 +114,7 @@ nav {
display: flex;
justify-content: center space-around;
background-color: var(--color_bg_2);
color: white;
color: var(--color_header);
margin: 0;
padding: 0 20px;
border-bottom: 6px solid var(--color_bg_1);
......@@ -95,7 +123,7 @@ nav {
nav a {
padding: 5px;
font-size: 20px;
color: white;
color: var(--color_header);
text-decoration: none;
text-emphasis: bold;
width: 100%;
......@@ -107,6 +135,10 @@ nav a:hover {
background-color: var(--color_bg_3);
transition-duration: 0.5s;
}
nav a:focus {
background-color: var(--color_bg_3);
}
nav a.current {
background-color: var(--color_bg_1);
......@@ -154,7 +186,6 @@ main {
padding: 1px 30px 50px;
max-width: 920px;
margin: 0 auto 0 auto;
min-height: calc(100vh - 370px); /* viewport-height - (header+footer+nav bar) */
}
main h2 {
......@@ -176,6 +207,18 @@ main h3 {
border-image-source: linear-gradient(to right, var(--color_bg_1) 0%, transparent 75%);
border-image-slice: 1;
}
@media (max-width: 600px) {
main {
padding-left: 10px;
padding-right: 10px;
}
main h2 {
font-size: 1.1rem;
}
main h3 {
font-size: 1rem;
}
}
main p {
......@@ -185,6 +228,10 @@ main p {
strong {
font-weight: bold;
}
.underline {
text-decoration: underline;
}
main a:link {
text-decoration: underline;
......@@ -227,6 +274,11 @@ main a:link {
.stat .nb_small {
font-size: 2em;
}
@media (max-width:600px) {
.stat {
min-width: 0;
}
}
/* ===========================
// Lists
......@@ -238,6 +290,7 @@ dl {
grid-template-columns: auto auto;
justify-content: left;
padding-left: 10px;
margin-bottom: 5px;
}
dl dt {
justify-self: end;
......@@ -249,6 +302,16 @@ dl dd {
justify-self: start;
text-align: left;
}
div.desc {
margin-top: 0;
padding: 0 10px 20px;
}
div.desc p {
margin: 0;
}
div.desc p.indent {
text-indent: 25px;
}
/* ===========================
// Forms
......@@ -267,6 +330,11 @@ dl dd {
.button:hover, input[type=submit]:hover {
background-color: var(--color_bg_2);
}
.disabled, .disabled:hover {
color: black;
background-color: #888;
cursor: not-allowed;
}
span.helptext {
color: #444444;
......@@ -383,40 +451,54 @@ ul.messagelist li.info:before {
height: 35px;
margin: 0 5px 0 0;
}
#transport-metro-icon,
#transport-metro-stop,
#transport-bus-1-icon,
#transport-bus-1-stop {
#transport-ratp-metro-icon,
#transport-ratp-metro-stop,
#transport-ratp-bus-1-icon,
#transport-ratp-bus-1-stop,
#transport-tcl-metro,
#transport-tcl-bus-1 {
grid-row: 1;
}
#transport-rer-icon,
#transport-rer-stop,
#transport-bus-2-icon,
#transport-bus-2-stop {
#transport-ratp-rer-icon,
#transport-ratp-rer-stop,
#transport-ratp-bus-2-icon,
#transport-ratp-bus-2-stop,
#transport-tcl-tram-1,
#transport-tcl-bus-2,
#transport-tcl-stop {
grid-row: 2;
}
#transport-noctilien-icon,
#transport-noctilien-stop {
#transport-ratp-noctilien-icon,
#transport-ratp-noctilien-stop,
#transport-tcl-tram-2,
#transport-tcl-bus-3 {
grid-row: 3;
}
#transport-metro-icon,
#transport-rer-icon {
#transport-ratp-metro-icon,
#transport-ratp-rer-icon{
grid-column: 1;
justify-self: end;
}
#transport-metro-stop,
#transport-rer-stop {
#transport-ratp-metro-stop,
#transport-ratp-rer-stop,
#transport-tcl-stop {
grid-column: 2;
}
#transport-bus-1-icon,
#transport-bus-2-icon,
#transport-noctilien-icon {
#transport-ratp-bus-1-icon,
#transport-ratp-bus-2-icon,
#transport-ratp-noctilien-icon,
#transport-tcl-metro,
#transport-tcl-tram-1,
#transport-tcl-tram-2 {
grid-column: 3;
justify-self: end;
}
#transport-bus-1-stop,
#transport-bus-2-stop,
#transport-noctilien-stop {
#transport-ratp-bus-1-stop,
#transport-ratp-bus-2-stop,
#transport-ratp-noctilien-stop,
#transport-tcl-bus-1,
#transport-tcl-bus-2,
#transport-tcl-bus-3 {
grid-column: 4;
}
......@@ -429,39 +511,39 @@ ul.messagelist li.info:before {
grid-template-rows: auto auto auto auto auto;
align-items: center;
}
#transport-metro-icon,
#transport-metro-stop {
#transport-ratp-metro-icon,
#transport-ratp-metro-stop {
grid-row: 1;
}
#transport-rer-icon,
#transport-rer-stop {
#transport-ratp-rer-icon,
#transport-ratp-rer-stop {
grid-row: 2;
}
#transport-bus-1-icon,
#transport-bus-1-stop {
#transport-ratp-bus-1-icon,
#transport-ratp-bus-1-stop {
grid-row: 3;
}
#transport-bus-2-icon,
#transport-bus-2-stop {
#transport-ratp-bus-2-icon,
#transport-ratp-bus-2-stop {
grid-row: 4;
}
#transport-noctilien-icon,
#transport-noctilien-stop {
#transport-ratp-noctilien-icon,
#transport-ratp-noctilien-stop {
grid-row: 5;
}
#transport-metro-icon,
#transport-rer-icon,
#transport-bus-1-icon,
#transport-bus-2-icon,
#transport-noctilien-icon {
#transport-ratp-metro-icon,
#transport-ratp-rer-icon,
#transport-ratp-bus-1-icon,
#transport-ratp-bus-2-icon,
#transport-ratp-noctilien-icon {
grid-column: 1;
justify-self: end;
}
#transport-metro-stop,
#transport-rer-stop,
#transport-bus-1-stop,
#transport-bus-2-stop,
#transport-noctilien-stop {
#transport-ratp-metro-stop,
#transport-ratp-rer-stop,
#transport-ratp-bus-1-stop,
#transport-ratp-bus-2-stop,
#transport-ratp-noctilien-stop {
grid-column: 2;
}
}
......@@ -484,9 +566,7 @@ footer {
bottom: 0;
width: 100%;
text-align: center;
padding: 10px 0;
height: 50px;
color: white;
color: var(--color_header);
display: flex;
background-color: var(--color_bg_1);
border-top: 5px solid var(--color_bg_2);
......@@ -498,4 +578,21 @@ footer #sponsors img {
margin: 0;
height: 40px;
margin-right: 20px;
padding: 5px;
}
@media (max-width: 500px) {
main {
padding-bottom: 80px;
}
footer #sponsors img {
margin: 0;
height: 30px;
}
footer {
flex-direction: column;
}
footer p {
margin: 5px;
}
}
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

home/static/imgs/2021/logointerludes.png

184 KiB

home/static/imgs/2022/bul.png

147 KiB

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="561.182px" height="235.275px" viewBox="0 0 561.182 235.275" enable-background="new 0 0 561.182 235.275"
xml:space="preserve">
<g>
<path d="M40.278,35.72c0,0.807-0.149,1.384-0.446,1.733c-0.297,0.347-0.591,0.521-1.158,0.521c-0.551,0-0.86-0.184-1.162-0.549
c-0.303-0.366-0.454-0.934-0.454-1.706V27.1H34.71v8.62c0,1.346,0.376,2.387,1.128,3.123c0.753,0.737,1.619,1.105,2.836,1.105
c1.218,0,2.062-0.365,2.815-1.101c0.752-0.733,1.128-1.775,1.128-3.128V27.1h-2.339V35.72z"/>
<polygon points="50.723,35.172 50.672,35.191 46.996,27.1 44.64,27.1 44.64,39.766 46.996,39.766 46.996,31.684 47.045,31.667
50.723,39.766 53.062,39.766 53.062,27.1 50.723,27.1 "/>
<rect x="55.424" y="27.1" width="2.339" height="12.666"/>
<polygon points="65.112,39.766 68.476,27.1 66.005,27.1 63.997,36.555 63.906,37.016 63.857,37.016 63.765,36.531 61.773,27.1
59.303,27.1 62.665,39.766 "/>
<polygon points="76.998,37.791 72.245,37.791 72.245,34.207 76.287,34.207 76.287,32.233 72.245,32.233 72.245,29.075
76.981,29.075 76.981,27.1 69.891,27.1 69.891,39.767 76.998,39.767 "/>
<path d="M83.912,35.25c0.245,0.348,0.367,0.839,0.367,1.471v0.861c0,0.436,0.025,0.86,0.075,1.277
c0.049,0.418,0.17,0.72,0.363,0.906h2.43v-0.184c-0.194-0.18-0.328-0.468-0.406-0.864c-0.076-0.399-0.114-0.771-0.114-1.119v-0.895
c0-0.766-0.137-1.404-0.41-1.915c-0.272-0.511-0.718-0.876-1.334-1.097c0.54-0.282,0.949-0.665,1.23-1.143
c0.282-0.478,0.422-1.049,0.422-1.71c0-1.171-0.337-2.085-1.012-2.744c-0.676-0.658-1.604-0.988-2.79-0.988h-3.924v12.658h2.346
v-5.038h1.702C83.315,34.729,83.665,34.904,83.912,35.25 M82.725,32.755h-1.57v-3.672h1.578c0.474,0,0.836,0.173,1.083,0.514
c0.247,0.342,0.371,0.803,0.371,1.383c0,0.569-0.124,1.006-0.371,1.314C83.568,32.602,83.204,32.755,82.725,32.755"/>
<path d="M92.926,32.371c-0.722-0.329-1.229-0.629-1.521-0.899c-0.291-0.27-0.438-0.614-0.438-1.031c0-0.451,0.13-0.828,0.393-1.127
c0.261-0.299,0.627-0.447,1.095-0.447c0.502,0,0.89,0.179,1.165,0.54c0.276,0.359,0.413,0.828,0.413,1.409h2.282l0.016-0.053
c0.022-1.107-0.326-2.025-1.041-2.754c-0.717-0.728-1.649-1.091-2.794-1.091c-1.13,0-2.058,0.325-2.785,0.978
c-0.728,0.652-1.091,1.498-1.091,2.536c0,1.05,0.312,1.866,0.938,2.448c0.625,0.583,1.563,1.104,2.814,1.562
c0.611,0.278,1.038,0.562,1.28,0.849c0.244,0.287,0.365,0.679,0.365,1.172c0,0.475-0.12,0.851-0.361,1.129
c-0.239,0.279-0.606,0.417-1.102,0.417c-0.634,0-1.108-0.17-1.421-0.511c-0.314-0.344-0.471-0.906-0.471-1.689h-2.29l-0.017,0.053
c-0.026,1.316,0.379,2.327,1.218,3.031c0.842,0.704,1.835,1.058,2.981,1.058c1.15,0,2.073-0.312,2.768-0.931
c0.695-0.621,1.042-1.48,1.042-2.577c0-1.06-0.289-1.902-0.865-2.525S94.066,32.778,92.926,32.371"/>
<rect x="98.383" y="27.1" width="2.338" height="12.666"/>
<polygon points="102.186,29.075 104.879,29.075 104.879,39.767 107.226,39.767 107.226,29.075 109.938,29.075 109.938,27.1
102.186,27.1 "/>
<polygon points="117.583,23.933 117.559,23.882 115.086,23.882 113.632,26.19 115.525,26.196 "/>
<polygon points="113.847,37.791 113.847,34.207 117.889,34.207 117.889,32.233 113.847,32.233 113.847,29.075 118.583,29.075
118.583,27.1 111.492,27.1 111.492,39.767 118.599,39.767 118.599,37.791 "/>
<path d="M38.337,42.279h-3.635v12.665h3.635c1.235,0,2.254-0.489,3.059-1.47c0.804-0.979,1.206-2.244,1.206-3.792V47.55
c0-1.549-0.402-2.814-1.206-3.797S39.573,42.279,38.337,42.279 M40.254,49.679c0,0.993-0.176,1.789-0.532,2.39
c-0.355,0.601-0.862,0.901-1.516,0.901h-1.149v-8.717h1.149c0.654,0,1.161,0.3,1.516,0.896c0.356,0.599,0.532,1.392,0.532,2.378
V49.679z"/>
<polygon points="46.993,49.386 51.034,49.386 51.034,47.411 46.993,47.411 46.993,44.252 51.727,44.252 51.727,42.279
44.638,42.279 44.638,54.944 51.745,54.944 51.745,52.97 46.993,52.97 "/>
<polygon points="59.74,42.279 57.385,42.279 57.385,54.944 63.842,54.944 63.842,52.97 59.74,52.97 "/>
<polygon points="67.282,47.707 67.232,47.707 65.247,42.279 62.679,42.279 66.058,50.282 66.058,54.944 68.407,54.944
68.407,50.438 71.843,42.279 69.29,42.279 "/>
<path d="M76.657,42.097c-1.267,0-2.281,0.421-3.04,1.264c-0.761,0.845-1.14,2.007-1.14,3.485v3.548c0,1.485,0.379,2.646,1.14,3.481
c0.759,0.834,1.776,1.253,3.049,1.253s2.292-0.419,3.058-1.253c0.765-0.836,1.148-1.996,1.148-3.481v-3.548
c0-1.479-0.384-2.641-1.155-3.485C78.945,42.518,77.926,42.097,76.657,42.097 M78.534,50.396c0,0.919-0.16,1.608-0.48,2.067
c-0.319,0.46-0.782,0.688-1.388,0.688c-0.611,0-1.075-0.228-1.389-0.688c-0.312-0.459-0.47-1.148-0.47-2.067v-3.577
c0-0.913,0.153-1.599,0.462-2.058c0.309-0.461,0.772-0.69,1.388-0.69c0.613,0,1.078,0.231,1.397,0.693
c0.32,0.464,0.48,1.147,0.48,2.055V50.396z"/>
<polygon points="88.996,50.351 88.946,50.369 85.269,42.279 82.913,42.279 82.913,54.943 85.269,54.943 85.269,46.863
85.319,46.846 88.996,54.943 91.334,54.943 91.334,42.279 88.996,42.279 "/>
<path d="M112.127,142.275c-0.009,11.787-0.019,27.052-0.019,27.19c0,19.518-15.883,35.401-35.424,35.405
c-0.069,0-0.136-0.004-0.203-0.006h-0.009h-0.003c-0.068,0.002-0.134,0.006-0.202,0.006c-19.542-0.004-35.426-15.889-35.426-35.405
c0-0.14-0.009-15.414-0.017-27.19H34.3c0.01,11.768,0.019,27.03,0.019,27.193c0,23.116,18.811,41.926,41.933,41.926
c0.076,0,0.149-0.005,0.223-0.005c0.075,0,0.147,0.005,0.223,0.005c23.123,0,41.933-18.81,41.933-41.926
c0-0.16,0.009-15.414,0.018-27.193H112.127z"/>
<path d="M99.046,142.275c-0.01,11.852-0.018,27.126-0.018,27.17c0,12.315-10.018,22.335-22.331,22.335
c-0.073,0-0.143-0.006-0.216-0.009v-0.003c0,0-0.005,0.003-0.009,0.003c0,0-0.001-0.003-0.003-0.003v0.003
c-0.072,0.003-0.143,0.009-0.215,0.009c-12.314,0-22.331-10.02-22.331-22.335c0-0.044-0.009-15.323-0.019-27.17h-6.567
l0.013,20.296c0.004,4.075,0.006,6.825,0.006,6.878c0,15.935,12.969,28.898,28.912,28.898c0.07,0,0.137-0.003,0.207-0.003
c0.068,0,0.137,0.003,0.207,0.003c15.941,0,28.913-12.965,28.913-28.898c0-0.053,0.001-2.803,0.005-6.878l0.013-20.296H99.046
L99.046,142.275z"/>
<path d="M85.884,142.275c-0.008,12.051-0.019,26.914-0.019,26.957c0,5.168-4.183,9.378-9.342,9.414h-0.048h-0.049
c-5.157-0.036-9.342-4.246-9.342-9.414c0-0.043-0.009-14.9-0.019-26.957h-6.582c0.01,12.057,0.019,26.914,0.019,26.957
c0,8.813,7.161,15.98,15.967,15.995v0.003h0.006h0.005l0.001-0.003c8.807-0.015,15.968-7.183,15.968-15.995
c0-0.043,0.009-14.906,0.019-26.957H85.884z"/>
<rect x="34.468" y="129.298" width="84.191" height="6.49"/>
<rect x="34.468" y="116.319" width="84.191" height="6.49"/>
<path d="M120.642,68.449c0,0-0.134-0.244-0.406-0.542c-0.269-0.304-0.949-0.714-1.276-1.034c-0.323-0.334-0.712-0.853-0.903-0.986
c-0.184-0.138-0.574-0.753-0.753-0.894c-0.199-0.137-0.417-1.142-0.443-1.253c-0.021-0.107-0.1-0.413-0.4-0.572
c-0.17-0.072-0.635-0.381-0.939-0.623c-0.293-0.246-1.048-0.981-1.048-0.981s-0.198-1.223-0.491-1.87
c-0.295-0.657-0.414-0.9-0.554-1.146c-0.13-0.247-0.216-0.297-0.289-0.68c0,0-0.411-0.031-0.793,0.214c0,0-0.786-0.598-1.57-1.058
c-0.791-0.467-1.721-0.951-2.105-1.027c-0.371-0.086-0.88-0.381-1.077-0.572c0,0-0.557-0.027-1.009,0.05
c-0.325,0.042-1.401,0.152-1.972,0.238c-0.568,0.075-1.971,0.311-2.291,0.333c-0.677,0.047-0.817,0.099-1.835,0.291
c0,0-0.806,0.142-1.08,0.247c-0.276,0.109-1.145,0.447-1.748,0.522c-0.661,0.08-1.352,0.083-2.148,0.211
c-0.357,0.064-1.706,0.603-2.342,0.751c-0.286,0.07-0.671,0.115-0.671,0.115s0.13,0.451,0.482,0.532c0,0-0.188,0.404-0.625,0.618
c-0.435,0.225-0.892,0.41-1.488,0.632c-0.606,0.209-1.553,0.589-1.798,0.783c0,0,0.19,0.378,0.562,0.579
c0,0,0.09,0.052-0.423,0.313c-0.522,0.279-1.701,0.769-2.234,1.012c-0.552,0.248-1.296,0.689-1.662,0.869
c-0.52,0.271-0.571,0.284-1.067,0.594c-0.486,0.296-0.239,0.279-0.239,0.279s0.244,0.054,0.589,0.079
c0.357,0.027,0.547,0.165,0.547,0.165s0.038,0.189-0.211,0.411c-0.244,0.215-0.599,0.7-1.439,1.276
c-0.417,0.282-0.78,0.607-1.238,0.929c-0.462,0.319-0.929,0.869-1.093,0.98l0.492,0.434l-0.074,0.34
c-0.213,0.133-0.48,0.281-0.818,0.414c-0.382,0.168-3.65,0.608-7.427,0.562c-6.309-0.087-8.048-0.9-11.26-1.046
c-3.219-0.15-5.606,0.563-8.544,1.458c-0.578,0.176-1.031,0.353-1.374,0.527c-1.235,0.256-2.481,0.6-3.623,1.04
c-4.087,1.567-11.221,7.473-13.218,8.313c-1.969,0.832-3.079,0.872-4.478,0.697c-1.397-0.172-1.948-0.524-1.948-0.524
s-0.713-1.516-1.533-2.134c-1.069-0.786-3.702-1.718-3.702-1.718s-0.459,1.729,0.475,3.396c1.123,1.987,3.317,1.862,4.11,2.187
c1.399,0.563,4.736,1.239,7.551,0.109c2.526-1.016,5.088-2.697,6.912-3.722c1.817-1.018,4.73-1.903,7.183-2.178
c-0.114,0.426-0.238,0.843-0.369,1.213c-0.687,2.087-3.223,6.515-3.74,7.251c-0.597,0.866-2.968,3.738-3.387,4.088
c-0.408,0.357-1.413,0.754-2.673,0.986c-1.385,0.258-5.36,0.657-5.36,0.657s-0.701,3.553-1.582,5.722
c-0.359,0.882-1.209,2.636-1.209,2.636s1.492,1.391,1.966,3.132c0.233,0.446,0.624,1.179,0.93,1.407v6.168h0.006v0.178h84.191
v-6.488h-0.002c0.157-0.494,0.384-1.61-0.587-2.074c-1.257-0.6-0.246-1.075-3.162-2.181c-0.835-0.318-2.228-0.765-2.7-1.154
c-0.469-0.381-2.995-3.911-4.248-5.397c-1.263-1.497-3.837-4.915-3.837-4.915l-0.567-1.35l0.102-1.02
c0.159-0.167,0.331-0.331,0.481-0.522c0.457-0.49,1.152-1.218,1.349-1.623c0.479-0.97,0.309-0.023,0.605-0.55
c0.222-0.4,1.087-0.877,1.447-1.106c0.354-0.215,0.882-0.869,0.882-0.869s0.013,0.39,0.16,0.767c0,0,0.252-0.208,0.653-0.699
c0.401-0.487,0.264-0.534,0.755-1.029c0.492-0.485,0.841-1.456,1.151-2.089c0.318-0.621,0.503-0.803,0.585-0.938
c0,0,0.208,0.498,0.345,0.849c0,0,0.322-0.311,0.457-0.621c0.132-0.311,0.224-0.714,0.446-1.067c0.222-0.36,0.39-0.823,0.605-0.915
c0.227-0.092,0.65-0.354,1.104-0.377c0.33-0.016,0.784,0.173,1.195,0.258c0.391,0.095,0.576-0.085,0.846,0.267
c0.264,0.357,0.625,0.543,0.625,0.543s0.006-0.056,0.331-0.219c0.33-0.167,0.349-0.162,0.655-0.38
c0.302-0.221,0.254-0.33,0.482-0.549c0.251-0.214,0.392-0.459,0.492-0.872c0.113-0.399-0.073-0.642,0.124-0.832
c0.193-0.187,0.583-0.196,0.782-0.668c0.188-0.457,0.131-0.54,0.214-0.618c0.079-0.083,0.13-0.41,0.13-0.41s0.551-0.46,0.629-0.84
c0.078-0.379,0.161-0.519,0.332-0.684C121.52,69.451,121.376,68.902,120.642,68.449 M76.793,89.907
c0.589,0.271,2.061,0.904,3.655,1.511c0.158,0.416,0.275,1.017,0.225,1.854c-0.145,2.091-2.219,4.689-2.219,4.689
s1.05,2.591,1.35,2.969c0.294,0.37,1.254,1.144,1.578,1.767c0.092,0.186,0.208,0.437,0.3,0.637h-5.697
c0.094-0.778-0.417-1.104-0.554-1.237c-0.311-0.308-0.581-0.915-1.342-1.184c-2.661-0.948-2.619-0.564-3.387-1.18
c-0.522-0.416-1.844-1.293-2.143-1.832c-0.297-0.538-0.836-2.983-0.845-5.072c0.004-1.263-0.087-2.462-0.175-3.212
C69.991,89.192,73.858,88.584,76.793,89.907 M37.519,95.526c0.854-0.354,2.739,0.37,4.623,0.026
c2.878-0.523,6.984-1.96,8.368-2.582c1.257-0.548,3.501-2.704,4.196-3.275c0.683-0.578,1.401-0.743,1.401-0.743
s0.834,0.896,2.511,2.38c1.682,1.489,2.25,2.181,2.836,2.664c0.573,0.484,1.276,1.471,1.392,1.864
c0.462,1.482,0.298,3.948,0.298,3.948s2.62,0.929,3.46,1.678c0.592,0.525,1.088,1.386,1.33,1.848H41.117
c0.067-0.445,0.098-1.435-0.87-1.775c-0.704-0.298-0.561-0.556-2.051-1.072c-0.599-0.214-0.934-0.462-1.242-1.241
c-0.298-0.784-0.302-2.082-0.302-2.082S36.786,95.824,37.519,95.526 M104.179,98.05c0,0,2.712,1.033,3.361,1.943
c0.422,0.596,2.881,1.294,2.881,1.294s0.751,0.053,1.728,0.947c0.746,0.684,0.986,0.966,1.453,1.099H88.694
c0.077-0.406,0.1-1.081-0.503-1.425c-0.397-0.223-0.741-0.657-1.187-0.873c-1.718-0.82-1.906-0.521-2.625-1.095
c-0.404-0.297-0.673-1.614-0.673-1.614s3.143-3.1,3.744-3.883c0.755-0.977,1.053-1.367,1.391-1.85
c2.364-0.222,4.844-0.647,5.697-1.135c0.242-0.145,0.462-0.274,0.671-0.396c0.269,0.152,0.507,0.291,0.714,0.416
c1.26,0.747,4.436,2.085,5.554,2.976C102.599,95.356,104.179,98.05,104.179,98.05"/>
<rect x="174.743" y="25.038" width="1.096" height="186.355"/>
<path d="M230.258,79.797h90.524v12.284h-90.524V79.797z M230.258,115.657v-12.038h90.655v12.038H230.258z M320.782,127.689v12.28
h-90.524v-12.28H320.782z"/>
<path d="M333.062,79.796h67.562c15.308,0,22.962,4.098,22.962,12.283H333.06V79.796H333.062z M333.062,103.62h23.502v12.032
h-23.502V103.62z M356.563,127.691v12.274l-23.502,0.129v-12.403H356.563L356.563,127.691z M400.623,103.62h23.101v12.032h-23.101
V103.62z M400.623,127.691h22.964v12.28h-22.964V127.691z"/>
<path d="M526.718,127.691c0,8.182-7.742,12.274-23.232,12.274h-67.29v-12.274H526.718z M459.424,79.794h67.294v12.281h-90.523
C436.194,83.894,443.94,79.794,459.424,79.794 M526.718,115.653h-90.522v-12.03h90.522V115.653z"/>
<polygon points="381.374,153.397 390.235,153.397 390.235,154.829 383.162,154.829 383.162,160.764 389.875,160.764
389.875,162.199 383.162,162.199 383.162,168.608 390.545,168.608 390.545,170.043 381.374,170.043 "/>
<polygon points="394.101,153.397 396.354,153.397 405.558,168.036 405.604,168.036 405.604,153.397 407.397,153.397
407.397,170.043 404.985,170.043 395.938,155.683 395.888,155.683 395.888,170.043 394.101,170.043 "/>
<path d="M411.127,167.914c1.06,0.618,2.308,0.979,3.99,0.979c2.228,0,4.069-1.047,4.069-3.409c0-3.266-8.293-3.672-8.293-8.033
c0-2.672,2.539-4.34,5.728-4.34c0.887,0,2.308,0.119,3.555,0.555l-0.284,1.499c-0.81-0.405-2.078-0.625-3.3-0.625
c-1.866,0-3.911,0.722-3.911,2.863c0,3.343,8.294,3.36,8.294,8.225c0,3.365-3.135,4.698-5.938,4.698
c-1.762,0-3.131-0.336-4.092-0.716L411.127,167.914z"/>
<path d="M431.631,153.397h4.481c6.477,0,9.043,3.437,9.043,8.301c0,6.03-4.377,8.343-10.366,8.343h-3.158V153.397z
M433.413,168.608h1.479c5.134,0,8.477-1.906,8.477-7.01c0-5.074-3.293-6.771-7.332-6.771h-2.624V168.608L433.413,168.608z"/>
<polygon points="449.069,153.397 457.935,153.397 457.935,154.829 450.857,154.829 450.857,160.764 457.57,160.764 457.57,162.199
450.857,162.199 450.857,168.608 458.246,168.608 458.246,170.043 449.069,170.043 "/>
<polygon points="469.077,153.397 470.868,153.397 470.868,168.608 478.129,168.608 478.129,170.043 469.077,170.043 "/>
<polygon points="482.66,162.986 475.947,153.4 478.023,153.4 483.546,161.533 489.222,153.4 491.164,153.4 484.451,162.986
484.451,170.037 482.66,170.037 "/>
<path d="M500.834,153.115c5.441,0,8.008,4.097,7.955,8.6c-0.053,5.104-2.75,8.608-7.955,8.608c-5.209,0-7.908-3.504-7.965-8.608
C492.823,157.211,495.387,153.115,500.834,153.115 M494.663,161.714c0,3.531,1.918,7.18,6.171,7.18c4.248,0,6.171-3.647,6.171-7.18
c0-3.523-1.923-7.167-6.171-7.167C496.581,154.547,494.663,158.191,494.663,161.714"/>
<polygon points="512.184,153.397 514.435,153.397 523.639,168.036 523.69,168.036 523.69,153.397 525.481,153.397 525.481,170.043
523.072,170.043 514.021,155.683 513.968,155.683 513.968,170.043 512.184,170.043 "/>
</g>
</svg>