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 827 additions and 362 deletions
{% extends "base.html" %}
{% load static %}
{% block nav_faq %}current{% endblock %}
{% block "content" %}
<h2>Dois-je être inscrit&middot;e pour participer&nbsp;?</h2>
<p>Les interludes ayant lieu à distance cette année, l'inscription est facultative.
Une inscription est seulement nécessaire pour certaines activités au nombre de places limité.
Sans inscription tu peux rejoindre notre
{% if settings.discord_link %}
<a href="{{ settings.discord_link }}">serveur discord</a>
{% else %}serveur discord{% endif %}
et y participer au jeux libres et à toutes les activités sans inscriptions.
</p>
<h2>Comment seront organisées les interludes à distance&nbsp;?</h2>
<p>Les interludes passeront principalement par un
{% if settings.discord_link %}<a href="{{ settings.discord_link }}">serveur discord</a>{% else %}serveur discord{% endif %}.
Nous y regrouperont
une liste de sites permettant de jouer en ligne et des canaux vocaux dédiés.
</p>
<p>Pour les <a href="{% url 'activites' %}">activités</a> proposées, leur description
vous indiquera s'il vous faudra autre chose qu'un ordinateur et une connexion internet pour participer.
</p>
<p>Le serveur contiendra des salons écrits et vocaux pour organiser des parties sur d'autres plateformes en lignes.</p>
<h2>Comment rejoindre le serveur discord&nbsp;?</h2>
{% if settings.discord_link %}
<p>Suivez <a href="{{ settings.discord_link }}">ce lien</a>.</p>
{% else %}
<p>Le lien vous sera communiqué par mail peu avant l'événement. Il apparaîtra également sur ce site.</p>
{% endif %}
<p>Vous pouvez utiliser Discord sur navigateur sans créer de compte (mais se connecter sans compte
pose parfois des problèmes). Sinon il existe une application mobile (Android et iOS) et un programme (Linux/Windows/Mac)
permettant d'y accèder via un compte.
</p>
<h2>Où jouer en ligne&nbsp;?</h2>
<p>Voici une liste (non-exhaustive) des plateformes de jeu en ligne&nbsp;:
<ul>
<li><a href="https://fr.boardgamearena.com/">Board Game Arena</a> (site gratuit nécessitant un compte)</li>
<li><a href="https://store.steampowered.com/app/286160/Tabletop_Simulator/">Tabletop Simulator</a> (jeu vendu 20€ sur steam)</li>
<li>Des sites spécialisés (gratuits)&nbsp;<ul>
<li>Pour Codenames : <a href="https://codenames.game/">codenames.game</a> ou <a href="https://www.horsepaste.com/">horsepaste.com</a>.</li>
<li>Pour Hanabi : <a href="https://hanab.live/">hanab.live</a> (nécessite un compte).</li>
<li>Pour Catan : <a href="https://colonist.io/">colonist.io</a>.</li>
<li>Pour Shadow-Hunter : <a href="http://shadowhunters.live/">shadowhunters.live</a>.</li>
<li>Pour Diplomacy : <a href="https://vdiplomacy.com/">vdiplomacy.com</a> (nécessite un compte).</li>
</ul></li>
<li>N'importe quel jeu vidéo multijoueur</li>
</ul>
Vous trouverez une liste plus fournie sur le discord interludes.
</p>
<h2>Comment proposer une activité&nbsp;?</h2>
{% if settings.activity_submission_open %}
<p>Vous pouver proposer une activité en remplissant <a href="{% url 'activity_submission' %}">ce formulaire</a>.</p>
<p>Il vous faudra être connecté et renseigner, le titre, une description, le nombre de places, la durée, les horaires possibles/idéaux, et tout autre besoin particulier...</p>
{% else %}
<p>L'appel a activités est fermé pour le moment.
Contactez nous en cas de besoin urgent ou d'idée trop géniale pour être délaissée.</p>
{% endif %}
<h2>Comment sont réparties les activités&nbsp;?</h2>
<p>
La répartition est faite par un algorithme puis vérifiée à la main.
Dans la mesure du possible, l'algorithme essaie d'attribue au moins une activité par personne. Par conséquent, si vous ne mettez qu'une seule activité, vous avez plus de chance de l'avoir.
</p>
<p>
Les activités qui n'ont pas de limite de place (et toutes les activités avec moins de demande que de places) ne comptent pas pour ce système, donc vous pouvez les mettre et vous ne serez pas pénalisés.
</p>
<p>
Vous ne pourrez pas avoir deux activités qui se déroulent en même temps. Si l'une vous est attribué l'autre souhait sera ignoré.
</p>
<p>On n'a pas trouvé le code des années précédentes, mais je suspecte fortement que ce soit un algo similaire en départageant les égalités aléatoirement plutôt qu'au shotgun.</p>
<p>Plus précisément : l'algorithme se base sur le problème hôpital-résident&nbsp;:</p>
<ol>
<li>Il commence par essayer d'attribuer une activité à chaque participant.es au mieux possible, en utilisant la librairie matching de python. Les égalités sont départagées aléatoirement. Plus un choix est haut dans votre liste de souhait, plus vous avez de chance de vous le voir attribuer. Si vous n'avez qu'un seul choix, vous avez plus de chance de vous le voir attribué. (les participant.es avec un seul choix sont automatique placé.es avant ceux qui en ont plusieurs)</li>
<li>Toutes les activités attribuées sont supprimées, ainsi que les voeux résolus des joueurs.</li>
<li>Tant qu'il reste des place dans des activités et des participant.es qui veulent y participer, on recommence à l'étape 1.</li>
</ol>
<p>Le code est sur <a href="https://github.com/Imakoala/InterludesMatchings">github</a>, il ne marche pas encore parfaitement, et on risque de devoir bidouiller à la main en plus pour résoudre tous les cas particuliers (conflits d'horaires, activité présente plusieurs fois...).
{% endblock %}
......@@ -4,76 +4,87 @@
{% block "content" %}
<h2>Quelle différence entre l'inscription sur HelloAsso et sur le site&nbsp;?</h2>
<p>L'inscription HelloAsso vous inscrit à l'événement, au logement et aux repas (selon l'option que vous prenez). Sur le site internet, vous pouvez créer un compte pour vous inscrire aux différentes activités qui seront proposées lors de l'événement. </p>
<h2>Quelles seront les mesures de protection sanitaire&nbsp;?</h2>
<p>Les mesures définitives vous seront communiquées à l'arrivée à l'événement. Elles inclueront probablement&nbsp;:</p>
<p>Les mesures définitives vous seront communiquées à l'arrivée à l'événement. Elles incluront probablement&nbsp;:</p>
<ul>
<li>Contrôle des passes sanitaires chaque jour</li>
<li>Port du masque obligatoire en permanence</li>
<li>Lavage de main entre chaque jeu/activité</li>
<li>Pause aération des salles régulières</li>
<li>Lavage de main entre chaque jeu/activité conseillé</li>
<li>Des QR Codes à scanner sur chaque table (pour garder trace des cas contacts si une personne est déclarée positive)</li>
<li>Salles aérées en permanence et joueur&middot;ses invité&middot;es à se rendre dehors si le taux de CO2 devient trop élevé</li>
</ul>
{% if settings.activity_submission_form %}
<h2>Comment proposer une activité ?</h2>
<p>Vous pouver proposer une activité en remplissant <a href="{{ settings.activity_submission_form }}">ce formulaire</a>.</p>
<p>Il vous faudra renseigner votre nom/mail, le titre, une description, le nombre de places, la durée, les horaires possibles/idéaux, et tout autre besoin particulier...
</p>
{% endif %}
<h2>Quelles seront les conditions pour dormir/manger</h2>
<p> Il est possible que nous n'ayons pas le droit de faire dormir les participants et/ou
de distribuer des repas pendant l'événement pour des raisons sanitaires.
Nous communiquerons les plus tôt possible les mesures définitives.
</p>
<p>En l'état actuel, nous espérons pouvoir distribuer des repas qu'il faudra manger en extérieur ou
dans des salles dédiées.
</p>
<p>
Si vous dormez sur place il vous faudra prendre tapis de sol et duvet. Une salle (probablement le gymnase) y sera dédiée.
</p>
<h2>Comment se rendre à l'ENS Ulm ?</h2>
<div>
<p class="centered">
<i class="fas fa-map-marker-alt"></i>
45 rue d'Ulm, 75005 Paris
</p>
<h2>Comment proposer une activité&nbsp;?</h2>
{% if settings.activity_submission_open %}
<p>Vous pouver proposer une activité en remplissant <a href="{% url 'activity_submission' %}">ce formulaire</a>.</p>
<p>Il vous faudra être connecté et renseigner, le titre, une description, le nombre de places, la durée, les horaires possibles/idéaux, et tout autre besoin particulier...</p>
{% else %}
<p>L'appel a activités est fermé pour le moment.
Contactez nous en cas de besoin urgent ou d'idée trop géniale pour être délaissée.</p>
{% endif %}
<iframe id="interactive-map" width="750" height="400" src="https://www.openstreetmap.org/export/embed.html?bbox=2.334809303283692%2C48.8368567401711%2C2.3548936843872075%2C48.84636099015179&amp;layer=hot&amp;marker=48.841602029496684%2C2.344851493835449"></iframe>
</div>
<h2>Quelles seront les conditions pour dormir/manger&nbsp;?</h2>
<p> Nous proposerons un logement en tente, pour celleux qui le souhaitent</p>
<p> Les repas seront préparés et servis sur place.</p>
<p> Si vous dormez dans les tentes, pensez à prendre un tapis de sol et duvet.</p>
<div id="public-transport-info">
<div id="transport-metro-icon">
<img src="{% static 'imgs/ratp/metro-7.svg' %}" alt="Métro 7">
</div>
<span id="transport-metro-stop">Place Monge</span>
<h2>Comment se rendre à l'événement&nbsp;?</h2>
<div>
<p class="centered">
<i class="fas fa-map-marker-alt"></i>
<div id="transport-rer-icon">
<img src="{% static 'imgs/ratp/rer-B.svg' %}" alt="RER B">
</div>
<span id="transport-rer-stop">Luxembourg</span>
<div id="transport-bus-1-icon">
<img src="{% static 'imgs/ratp/bus-24.svg' %}" alt="Bus 24">
</div>
<span id="transport-bus-1-stop">École Normale Supérieure</span>
</p>
<div id="transport-bus-2-icon">
<img src="{% static 'imgs/ratp/bus-21.svg' %}" alt="Bus 21">
<img src="{% static 'imgs/ratp/bus-27.svg' %}" alt="Bus 27">
</div>
<span id="transport-bus-2-stop">Feuillantines</span>
<iframe id="interactive-map" width="750" height="400"
src="https://www.openstreetmap.org/export/embed.html?bbox=4.82691%2C45.73663%2C4.84084%2C45.72936&amp;layer=hot&amp;marker=45.73298%2C4.83397"></iframe>
</div>
<div id="transport-noctilien-icon">
<img src="{% static 'imgs/ratp/noctilien-14.svg' %}" alt="Noctilien 14">
<img src="{% static 'imgs/ratp/noctilien-21.svg' %}" alt="Noctilien 21">
<img src="{% static 'imgs/ratp/noctilien-122.svg' %}" alt="Noctilien 122">
</div>
<span id="transport-noctilien-stop">Auguste Comte</span>
<div id="public-transport-info">
<div id="transport-tcl-metro">
<img src="{% static 'imgs/tcl/B.svg' %}" alt="Métro B">
</div>
<div id="transport-tcl-tram-1">
<img src="{% static 'imgs/tcl/T1.svg' %}" alt="Tram T1">
</div>
<div id="transport-tcl-tram-2">
<img src="{% static 'imgs/tcl/T6.svg' %}" alt="Tram T6">
</div>
<div id="transport-tcl-bus-1">
<img src="{% static 'imgs/tcl/34.svg' %}" alt="Bus 34">
</div>
<div id="transport-tcl-bus-2">
<img src="{% static 'imgs/tcl/60.svg' %}" alt="Bus 60">
</div>
<div id="transport-tcl-bus-3">
<img src="{% static 'imgs/tcl/64.svg' %}" alt="Bus 64">
</div>
<span id="transport-tcl-stop">Arrêt : Debourg</span>
</div>
{% endblock %}
\ No newline at end of file
<h2>Comment sont réparties les activités&nbsp;?</h2>
<p>
La répartition est faite par un algorithme puis vérifiée à la main.
Dans la mesure du possible, l'algorithme essaie d'attribue au moins une activité par personne. Par conséquent, si vous ne mettez qu'une seule activité, vous avez plus de chance de l'avoir.
</p>
<p>
Les activités qui n'ont pas de limite de place (et toutes les activités avec moins de demande que de places) ne comptent pas pour ce système, donc vous pouvez les mettre et vous ne serez pas pénalisés.
</p>
<p>
Vous ne pourrez pas avoir deux activités qui se déroulent en même temps. Si l'une vous est attribué l'autre souhait sera ignoré.
</p>
<p>On n'a pas trouvé le code des années précédentes, mais je suspecte fortement que ce soit un algo similaire en départageant les égalités aléatoirement plutôt qu'au shotgun.</p>
<p>Plus précisément : l'algorithme se base sur le problème hôpital-résident&nbsp;:</p>
<ol>
<li>Il commence par essayer d'attribuer une activité à chaque participant.es au mieux possible, en utilisant la librairie matching de python. Les égalités sont départagées aléatoirement. Plus un choix est haut dans votre liste de souhait, plus vous avez de chance de vous le voir attribuer. Si vous n'avez qu'un seul choix, vous avez plus de chance de vous le voir attribué. (les participant.es avec un seul choix sont automatique placé.es avant ceux qui en ont plusieurs)</li>
<li>Toutes les activités attribuées sont supprimées, ainsi que les voeux résolus des joueurs.</li>
<li>Tant qu'il reste des place dans des activités et des participant.es qui veulent y participer, on recommence à l'étape 1.</li>
</ol>
<p>Le code est sur <a href="https://github.com/Imakoala/InterludesMatchings">github</a>, il ne marche pas encore parfaitement, et on risque de devoir bidouiller à la main en plus pour résoudre tous les cas particuliers (conflits d'horaires, activité présente plusieurs fois...).
<h2>J'ai encore une question, je fais quoi ?</h2>
<p> Hésite pas à nous passer un mail à <span class="antispam">{{ settings.contact_email_reversed }}</span> pour nous poser tes questions !</p>
{% endblock %}
......@@ -3,17 +3,83 @@
{% block nav_home %}current{% endblock %}
{% block "content" %}
<h2>Présentation</h2>
{% if settings.discord_link %}
<ul class="messagelist"><li class="info">
Rejoins notre <a href="{{ settings.discord_link }}">serveur discord</a> pour participer à l'événement.{{ settings.global_message }}
</li></ul>
{% endif %}
<p>
Les interludes, ou interENS ludiques, regroupent annuellement
les étudiants des quatres ENS de France autour d'activités ludiques.
les étudiants des quatre ENS de France autour d'activités ludiques.
Jeux de plateau, jeux de rôles, jeux vidéos, murders et autres sont
à l'honneur durant ce week-end de trois jours.
</p>
<p>
Cette année, c'est au tour de l'ENS Ulm d'organiser les interludes.
L'événement a habituellement lieu en février mais à cause du COVID,
il a été retardé et est prévu le week-end du vendredi 9 au dimanche 11
avril 2021.
Cette année, c'est au tour de l'{{ settings.hosting_school }} d'organiser les interludes.
Elles auront lieu
{% if settings.date_start %}{% if settings.date_end %}
le week-end du <strong>{{ settings.date_start.day }}-{{ settings.date_end }}</strong>.
{% else %}
le week-end du <strong>{{ settings.date_start }}</strong>.
{% endif %}
{% else %}
à une date non-fixée.
{% endif %}
</p>
<h2>Inscriptions</h2>
<h3>Inscription à l'événement</h3>
<p>
L'inscription à l'événement,
à l'hébergement et aux repas se fait sur <a
href="https://www.helloasso.com/associations/bureau-ludique-de-l-ens-de-lyon/evenements/interludes-ens-de-lyon">le
formulaire HelloAsso</a>
</p>
<h3>Inscription aux activités</h3>
<p>
Une fois votre inscription à l'événement effectuée, vous pourrez vous <a href="{% url 'inscription'%}">inscrire aux activités</a>
</p>
{% if settings.inscriptions_start and settings.inscriptions_end %}
<p>Les inscriptions seront ouvertes
du <strong>{{ settings.inscriptions_start|date:"l d F Y à H:i" }}</strong>
au <strong>{{ settings.inscriptions_end|date:"l d F Y à H:i" }}</strong>.
</p>
{% elif settings.inscriptions_start %}
<p>Les inscriptions ouvrirons le <strong>{{ settings.inscriptions_start|date:"l d F Y à H:i" }}</strong>.</p>
{% elif settings.inscriptions_end %}
<p>Les inscriptions fermerons le <strong>{{ settings.inscriptions_end|date:"l d F Y à H:i" }}</strong>.</p>
{% endif %}
<h2>Tarifs</h2>
<p>
Les tarifs sont différenciés entre salarié·es et non-salarié·es.
<ul>
<li><strong>Participation à l'événement :</strong> 6€/4€</li>
<li><strong>Repas (du vendredi soir au dimanche midi) :</strong> 10€/6€</li>
<li><strong>Logement en tente :</strong> 3€</li>
</ul>
</p>
<h2>Menu des repas</h2>
<p>
Cette année, le menu sera entièrement végétarien, avec options végan et/ou sans gluten possibles.
<ul>
<li><strong>Vendredi soir :</strong> Chili sin carne, riz & compote</li>
<li><strong>Samedi midi :</strong> Brunch (Salade bar, Croques pesto-mozza, Houmous, Gâteaux)</li>
<li><strong>Samedi soir :</strong> Soupe de saison & Tartiflette (option vegan : gratin de pommes de terre aux champignons)</li>
<li><strong>Dimanche midi :</strong> Sandwich au choix</li>
</ul>
<h2>Liens divers</h2>
<ul>
<li>Le code source de ce site est sur <a href="https://github.com/Pantoofle/site-interludes">github</a>.</li>
<li>Un historique des interludes avec leurs visuels, site webs et photos est sur le <a
href="https://wiki.crans.org/VieBdl/InterLudes">wiki de Paris-Saclay</a>.</li>
</ul>
{% endblock %}
......@@ -3,9 +3,29 @@
{% block nav_inscription %}current{% endblock %}
{% block "content" %}
<h2>Inscriptions</h2>
<p>
Les inscriptions ne sont pas encores ouvertes.
Nous communiquerons pas mail via les BDE des différentes écoles pour leur ouverture.
</p>
<h2>Inscriptions à l'événement</h2>
<p>L'inscription à l'événement,
l'hébergement et aux repas se fait sur <a
href="https://www.helloasso.com/associations/bureau-ludique-de-l-ens-de-lyon/evenements/interludes-ens-de-lyon">le
formulaire HelloAsso</a>.</p>
<h2>Inscriptions aux activités</h2>
{% if settings.inscriptions_not_open_yet %}
<p>Les inscriptions aux activités ne sont pas encores ouvertes.</p>
<p>Leur ouverture est prévue le <strong>{{ settings.inscriptions_start|date:"l d F Y à H:i" }}</strong>.</p>
<p>Nous communiquerons par mail via les BDE des différentes écoles pour leur ouverture.</p>
{% elif settings.inscriptions_have_closed %}
<p>Les inscriptions aux activités sont fermées.</p>
<p>
Les inscriptions cette année sont facultatives,
tu peux quand même rejoindre le {% if settings.discord_link %}<a href="{{ settings.discord_link }}">serveur discord</a>{% else %}serveur discord{% endif %} et participer aux jeux libres et
aux activités qui ne nécessitent pas d'inscription.
</p>
{% if settings.contact_email %}
<p>Pour tout problème, contacter&nbsp;:<br><span class="antispam">{{ settings.contact_email_reversed }}</span></p>
{% endif %}
{% else %}
<p>Les inscriptions aux activités ne sont pas encores ouvertes ou ont été fermées.</p>
<p>Nous communiquerons par mail via les BDE des différentes écoles pour leur ouverture.</p>
{% endif %}
{% endblock %}
{% extends "base.html" %}
{% load static %}
{% block nav_inscription %}current{% endblock %}
{% block "content" %}
<h2>Inscriptions</h2>
{% if settings.discord_link %}
<ul class="messagelist">
<li class="info">Rejoignez aussi <a href="{{ settings.discord_link }}">le serveur discord</a>,
C'est là que s'organiseront la plupart des activités.</li>
</ul>
{% endif %}
<form id="main_form" method="post" action="{% url 'inscription' %}">
{% csrf_token %}
Cette année, l'événement étant en distanciel, il est gratuit.
<p>{{ form.school.label_tag }} {{ form.school }}</p>
<h3>Choix d'activités</h3>
<p>Saissisez les activités auquelles vous voulez vous inscrire, par ordre de préférence.
Vous trouverez une description des activités sur <a href="{% url 'activites' %}">cette page</a>.
</p>
<p>
Vous pouvez revenir modifier votre choix jusqu'à la fermeture des
inscriptions{% if settings.inscriptions_end %} (le {{ settings.inscriptions_end|date:"l d F Y à H:i" }}){% endif %}.
</p>
<p>Si vous vous inscrivez à une activité qui nécessite préparation, nous communiquerons
votre email aux orgas pour qu'iels puissent vous contacter.
</p>
{% if formset.non_form_errors %}
{{ formset.non_form_errors }}
{% endif %}
{{ formset.management_data }}
{{ formset.management_form }}
{% for form in formset %}
<div class="activity-form flex">
{{ form.as_p }}
<button class="button delete-activity" style="align-self: center; flex-grow: 0;">Supprimer</button>
</div>
{% endfor %}
<button class="button" id="add-activity">Ajouter une activité</button>
<div class="flex">
<input type="submit" value="Valider">
<a class="button" href="{% url 'profile' %}">Annuler</a>
</div>
</form>
<script>
const button_add_activity = document.querySelector("#add-activity");
const button_submit_form = document.querySelector('[type="submit"]');
const activity_form = document.getElementsByClassName("activity-form");
const main_form = document.querySelector("#main_form");
const total_forms = document.querySelector("#id_form-TOTAL_FORMS");
const form_regex = /form-(\d*)-/g;
var form_count = activity_form.length - 1;
function add_new_form(event) {
// adds a new activity form when clicking on the + button
event.preventDefault();
// clone the first form and replaces it's id
const new_form = activity_form[0].cloneNode(true);
form_count++;
new_form.innerHTML = new_form.innerHTML.replace(form_regex, `form-${form_count}-`);
// add it and increment form total
main_form.insertBefore(new_form, button_add_activity);
new_form.querySelector("select").value = "";
total_forms.setAttribute("value", `${form_count+1}`);
}
button_add_activity.addEventListener("click", add_new_form);
function delete_form(event) {
if (!event.target.classList.contains("delete-activity")) return;
event.preventDefault();
if (form_count == 0) {
// don't delete the first element
activity_form[0].querySelector("select").value = "";
return;
}
event.target.parentElement.remove();
form_count--;
total_forms.setAttribute("value", `${form_count+1}`);
// update form numbers
let count = 0;
for (const form of activity_form) {
// the replace changes the field value
// so we save and restore it
const select = form.querySelector("select");
const value = select.value;
form.innerHTML = form.innerHTML.replace(form_regex, `form-${count++}-`);
form.querySelector("select").value = value;
}
}
main_form.addEventListener("click", delete_form);
</script>
{% endblock %}
......@@ -4,42 +4,44 @@
{% block nav_inscription %}current{% endblock %}
{% block "content" %}
<h2>Inscriptions</h2>
<ul class="messagelist">
<li class="info">
Il est possible que nous n'ayons pas le droit de faire dormir les participants et/ou
de distribuer des repas pendant l'événement pour des raisons sanitaires.<br>
Nous communiquerons les mesures exactes quelques jours avant l'événement.
</li>
</ul>
<h2>Inscription à l'événement</h2>
L'inscription à l'événement,
à l'hébergement et aux repas se fait sur <a
href="https://www.helloasso.com/associations/bureau-ludique-de-l-ens-de-lyon/evenements/interludes-ens-de-lyon">le
formulaire HelloAsso</a>
<h2>Inscription aux activités</h2>
Une fois votre inscription à l'événement effectuée, vous pourrez
vous inscrire à certaines activités sur cette page. La plupart des
activités ne demandent pas d'inscription et seront en libre accès
durant tout l'événement, mais certaines demandent une inscription à
l'avance.
<form id="main_form" method="post" action="{% url 'inscription' %}">
{% csrf_token %}
Cette année, l'événement est entièrement subventionné par le <a class="external" href="https://cof.ens.fr/">COF</a>. L'inscription est gratuite
(mais n'inclut pas les frais de transport jusqu'à Paris).
<p>{{ form.school.label_tag }} {{ form.school }}</p>
<p>{{ form.sleeps.label_tag }} {{ form.sleeps }}</p>
<h3>Repas</h3>
<table>
<tr><td>Vendredi soir&nbsp;:</td><td>{{ form.meal_friday_evening }}</td></tr>
<tr><td>Samedi matin&nbsp;:</td><td>{{ form.meal_saturday_morning }}</td></tr>
<tr><td>Samedi midi&nbsp;:</td><td>{{ form.meal_saturday_midday }}</td></tr>
<tr><td>Samedi soir&nbsp;:</td><td>{{ form.meal_saturday_evening }}</td></tr>
<tr><td>Dimanche matin&nbsp;:</td><td>{{ form.meal_sunday_morning }}</td></tr>
<tr><td>Dimanche midi&nbsp;:</td><td>{{ form.meal_sunday_midday }}</td></tr>
</table>
<p>{{ form.meal_friday_evening.label_tag }} {{ form.meal_friday_evening }} </p>
<p>{{ form.meal_saturday_morning.label_tag }} {{ form.meal_saturday_morning }} </p>
<p>{{ form.meal_saturday_midday.label_tag }} {{ form.meal_saturday_midday }}</p>
<p>{{ form.meal_saturday_evening.label_tag }} {{ form.meal_saturday_evening }} </p>
<p>{{ form.meal_sunday_morning.label_tag }} {{ form.meal_sunday_morning}} </p>
<p>{{ form.meal_sunday_midday.label_tag }} {{ form.meal_sunday_midday }}</p>
<h3>Choix d'activités</h3>
<p>Vous êtes considéré⋅e commme extérieur si vous n'êtes pas actuellement scolarisé⋅e ou inscrit⋅e à l'ENS Paris-Saclay. Nous avons besoin de cette information pour transmettre la liste des extérieurs à l'administration de l'ENS.</p>
<p>Saissisez les activités auquelles vous voulez vous inscrire, par ordre de préférence.</p>
<p>Vous pouvez revenir modifier votre choix jusqu'à la fermeture des inscriptions.</p>
<h3>Choix d'activités</h3>
<p>Saissisez les activités auquelles vous voulez vous inscrire, par ordre de préférence.
Vous trouverez une description des activités sur <a href="{% url 'activites' %}">cette page</a>.
</p>
<p>
Vous pouvez revenir modifier votre choix jusqu'à la fermeture des
inscriptions{% if settings.inscriptions_end %} (le {{ settings.inscriptions_end|date:"l d F Y à H:i" }}){% endif %}.
</p>
<p>Si vous vous inscrivez à une activité qui nécessite préparation, nous communiquerons
votre email aux orgas pour qu'iels puissent vous contacter.
</p>
{% if formset.non_form_errors %}
{{ formset.non_form_errors }}
{% endif %}
......@@ -56,7 +58,7 @@
<div class="flex">
<input type="submit" value="Valider">
<a class="button" href="{% url 'accounts:profile' %}">Annuler</a>
<a class="button" href="{% url 'profile' %}">Annuler</a>
</div>
</form>
......
......@@ -4,7 +4,9 @@
{% block "content" %}
<h2>Inscriptions</h2>
<p>Vous devez être connecté pour pouvoir vous inscrire à l'événement.</p>
<p>Aller à la page de <a href="{% url 'accounts:login' %}">connexion</a> pour vous connectez
ou à celle de <a href="{% url 'accounts:create' %}">création de compte</a> si vous n'avez pas de compte.</p>
<p>Vous devez être connecté pour pouvoir vous inscrire à des activités.
<p>Aller à la page de <a href="{% url 'account_login' %}">connexion</a> pour vous connecter ou créer un compte.
{% if settings.inscriptions_end %}
<p>Les inscriptions seront ouvertes jusqu'au {{ settings.inscriptions_end|date:"l d F Y à H:i" }})</p>
{% endif %}
{% endblock %}
......@@ -10,21 +10,35 @@
{% if user.is_superuser %}
<ul class="messagelist">
<li class="info">Vous avez les droits d'administrateurs. Aller à la <a href="{% url 'site_admin' %}">page d'administration du site</a></li>
<li class="info">
Vous avez les droits d'administrateurs.
<br>Aller à la <a href="{% url 'admin_pages:index' %}">page d'administration du site</a>
{% if user.is_staff %}
<br>Aller à la <a href="{% url 'admin:index' %}">page d'administration de django</a>
(N'y modifier rien sy vous n'êtes pas sûrs de vous)
{% endif %}
</li>
</ul>{% endif %}
{% if user.profile.is_registered %}
<strong>Mon inscription&nbsp;:</strong>
{% if settings.inscriptions_open and settings.inscriptions_end %}
modifiable jusqu'au {{ settings.inscriptions_end|date:"l d F Y à H:i" }}
{% endif %}
<ul>
<li>{% if user.profile.sleeps %}Inscrit pour dormir sur place{% else %}Ne dors pas sur place{% endif %}</li>
<!--<li>{% if user.profile.mug %}Commande une tasse{% else %}Ne commande pas de tasse{% endif %}</li>-->
<li>Inscrit à {{ user.profile.nb_meals }} repas.</li>
<!-- <li>{% if user.profile.sleeps %}Inscrit pour dormir sur place{% else %}Ne dors pas sur place{% endif %}</li> -->
<!-- <li>{% if user.profile.mug %}Commandse une tasse{% else %}Ne commande pas de tasse{% endif %}</li> -->
<!-- <li>Inscrit à {{ user.profile.nb_meals }} repas.</li> -->
{% if settings.activities_allocated %}
{% if my_activities %}
<li>Inscrit à {{ my_activities|length }} activités&nbsp;:
{% if my_choices %}
<li>Inscrit à {{ my_choices|length }} activités&nbsp;:
<ul>
{% for activity in my_activities %}
<li>{{ activity.activity }}</li>
{% for choice in my_choices %}
<li><a href="{% url 'activites' %}#{{ choice.slot.activity.slug }}">{{ choice.slot }}</a>
{% if choice.slot.on_planning %}
(le {{ choice.slot.start|date:"l à H:i" }}<!-- en {{ choice.slot.room }} -->)
{% endif %}
</li>
{% endfor %}
</ul>
</li>
......@@ -32,11 +46,15 @@
<li>Inscrit à aucune activité</li>
{% endif %}
{% else %}
{% if my_activities %}
<li>{{ my_activities|length }} activités souhaitées&nbsp;:
{% if my_choices %}
<li>{{ my_choices|length }} activités souhaitées&nbsp;:
<ol>
{% for activity in my_activities %}
<li>{{ activity.activity }}</li>
{% for choice in my_choices %}
<li><a href="{% url 'activites' %}#{{ choice.slot.activity.slug }}">{{ choice.slot }}</a>
{% if choice.slot.on_planning %}
(le {{ choice.slot.start|date:"l à H:i" }}<!-- en {{ choice.slot.room }} -->)
{% endif %}
</li>
{% endfor %}
</ol>
</li>
......@@ -47,10 +65,18 @@
{% endif %}
</ul>
{% else %}
<strong>Vous n'êtes pas incrit à l'événement.</strong>
<strong>Vous n'avez pas encore renseigné vos choix d'activités.</strong>
{% if not settings.inscriptions_open %}
<p>Les inscriptions ne sont pas encore ouvertes ou ont été fermées.</p>
{% if settings.inscriptions_not_open_yet %}
<p>Les inscriptions aux activités ne sont pas encores ouvertes. Elles ouvrirons le <strong>{{ settings.inscriptions_start|date:"l d F Y à H:i" }}</strong>.</p>
{% elif settings.inscriptions_have_closed %}
<p>Les inscriptions aux activités sont fermées.</p>
{% else %}
<p>Les inscriptions aux activités ne sont pas encores ouvertes ou ont été fermées.</p>
{% endif %}
{% elif settings.inscriptions_end %}
<p>les inscriptions aux activités sont ouvertes jusqu'au {{ settings.inscriptions_end|date:"l d F Y à H:i" }}).</p>
{% endif %}
<br><br>
{% endif %}
......@@ -76,7 +102,7 @@
{% endif %}
<a class="button" href="{% url 'accounts:update' %}">Modifier mes informations</a>
<a class="button" href="{% url 'accounts:logout' %}">Déconnexion</a>
<a class="button" href="{% url 'account_logout' %}">Déconnexion</a>
</div>
{% endblock %}
\ No newline at end of file
{% endblock %}
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.views.generic import RedirectView
from django.urls import path, include
......@@ -11,15 +13,17 @@ urlpatterns = [
path('inscription/', views.RegisterView.as_view(), name = 'inscription'),
path('desinscription/', views.UnregisterView.as_view(), name="desinscription"),
path('activites/', views.ActivityView.as_view(), name = 'activites'),
path('activites/nouvelle/', views.ActivitySubmissionView.as_view(), name = 'activity_submission'),
path('faq/', views.FAQView.as_view(), name = 'FAQ'),
path("profil/", views.ProfileView.as_view(), name="profile"),
path('favicon.ico', RedirectView.as_view(url='/static/imgs/favicon.ico')),
path('admin_site/', views.AdminView.as_view(), name="site_admin"),
path('export/activities/', views.ExportActivities.as_view(), name="activities.csv"),
path('export/participants/', views.ExportParticipants.as_view(), name="participants.csv"),
path('export/activity_choices/', views.ExportActivityChoices.as_view(), name="activity_choices.csv"),
path(
'sitemap.xml', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'
),
path('accounts/', include("accounts.urls")),
path('admin_pages/', include(('admin_pages.urls', 'admin_pages'), namespace="admin_pages")),
path('comptes/', include("accounts.urls")),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
......@@ -3,17 +3,14 @@ from datetime import timedelta
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.sitemaps import Sitemap
from django.db.models import Count
from django.forms import formset_factory
from django.shortcuts import redirect, render
from django.urls import reverse
from django.views.generic import RedirectView, UpdateView, TemplateView, View
from django.urls import reverse, reverse_lazy
from django.views.generic import FormView, RedirectView, TemplateView, View
from accounts.models import EmailUser
from home.models import ActivityList, InterludesActivity, InterludesParticipant
from home.forms import ActivityForm, BaseActivityFormSet, InscriptionForm
from home import models
from home.forms import ActivityForm, ActivitySubmissionForm, BaseActivityFormSet, InscriptionForm
from site_settings.models import SiteSettings
from shared.views import CSVWriteView, SuperuserRequiredMixin
# ==============================
......@@ -25,6 +22,20 @@ class HomeView(TemplateView):
"""Vue pour la page d'acceuil"""
template_name = "home.html"
def get_planning_context():
"""Returns the context dict needed to display the planning"""
settings = SiteSettings.load()
context = dict()
context['planning'] = models.SlotModel.objects.filter(on_planning=True).order_by("title")
if settings.date_start is not None:
context['friday'] = settings.date_start.day
context['saturday'] = (settings.date_start + timedelta(days=1)).day
context['sunday'] = (settings.date_start + timedelta(days=2)).day
else:
context['friday'] = 1
context['saturday'] = 2
context['sunday'] = 3
return context
class ActivityView(TemplateView):
"""Vue pour la liste des activités"""
......@@ -33,12 +44,8 @@ class ActivityView(TemplateView):
def get_context_data(self, **kwargs):
"""ajoute la liste des activités au contexte"""
context = super(ActivityView, self).get_context_data(**kwargs)
settings = SiteSettings.load()
context['activities'] = InterludesActivity.objects.filter(display=True).order_by("title")
context['planning'] = InterludesActivity.objects.filter(on_planning=True).order_by("title")
context['friday'] = settings.date_start.day
context['saturday'] = (settings.date_start + timedelta(days=1)).day
context['sunday'] = (settings.date_start + timedelta(days=2)).day
context['activities'] = models.ActivityModel.objects.filter(display=True).order_by("title")
context.update(get_planning_context())
return context
......@@ -48,10 +55,32 @@ class FAQView(TemplateView):
# ==============================
# Registration
# Profile and registration
# ==============================
class ProfileView(LoginRequiredMixin, TemplateView):
"""Vue des actions de gestion de son profil"""
template_name = "profile.html"
redirect_field_name = "next"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
settings = SiteSettings.load()
if settings.activities_allocated:
my_choices = models.ActivityChoicesModel.objects.filter(
participant=self.request.user.profile,
accepted=True
)
else:
my_choices = models.ActivityChoicesModel.objects.filter(
participant=self.request.user.profile
)
context["my_choices"] = my_choices
return context
class RegisterClosed(TemplateView):
"""Vue pour quand les inscriptions ne sont pas ouvertes"""
template_name = "inscription/closed.html"
......@@ -66,30 +95,33 @@ class RegisterUpdateView(LoginRequiredMixin, TemplateView):
template_name = "inscription/form.html"
form_class = InscriptionForm
formset_class = formset_factory(form=ActivityForm, extra=3, formset=BaseActivityFormSet)
success_url = reverse_lazy("profile")
@staticmethod
def get_activities(participant):
activities = ActivityList.objects.filter(participant=participant).order_by("priority")
return [{"activity": act.activity} for act in activities]
def get_slots(participant):
activities = models.ActivityChoicesModel.objects.filter(participant=participant).order_by("priority")
return [{"slot": act.slot} for act in activities]
@staticmethod
def set_activities(participant, formset):
# delete old activites
ActivityList.objects.filter(participant=participant).delete()
models.ActivityChoicesModel.objects.filter(participant=participant).delete()
priority = 0
for form in formset:
data = form.cleaned_data
if data:
activity = data["activity"]
ActivityList(priority=priority, participant=participant, activity=activity).save()
slot = data["slot"]
models.ActivityChoicesModel(
priority=priority, participant=participant, slot=slot
).save()
priority += 1
def get(self, request, *args, **kwargs):
participant = request.user.profile
activities = self.get_activities(participant)
slots = self.get_slots(participant)
form = self.form_class(instance=participant)
formset = self.formset_class(initial=activities)
formset = self.formset_class(initial=slots)
context = {"form": form, "formset": formset}
return render(request, self.template_name, context)
......@@ -104,7 +136,7 @@ class RegisterUpdateView(LoginRequiredMixin, TemplateView):
self.set_activities(request.user.profile, formset)
messages.success(request, "Votre inscription a bien été enregistrée")
return redirect("accounts:profile", permanent=False)
return redirect(self.success_url, permanent=False)
class RegisterView(View):
"""Vue pour l'inscription
......@@ -119,7 +151,7 @@ class RegisterView(View):
class UnregisterView(LoginRequiredMixin, RedirectView):
pattern_name = "accounts:profile"
pattern_name = "profile"
def get_redirect_url(self, *args, **kwargs):
participant = self.request.user.profile
......@@ -130,180 +162,55 @@ class UnregisterView(LoginRequiredMixin, RedirectView):
# ==============================
# Admin views
# Activity Submission Form
# ==============================
class AdminView(SuperuserRequiredMixin, TemplateView):
template_name = "admin.html"
def get_metrics(self):
registered = InterludesParticipant.objects.filter(is_registered = True)
acts = InterludesActivity.objects.all()
wishes = ActivityList.objects.filter(participant__is_registered=True)
class metrics:
participants = registered.count()
ulm = registered.filter(school="U").count()
lyon = registered.filter(school="L").count()
rennes = registered.filter(school="R").count()
saclay = registered.filter(school="P").count()
non_registered = EmailUser.objects.filter(is_active=True).count() - participants
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
# mugs = registered.filter(mug=True).count()
sleeps = registered.filter(sleeps=True).count()
activites = acts.count()
act_ins = acts.filter(must_subscribe=True).count()
wish = wishes.count()
granted = wishes.filter(accepted=True).count()
# validation de la repartition des activités
accepted = wishes.filter(accepted=True)
# order_by is useless but required
counts = accepted.values("activity").annotate(total=Count("id")).order_by("activity")
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"""
activities = InterludesActivity.objects.filter(must_subscribe=True, display=True)
min_fails = ""
max_fails = ""
for act in activities:
total = ActivityList.objects.filter(
activity=act, accepted=True, participant__is_registered=True
).aggregate(total=Count("id"))["total"]
max = act.max_participants
min = act.min_participants
if max != 0 and max < total:
max_fails += "<br> &bullet;&ensp;{}: {} inscrits (maximum {})".format(
act.title, total, max
)
if min > total:
min_fails += "<br> &bullet;&ensp;{}: {} inscrits (minimum {})".format(
act.title, total, max
)
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"""
activities = InterludesActivity.objects.filter(must_subscribe=True, display=True)
conflicts = []
for i, act1 in enumerate(activities):
for act2 in activities[i+1:]:
if act1.conflicts(act2):
conflicts.append((act1, act2))
base_qs = ActivityList.objects.filter(accepted=True, participant__is_registered=True)
errors = ""
for act1, act2 in conflicts:
participants1 = base_qs.filter(activity=act1).values("participant")
participants2 = base_qs.filter(activity=act1).values("participant")
intersection = participants1 & participants2
if intersection:
print(intersection)
errors += '<br> &bullet;&ensp; {} participe(nt) à la fois à "{}" et à "{}"'.format(
",".join(str(InterludesParticipant.objects.get(id=x["participant"])) for x in intersection),
act1.title, act2.title
)
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>'
class ActivitySubmissionView(LoginRequiredMixin, FormView):
"""Vue pour proposer une activité"""
template_name = "activity_submission.html"
form_class = ActivitySubmissionForm
success_url = reverse_lazy("profile")
def validate_activity_allocation(self):
@staticmethod
def submission_check():
"""Vérifie si le formulaire est ouvert ou non"""
settings = SiteSettings.load()
validations = '<ul class="messagelist">'
return settings.activity_submission_open
def get_initial(self):
init = super().get_initial()
user = self.request.user
init.update({
"host_name": "{} {}".format(user.first_name, user.last_name),
"host_email": user.email,
})
return init
def not_open(self, request):
"""Appelé quand le formulaire est désactivé"""
messages.error(request, "La soumission d'activité est desactivée")
return redirect(self.success_url, permanent=False)
# 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 += '</ul>'
return validations
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["metrics"] = self.get_metrics()
context["validations"] = self.validate_activity_allocation()
return context
def get(self, request, *args, **kwargs):
if not self.submission_check():
return self.not_open(request)
return super().get(self, request, *args, **kwargs)
def post(self, request, *args, **kwargs):
if not self.submission_check():
return self.not_open(request)
form = self.form_class(request.POST)
if not form.is_valid():
context = self.get_context_data()
context["form"] = form
return render(request, self.template_name, context)
form.save()
messages.success(request, "Votre activité a bien été enregistrée. Elle sera affichée sur le site après relecture par les admins.")
return redirect(self.success_url, permanent=False)
class ExportActivities(SuperuserRequiredMixin, CSVWriteView):
filename = "activites_interludes"
model = InterludesActivity
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 = InterludesParticipant.objects.filter(is_registered=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 = ActivityList
headers = ["id_participant", "nom_participant", "priorité", "nom_activité", "id_activité"]
def get_rows(self):
activities = ActivityList.objects.all()
rows = []
for act in activities:
if act.participant.is_registered:
rows.append([
act.participant.id, str(act.participant), act.priority,
str(act.activity), act.activity.id
])
return rows
# ==============================
# Sitemap
......
# Secrets that must be changed in production
SECRET_KEY = "i*4$=*fa(644(*!9m2)0-*&sows2uz$b^brb(=)elfn3+y6#1n"
ADMINS = [("superuser", "superuser@admin.fr"),]
DB_NAME = "db.sqlite3"
SERVER_EMAIL = "root@localhost"
DEFAULT_FROM_EMAIL = "webmaster@localhost"
EMAIL_HOST = "localhost"
EMAIL_PORT = 587
EMAIL_HOST_USER = None
EMAIL_HOST_PASSWORD = None
......@@ -19,16 +19,62 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'tx$xi%n!8cghirp377zb)gd24g#=&w*ik(bx2h(i8ji0_&9_5l'
# SECURITY WARNING: don't run with debug turned on in production!
try:
from . import secret
except ImportError:
raise ImportError(
"The interludes/secret.py file is missing.\n"
"Run 'make secret' to generate a secret."
)
def import_secret(name):
"""
Shorthand for importing a value from the secret module and raising an
informative exception if a secret is missing.
"""
try:
return getattr(secret, name)
except AttributeError:
raise RuntimeError("Secret missing: {}".format(name))
SITE_ID=1
SECRET_KEY = import_secret("SECRET_KEY")
DB_NAME = import_secret("DB_NAME")
ADMINS = import_secret("ADMINS")
SERVER_EMAIL = import_secret("SERVER_EMAIL")
DEFAULT_FROM_EMAIL = import_secret("DEFAULT_FROM_EMAIL")
EMAIL_HOST = import_secret("EMAIL_HOST")
EMAIL_PORT = import_secret("EMAIL_PORT")
EMAIL_HOST_USER = import_secret("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = import_secret("EMAIL_HOST_PASSWORD")
EMAIL_USE_SSL = True
# FIXME - set to False in production
DEBUG = True
ADMINS = [("respos", "respointerludes2021@ens.psl.eu"),]
# FIXME - set hosts in production
ALLOWED_HOSTS = ["127.0.0.1", "localhost","kwei-dev.crans.org"]
if DEBUG:
# This will display emails in Console.
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
else:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
ALLOWED_HOSTS = []
SECURE_SSL_REDIRECT = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_SECONDS = 3600
SECURE_HSTS_PRELOAD = True
SECURE_REFERRER_POLICY = "same-origin"
# Application definition
......@@ -40,11 +86,19 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sitemaps',
'django.contrib.sites',
'home.apps.HomeConfig',
'admin_pages.apps.AdminPagesConfig',
'accounts.apps.AccountsConfig',
'site_settings.apps.SiteSettingsConfig',
'shared.apps.SharedConfig'
'shared.apps.SharedConfig',
# allauth support.
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth_note_kfet',
]
MIDDLEWARE = [
......@@ -78,6 +132,8 @@ TEMPLATES = [
WSGI_APPLICATION = 'interludes.wsgi.application'
# Auto primary key type
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
......@@ -85,33 +141,39 @@ WSGI_APPLICATION = 'interludes.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
'NAME': os.path.join(BASE_DIR, DB_NAME),
}
}
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
)
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
AUTH_USER_MODEL = 'accounts.EmailUser'
AUTH_PROFILE_MODULE = 'home.InterludesParticipant'
AUTH_PROFILE_MODULE = 'home.ParticipantModel'
# Tell oauth how the user is configured.
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
SOCIALACCOUNT_PROVIDERS = {
'notekfet': {
'DOMAIN': 'note-dev.crans.org', # À remplacer si nécessaire
# 'SCOPE': ['1_1'], # Adapter les scopes demandées (par défaut lecture des champs utilisateur⋅rice)
},
}
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
{ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', },
{ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', },
{ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', },
{ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', },
]
# Session time in seconds
......@@ -120,7 +182,7 @@ SESSION_COOKIE_AGE = 3600
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = 'fr-fr'
LANGUAGE_CODE = 'fr'
TIME_ZONE = 'CET'
......@@ -137,9 +199,17 @@ USE_TZ = True
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
LOGIN_URL = "accounts:login"
LOGIN_REDIRECT_URL = "accounts:profile"
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
LOGIN_URL = 'accounts:login'
LOGIN_REDIRECT_URL = 'profile'
# Prefix to mails to admins
EMAIL_SUBJECT_PREFIX = '[DJANGO WEBLUDES] '
# Signature to mails to admins
EMAIL_SIGNATURE = '-- Site Interludes (mail généré automatiquement)'
# This will display emails in Console.
# FIXME: remove in production
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Prefix to mails to users
USER_EMAIL_SUBJECT_PREFIX = "[interludes] "
......@@ -16,7 +16,12 @@ Including another URLconf
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('home.urls')),
]
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + [
path('admin/', admin.site.urls),
path('accounts/', include('allauth.urls')),
path('comptes/', include('allauth.urls')),
path('', include('home.urls')),
]
Django~=3.0.8
\ No newline at end of file
Django~=3.2.7
django-allauth~=0.51.0
\ No newline at end of file
import csv
from shared.views import CSVWriteView
from django.http import HttpResponse
class CSVWriteViewForAdmin(CSVWriteView):
def get_values(self):
return self.queryset.values()
class ExportCsvMixin:
"""
class abstraite pour permettre l'export CSV rapide d'un modele
utiliser csv_export_exclude pour exclure des colonnes du fichier généré
"""
def export_as_csv(self, request, queryset):
"""renvoie un fichier CSV contenant l'information du queryset"""
meta = self.model._meta
field_names = [field.name for field in meta.fields]
if self.csv_export_exclude:
field_names = [field for field in field_names if not field in self.csv_export_exclude]
filename = None
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
writer = csv.writer(response)
csv_export_exclude = []
csv_export_fields = None
writer.writerow(field_names)
for obj in queryset:
writer.writerow([getattr(obj, field) for field in field_names])
def get_filename(self):
if self.filename:
return self.filename
return str(self.model._meta)
return response
def export_as_csv(self, request, queryset):
"""renvoie un fichier CSV contenant l'information du queryset"""
view = CSVWriteViewForAdmin(
request=request, queryset=queryset, model=self.model,
filename=self.get_filename(), exclude_fields=self.csv_export_exclude,
fields=self.csv_export_fields,
)
return view.get(request)
export_as_csv.short_description = "Exporter au format CSV"
actions = ["export_as_csv"]
csv_export_exclude = None
......@@ -18,32 +18,52 @@ class CSVWriteView(View):
filename = "csv_file.csv"
headers = None
model = None
exclude_fields = []
fields = None
def get_filename(self):
return self.filename
def get_headers(self):
"""overload this to dynamicaly generate headers"""
if self.headers:
return self.headers
if self.model:
return [field.name for field in self.model._meta.fields]
return self.get_field_names()
return None
def get_values(self):
"""overload to change queryset used in self.get_rows"""
if self.model:
return self.model.objects.values()
raise NotImplementedError("{}.get_values() isn't implemented when model is None".format(
self.__class__.__name__
))
def get_field_names(self):
"""overload to limit/change field names
default to:
- the value of self.field if not None
- all fields minus those in exclude_fields otherwise"""
if self.fields is not None:
return self.fields
return [
field.name for field in self.model._meta.get_fields()
if not field.name in self.exclude_fields
]
def get_rows(self):
"""overload this to return the list of rows"""
if self.model:
queryset = self.model.objects.values()
fields = self.model._meta.fields
table = []
for row in queryset:
table.append([row[field.name] for field in fields])
return table
raise NotImplementedError("{}.get_rows isn't implemented".format(self.__class__.__name__))
queryset = self.get_values()
fields = self.get_field_names()
table = []
for row in queryset:
table.append([row[field] for field in fields])
return table
def get(self, request, *args, **kwargs):
print("\n\nHelloe\n\n\n")
response = HttpResponse(content_type='text/csv')
filename = self.filename
filename = self.get_filename()
if not filename.endswith(".csv"):
filename += ".csv"
response['Content-Disposition'] = 'attachment; filename="{}"'.format(self.filename)
......@@ -55,5 +75,4 @@ class CSVWriteView(View):
for row in self.get_rows():
writer.writerow(row)
print(writer)
return response
let
pkgs = import <nixpkgs> {};
in pkgs.mkShell {
buildInputs = [
pkgs.python3
pkgs.python39Packages.django
pkgs.python39Packages.pip
];
shellHook = ''
# Tells pip to put packages into $PIP_PREFIX instead of the usual locations.
# See https://pip.pypa.io/en/stable/user_guide/#environment-variables.
export PIP_PREFIX=$(pwd)/_build/pip_packages
export PYTHONPATH="$PIP_PREFIX/${pkgs.python3.sitePackages}:$PYTHONPATH"
export PATH="$PIP_PREFIX/bin:$PATH"
unset SOURCE_DATE_EPOCH
'';
}
\ No newline at end of file
......@@ -14,4 +14,9 @@ class SingletonModelAdmin(admin.ModelAdmin):
@admin.register(SiteSettings)
class SiteSettingsAdmin(SingletonModelAdmin):
pass
\ No newline at end of file
def planning_file_link(self, obj):
if obj.file:
return "<a href='%s'>download</a>" % (obj.file.url,)
else:
return "No attachment"
# The website version number
WEBSITE_VERSION = "2.0.0-beta"
WEBSITE_VERSION_DATE = "2021-10-05"
WEBSITE_FULL_VERSION = "{} - {}".format(
WEBSITE_VERSION, WEBSITE_VERSION_DATE
)
# Update this to force reload of cached css
CSS_VERSION = "1.0"
from site_settings import constants
from site_settings.models import SiteSettings
def settings(request):
return {'settings': SiteSettings.load()}
return {
'settings': SiteSettings.load(),
'constants' : constants,
}