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
Commits on Source (55)
Showing
with 412 additions and 143 deletions
# Change Log # Change Log
## Version 2.0.0-beta - Coming soon
- Added a form that allows admins to send emails to all users
- Added a form for users to submit activities
- Added a changeable caption for the planning
- Added fixes/improvement from 48h des jeux:
- bug fixed in activity submission form
- new validator that checks the number of slots for each activity in the planning
- fixed room display on activity page
- fixed planning info displayed on activity even when planning hidden
- added boolean field to show host email on activity
- added boolean field to separate showing slot on planning and next to activity
## Version 1.2.8 - 2021-05-06 ## Version 1.2.8 - 2021-05-06
- Added links to FAQ - Added links to FAQ
......
...@@ -6,7 +6,8 @@ SECRET := interludes/secret.py ...@@ -6,7 +6,8 @@ SECRET := interludes/secret.py
.PHONY: help .PHONY: help
help: ## Show this help help: ## Show this help
@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' @echo "make: list of useful targets :"
@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
.PHONY: install .PHONY: install
install: ## Install requirements install: ## Install requirements
...@@ -37,8 +38,8 @@ start: install $(SECRET) migrate serve ## Install requirements, apply migrations ...@@ -37,8 +38,8 @@ start: install $(SECRET) migrate serve ## Install requirements, apply migrations
.PHONY: clean .PHONY: clean
clean: ## Remove migrations and delete database clean: ## Remove migrations and delete database
find . -path "*/migrations/*.py" -not -name "__init__.py" -delete find . -path "*/migrations/*.py" -not -name "__init__.py" -not -path "*/venv/*" -delete
find . -path "*/migrations/*.pyc" -delete find . -path "*/migrations/*.pyc" -not -path "*/venv/*" -delete
rm $(DB) rm $(DB)
.PHONY: test .PHONY: test
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
Ce répo contient le sites des interludes. Ce site est en ligne à [https://interludes.ens.fr](https://interludes.ens.fr). Ce répo contient le sites des interludes. Ce site est en ligne à [https://interludes.ens.fr](https://interludes.ens.fr).
Ce répo est une copie du répo initial sur le [git interne de l'ENS Ulm](https://git.eleves.ens.fr/dlesbre/site-interludes).
Ce répo est diffusé sous une [license MIT](https://choosealicense.com/licenses/mit/). Ce répo est diffusé sous une [license MIT](https://choosealicense.com/licenses/mit/).
**Contenu:** **Contenu:**
...@@ -17,7 +19,7 @@ Ce répo est diffusé sous une [license MIT](https://choosealicense.com/licenses ...@@ -17,7 +19,7 @@ Ce répo est diffusé sous une [license MIT](https://choosealicense.com/licenses
Pour installer toutes les dépendances et lancer le serveur : Pour installer toutes les dépendances et lancer le serveur :
git clone https://git.eleves.ens.fr/dlesbre/site-interludes.git && git clone https://github.com/dlesbre/site-interludes.git &&
cd site-interlude && cd site-interlude &&
python3 -m venv venv && python3 -m venv venv &&
source venv/bin/activate && source venv/bin/activate &&
...@@ -88,12 +90,13 @@ Le site se gère depuis deux pages d'administration: ...@@ -88,12 +90,13 @@ Le site se gère depuis deux pages d'administration:
- Utilisateurs - contient tous les utilisateurs et leur permissions. Pour donner les droits d'administrateur à quelqu'un il faut lui donner le statut superutilisateur (accès à l'admin du site) ET le statut équipe (accès à l'admin django) - Utilisateurs - contient tous les utilisateurs et leur permissions. Pour donner les droits d'administrateur à quelqu'un il faut lui donner le statut superutilisateur (accès à l'admin du site) ET le statut équipe (accès à l'admin django)
- Paramêtres - les réglages du site, ils permettent: - Paramêtres - les réglages du site, ils permettent:
- ouvrir/fermer la création de compte, les inscriptions - ouvrir/fermer la création de compte, les inscriptions
- ouvrir fermer le formulaire de proposition d'activités
- afficher/cacher le planning - afficher/cacher le planning
- renseigner l'email de contact, les dates de l'événement, les dates d'inscription - renseigner l'email de contact, les dates de l'événement, les dates d'inscription
- ajouter un message global au dessus de toutes les pages - ajouter un message global au dessus de toutes les pages
- bloquer/autoriser l'envoi d'email globaux - bloquer/autoriser l'envoi d'email globaux
- Activités - liste des activités prévues. C'est ici que vous pouvez rajouter/modifier les activités qui s'affichent sur la page activité. - Activités - liste des activités prévues. C'est ici que vous pouvez rajouter/modifier les activités qui s'affichent sur la page activité.
Pour le moment il n'y a pas de formulaire qui permette aux orga de proposer une activité sur le site (on était passé par un appel à projet externe et on avait rempli les activités nous-même) Un formulaire permet aux utilisateurs de proposer des activités directement. Ils vous faudra les relire et les valider ensuite manuellement pour qu'elles soient affichées sur le site.
- Crénaux - place une activité sur le planning. Une activité peut avoir plusieurs crénaux si elle a lieu plusieurs fois. Noter que les inscriptions se font à des crénaux et non a des activités. - Crénaux - place une activité sur le planning. Une activité peut avoir plusieurs crénaux si elle a lieu plusieurs fois. Noter que les inscriptions se font à des crénaux et non a des activités.
- Participant - liste des gens inscrits et des informations sur leur inscription (ENS, repas choisi...) - Participant - liste des gens inscrits et des informations sur leur inscription (ENS, repas choisi...)
- Choix d'activité - Liste de (participant, priorité, activité) indiquant les voeux des participant. Une fois que vous avez fait l'attribution, cocher les case "Obtenues" pour indiquer qui a eu quelle activité. - Choix d'activité - Liste de (participant, priorité, activité) indiquant les voeux des participant. Une fois que vous avez fait l'attribution, cocher les case "Obtenues" pour indiquer qui a eu quelle activité.
...@@ -105,10 +108,11 @@ Le site se gère depuis deux pages d'administration: ...@@ -105,10 +108,11 @@ Le site se gère depuis deux pages d'administration:
- permet d'envoyer deux séries d'emails : - permet d'envoyer deux séries d'emails :
- une aux inscrits pour leur communiquer les activités qu'ils ont obtenus - une aux inscrits pour leur communiquer les activités qu'ils ont obtenus
- une aux orgas qui ont besoin de connaître la liste des participants à l'avance pour préparer leurs activités. - une aux orgas qui ont besoin de connaître la liste des participants à l'avance pour préparer leurs activités.
- permet l'écriture d'un mail à tous.
## En production ## En production
Le serveur a besoin d'être configuré pour HTTPS et d'être configuré pour livrer directement les fichiers situés des `/static/`. Le serveur a besoin d'être configuré pour HTTPS et d'être configuré pour livrer directement les fichiers situés dans `/static/` et `/media/`.
1. Installer les dépendances `make install` 1. Installer les dépendances `make install`
...@@ -126,7 +130,6 @@ Le serveur a besoin d'être configuré pour HTTPS et d'être configuré pour liv ...@@ -126,7 +130,6 @@ Le serveur a besoin d'être configuré pour HTTPS et d'être configuré pour liv
A.K.A. la liste des trucs utiles que j'ai pas eu le temps d'ajouter A.K.A. la liste des trucs utiles que j'ai pas eu le temps d'ajouter
- Un formulaire pour proposer une activité directement sur le site
- Intégrer l'[algorithme de répartition](https://github.com/Imakoala/InterludesMatchings) dans le site au lieu de le faire tourner en externe à partir des export CSV et de remplir les résultats à la main - Intégrer l'[algorithme de répartition](https://github.com/Imakoala/InterludesMatchings) dans le site au lieu de le faire tourner en externe à partir des export CSV et de remplir les résultats à la main
- Envoyer une concaténation de tous les emails aux admin (pour vérification, et pas juste en copie pour éviter le spam...) - Envoyer une concaténation de tous les emails aux admin (pour vérification, et pas juste en copie pour éviter le spam...)
- Générer la version PDF du planning automatiquement au lieu de la faire à base de captures d'écran - Générer la version PDF du planning automatiquement au lieu de la faire à base de captures d'écran
...@@ -135,5 +138,9 @@ A.K.A. la liste des trucs utiles que j'ai pas eu le temps d'ajouter ...@@ -135,5 +138,9 @@ A.K.A. la liste des trucs utiles que j'ai pas eu le temps d'ajouter
## Liens divers ## Liens divers
- [Le site des interludes 2021](https://interludes.ens.fr) - [Le site des interludes 2021](https://interludes.ens.fr)
- [Le répo initial](https://git.eleves.ens.fr/dlesbre/site-interludes) sur le gitlab de l'ENS Ulm
- [Le github de l'algorithme de répartition](https://github.com/Imakoala/InterludesMatchings) - [Le github de l'algorithme de répartition](https://github.com/Imakoala/InterludesMatchings)
- [Le wiki de Paris-Saclay](https://wiki.crans.org/VieBdl/InterLudes) qui recensent les visuels, sites webs et photos des interludes passées. - [Le wiki de Paris-Saclay](https://wiki.crans.org/VieBdl/InterLudes) qui recense les visuels, sites webs et photos des interludes passées.
- [Le gitlab du site des 48h des jeux](https://git.eleves.ens.fr/dlesbre/48h-des-jeux) un événement très similaire intra-ENS Ulm, c'est fork de ce répo.
- [Le site des 48h des jeux](https://48hdesjeux.cof.ens.fr/)
- [Le site du club jeu d'Ulm](https://jeux.cof.ens.fr/)
# Generated by Django 3.2.7 on 2021-10-05 18:45
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='EmailUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='adresse email')),
('first_name', models.CharField(max_length=100, verbose_name='prénom')),
('last_name', models.CharField(max_length=100, verbose_name='nom')),
('email_confirmed', models.BooleanField(default=False, verbose_name='email vérifié')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'utilisateur',
},
),
]
{% extends 'base.html' %} {% extends 'base.html' %}
{% block "content" %} {% block "content" %}
<h2>Mot de passe oublié ?</h2> <h2>Mot de passe oublié ?</h2>
<p>Saissisez votre adresse email ci-dessous pour recevoir un lien de réinitialisation du mot de passe.</p> <p>Saissisez votre adresse email ci-dessous pour recevoir un lien de réinitialisation du mot de passe.</p>
<form method="POST"> <form method="POST">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<div class="flex"> <div class="flex">
<input type="submit" value="Valider"> <input type="submit" value="Valider">
<a class="button" href="{% url 'accounts:login' %}">Annuler</a> <a class="button" href="{% url 'accounts:login' %}">Annuler</a>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}
...@@ -6,12 +6,12 @@ ...@@ -6,12 +6,12 @@
<h2>Saissisez un nouveau mot de passe</h2> <h2>Saissisez un nouveau mot de passe</h2>
<form method="POST"> <form method="POST">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<div class="flex"> <div class="flex">
<input type="submit" value="Changer mon mot de passe"> <input type="submit" value="Changer mon mot de passe">
<a class="button" href="{% url 'accounts:login' %}">Annuler</a> <a class="button" href="{% url 'accounts:login' %}">Annuler</a>
</div> </div>
</form> </form>
{% else %} {% else %}
...@@ -19,8 +19,8 @@ ...@@ -19,8 +19,8 @@
<h2>Lien invalide</h2> <h2>Lien invalide</h2>
<p>Le lien de réinitialisation est invalide, peut-être a-t-il déjà été utilisé. <p>Le lien de réinitialisation est invalide, peut-être a-t-il déjà été utilisé.
Veuillez <a href="{% url 'accounts:password_reset' %}">demander un nouveau lien</a>. Veuillez <a href="{% url 'accounts:password_reset' %}">demander un nouveau lien</a>.
</p> </p>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
\ No newline at end of file
...@@ -12,16 +12,6 @@ ...@@ -12,16 +12,6 @@
<a class="button" href="{% url 'profile' %}">Annuler</a> <a class="button" href="{% url 'profile' %}">Annuler</a>
</div> </div>
</form> </form>
<h2> Mot de Passe </h2>
<h2>Changer mon mot de passe</h2> <a href ="{% url 'account_reset_password'% }" rel="text/html"> Réinitialisez votre mot de passe. </a>
<form method="post" action="{% url 'accounts:change_password' %}">
{% csrf_token %}
{{ password_form.as_html }}
<br>
<div class="flex">
<input type="submit" value="Valider">
<a class="button" href="{% url 'profile' %}">Annuler</a>
</div>
</form>
{% endblock %} {% endblock %}
...@@ -5,16 +5,5 @@ from accounts import views ...@@ -5,16 +5,5 @@ from accounts import views
app_name = "accounts" app_name = "accounts"
urlpatterns = [ urlpatterns = [
path("login/", views.LoginView.as_view(), name="login"),
path("logout/", views.LogoutView.as_view(), name="logout"),
path("create/", views.CreateAccountView.as_view(), name="create"),
path("update/", views.UpdateAccountView.as_view(), name="update"), path("update/", views.UpdateAccountView.as_view(), name="update"),
path("change_password/", views.UpdatePasswordView.as_view(), name="change_password"),
path('activate/<uidb64>/<token>/', views.ActivateAccountView.as_view(), name='activate'),
path("password_reset/", views.ResetPasswordView.as_view(), name="password_reset"),
path(
"password_reset/<uidb64>/<token>/",
views.ResetPasswordConfirmView.as_view(),
name="password_reset_confirm"
),
] ]
...@@ -31,10 +31,12 @@ ...@@ -31,10 +31,12 @@
<li>Fermeture : {{ settings.inscriptions_end|default:"non fixée" }}</li> <li>Fermeture : {{ settings.inscriptions_end|default:"non fixée" }}</li>
</ul> </ul>
</li> </li>
<li>Les emails des orgas sont {% if settings.show_host_emails %}affichés sur la page activité{% else %}masqués{% endif %}.</li>
<li>Le planning {% if settings.display_planning %}est affiché{% else %}n'est pas affiché{% endif %}.</li> <li>Le planning {% if settings.display_planning %}est affiché{% else %}n'est pas affiché{% endif %}.</li>
<li>La répartition des activités {% if settings.activities_allocated %}est effectuée et affichée{% else %}n'est pas faite/affichée{% endif %}.</li> <li>La répartition des activités {% if settings.activities_allocated %}est effectuée et affichée{% else %}n'est pas faite/affichée{% endif %}.</li>
<li>{% if settings.global_message %}Un message global est affiché{% else %}Aucun message global{% endif %}.</li> <li>{% if settings.global_message %}Un message global est affiché{% else %}Aucun message global{% endif %}.</li>
<li>Le lien du serveur discord {% if settings.discord_link %}est affiché{% else %}n'est pas affiché{% endif %}.</li> <li>Le lien du serveur discord {% if settings.discord_link %}est affiché{% else %}n'est pas affiché{% endif %}.</li>
<li>L'envoi d'email en masse est {% if settings.allow_mass_email %}activé{% else %}désactivé{% endif %}</li>
</ul> </ul>
<h2>Métriques</h2> <h2>Métriques</h2>
...@@ -44,22 +46,14 @@ ...@@ -44,22 +46,14 @@
<div class="qty">Participants</div> <div class="qty">Participants</div>
<div class="nb_big">{{ metrics.participants }}</div> <div class="nb_big">{{ metrics.participants }}</div>
</div> </div>
<div class="stat">
<div class="qty">Ulm</div>
<div class="nb_small">{{ metrics.ulm }}</div>
</div>
<div class="stat">
<div class="qty">Lyon</div>
<div class="nb_small">{{ metrics.lyon }}</div>
</div>
<div class="stat">
<div class="qty">Rennes</div>
<div class="nb_small">{{ metrics.rennes }}</div>
</div>
<div class="stat"> <div class="stat">
<div class="qty">Paris-Saclay</div> <div class="qty">Paris-Saclay</div>
<div class="nb_small">{{ metrics.saclay }}</div> <div class="nb_small">{{ metrics.saclay }}</div>
</div> </div>
<div class="stat">
<div class="qty">Extérieur</div>
<div class="nb_small">{{ metrics.exterieur }}</div>
</div>
<div class="stat"> <div class="stat">
<div class="qty">Non inscrits</div> <div class="qty">Non inscrits</div>
<div class="nb_small">{{ metrics.non_registered }}</div> <div class="nb_small">{{ metrics.non_registered }}</div>
...@@ -69,7 +63,7 @@ ...@@ -69,7 +63,7 @@
<div class="nb_small">{{ metrics.sleeps }}</div> <div class="nb_small">{{ metrics.sleeps }}</div>
</div> </div>
</div> </div>
<!--
<div class="flex wrap lines"> <div class="flex wrap lines">
<div class="stat"> <div class="stat">
<div class="qty">Repas</div> <div class="qty">Repas</div>
...@@ -100,7 +94,7 @@ ...@@ -100,7 +94,7 @@
<div class="nb_small">{{ metrics.meal6 }}</div> <div class="nb_small">{{ metrics.meal6 }}</div>
</div> </div>
</div> </div>
-->
<div class="flex wrap lines"> <div class="flex wrap lines">
<div class="stat"> <div class="stat">
<div class="qty">Activités</div> <div class="qty">Activités</div>
...@@ -167,6 +161,10 @@ ...@@ -167,6 +161,10 @@
<h2>Prévisualisation du planning</h2> <h2>Prévisualisation du planning</h2>
<ul class="messagelist">
{{ planning_validation|safe }}
</ul>
<p>Vous pouver uploader une version PDF dans le réglages (depuis django-admin)</p> <p>Vous pouver uploader une version PDF dans le réglages (depuis django-admin)</p>
{% include "_planning.html" %} {% include "_planning.html" %}
......
...@@ -35,10 +35,8 @@ class AdminView(SuperuserRequiredMixin, TemplateView): ...@@ -35,10 +35,8 @@ class AdminView(SuperuserRequiredMixin, TemplateView):
) )
class metrics: class metrics:
participants = registered.count() 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() 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 non_registered = EmailUser.objects.filter(is_active=True).count() - participants
# mugs = registered.filter(mug=True).count() # mugs = registered.filter(mug=True).count()
sleeps = registered.filter(sleeps=True).count() sleeps = registered.filter(sleeps=True).count()
...@@ -169,6 +167,24 @@ class AdminView(SuperuserRequiredMixin, TemplateView): ...@@ -169,6 +167,24 @@ class AdminView(SuperuserRequiredMixin, TemplateView):
) )
return '<li class="success">Aucun inscrit plusieurs fois à une même activité</li>' 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): def validate_activity_allocation(self):
settings = SiteSettings.load() settings = SiteSettings.load()
validations = '<ul class="messagelist">' validations = '<ul class="messagelist">'
...@@ -207,7 +223,8 @@ class AdminView(SuperuserRequiredMixin, TemplateView): ...@@ -207,7 +223,8 @@ class AdminView(SuperuserRequiredMixin, TemplateView):
"validations": validations, "validations": validations,
"user_email_nb": user_email_nb, "user_email_nb": user_email_nb,
"orga_email_nb": orga_email_nb, "orga_email_nb": orga_email_nb,
"validation_errors": '<li class="error">' in validations "validation_errors": '<li class="error">' in validations,
"planning_validation": self.planning_validation(),
} }
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
...@@ -226,12 +243,25 @@ class AdminView(SuperuserRequiredMixin, TemplateView): ...@@ -226,12 +243,25 @@ class AdminView(SuperuserRequiredMixin, TemplateView):
class ExportActivities(SuperuserRequiredMixin, CSVWriteView): class ExportActivities(SuperuserRequiredMixin, CSVWriteView):
filename = "activites_interludes" filename = "activites_interludes"
model = models.ActivityModel 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): class ExportSlots(SuperuserRequiredMixin, CSVWriteView):
filename = "créneaux_interludes" filename = "créneaux_interludes"
headers = [ headers = [
"Titre", "Début", "Salle", "Titre", "Début", "Salle",
"Ouverte aux inscriptions", "Affichée sur le planning", "Ouverte aux inscriptions", "Affiché sur le planning", "Affiché sur l'activité",
"Couleur", "Durée", "Durée activité", "Couleur", "Durée", "Durée activité",
] ]
...@@ -241,8 +271,8 @@ class ExportSlots(SuperuserRequiredMixin, CSVWriteView): ...@@ -241,8 +271,8 @@ class ExportSlots(SuperuserRequiredMixin, CSVWriteView):
for slot in slots: for slot in slots:
rows.append([ rows.append([
str(slot), slot.start, slot.room, str(slot), slot.start, slot.room,
slot.subscribing_open, slot.on_planning, slot.subscribing_open, slot.on_planning, slot.on_activity,
Colors(slot.color).name, slot.duration, slot.activity.duration , Colors(slot.color).name, slot.duration, slot.activity.duration,
]) ])
return rows return rows
......
...@@ -18,26 +18,57 @@ class ActivityModelAdmin(ExportCsvMixin, admin.ModelAdmin): ...@@ -18,26 +18,57 @@ class ActivityModelAdmin(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.SlotModel) @admin.register(models.SlotModel)
class SlotModelAdmin(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",)
......
...@@ -66,7 +66,7 @@ class ActivitySubmissionForm(FormRenderMixin, forms.ModelForm): ...@@ -66,7 +66,7 @@ class ActivitySubmissionForm(FormRenderMixin, forms.ModelForm):
fields = ( fields = (
"title", "act_type", "game_type", "description", "title", "act_type", "game_type", "description",
"host_info", "host_name", "host_email", "host_info",
"must_subscribe", "communicate_participants", "must_subscribe", "communicate_participants",
"max_participants", "min_participants", "max_participants", "min_participants",
...@@ -82,15 +82,28 @@ class ActivitySubmissionForm(FormRenderMixin, forms.ModelForm): ...@@ -82,15 +82,28 @@ class ActivitySubmissionForm(FormRenderMixin, forms.ModelForm):
"available_sunday_afternoon", "available_sunday_afternoon",
"constraints", "constraints",
"status", "needs", #"status",
"needs",
"comments", "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): def save(self, *args, commit=True, **kwargs):
participant = super().save(*args, commit=False, **kwargs) """Enregistre l'activité dans la base de données"""
participant.is_registered = True activity = models.ActivityModel(
**self.cleaned_data,
)
if commit: if commit:
participant.save() activity.save()
return participant 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 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
...@@ -53,6 +54,10 @@ class ActivityModel(models.Model): ...@@ -53,6 +54,10 @@ class ActivityModel(models.Model):
display = models.BooleanField("afficher dans la liste", default=False, display = models.BooleanField("afficher dans la liste", default=False,
help_text="Si vrai, s'affiche sur la page activités" 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) title = models.CharField("Titre", max_length=200)
...@@ -137,7 +142,10 @@ class ActivityModel(models.Model): ...@@ -137,7 +142,10 @@ class ActivityModel(models.Model):
"Contraintes particulières", max_length=2000, blank=True, null=True "Contraintes particulières", max_length=2000, blank=True, null=True
) )
status = models.CharField("Présentiel/distanciel", choices=Status.choices, max_length=1) status = models.CharField(
"Présentiel/distanciel", choices=Status.choices, max_length=1,
default=Status.PRESENT, blank=True
)
needs = models.TextField( needs = models.TextField(
"Besoin particuliers", max_length=2000, blank=True, null=True "Besoin particuliers", max_length=2000, blank=True, null=True
) )
...@@ -166,15 +174,9 @@ class ActivityModel(models.Model): ...@@ -166,15 +174,9 @@ class ActivityModel(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:
...@@ -184,7 +186,7 @@ class ActivityModel(models.Model): ...@@ -184,7 +186,7 @@ class ActivityModel(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 SlotModel.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
...@@ -213,14 +215,16 @@ class SlotModel(models.Model): ...@@ -213,14 +215,16 @@ class SlotModel(models.Model):
) )
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" help_text="La légende des couleurs est modifiable dans les paramètres"
) )
...@@ -302,13 +306,14 @@ class ParticipantModel(models.Model): ...@@ -302,13 +306,14 @@ class ParticipantModel(models.Model):
class ENS(models.TextChoices): class ENS(models.TextChoices):
"""enum representant les ENS""" """enum representant les ENS"""
ENS_ULM = "U", _("ENS Ulm") #ENS_ULM = "U", _("ENS Ulm")
ENS_LYON = "L", _("ENS Lyon") #ENS_LYON = "L", _("ENS Lyon")
ENS_RENNES = "R", _("ENS Rennes") #ENS_RENNES = "R", _("ENS Rennes")
ENS_CACHAN = "C", _("ENS Paris Saclay") ENS_CACHAN = "C", _("ENS Paris-Saclay")
EXTERIEUR = "E", _("Extérieur")
user = models.OneToOneField(EmailUser, on_delete=models.CASCADE, related_name="Utilisateur") 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) is_registered = models.BooleanField("est inscrit", default=False)
...@@ -324,7 +329,7 @@ class ParticipantModel(models.Model): ...@@ -324,7 +329,7 @@ class ParticipantModel(models.Model):
# mug = models.BooleanField("commander une tasse", default=False) # mug = models.BooleanField("commander une tasse", default=False)
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 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)
@property @property
......
...@@ -98,7 +98,7 @@ header #head_main_infos { ...@@ -98,7 +98,7 @@ header #head_main_infos {
div.easter_egg { div.easter_egg {
display: inline-block; display: inline-block;
} }
#circle { #circle {
-webkit-clip-path: circle(50% at 50% 50%); -webkit-clip-path: circle(50% at 50% 50%);
clip-path: circle(50% at 50% 50%) clip-path: circle(50% at 50% 50%)
...@@ -135,6 +135,10 @@ nav a:hover { ...@@ -135,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);
...@@ -224,6 +228,10 @@ main p { ...@@ -224,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;
...@@ -443,40 +451,54 @@ ul.messagelist li.info:before { ...@@ -443,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;
} }
...@@ -489,39 +511,39 @@ ul.messagelist li.info:before { ...@@ -489,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;
} }
} }
......