Commit bab39490 authored by ourspalois's avatar ourspalois

Merge branch 'beta' into 'master'

Bugs mineurs, documentation

See merge request !162
parents d5a9bf17 0b93968b
Pipeline #6044 passed with stages
in 9 minutes and 42 seconds
......@@ -10,7 +10,6 @@ DJANGO_SECRET_KEY=CHANGE_ME
DJANGO_SETTINGS_MODULE=note_kfet.settings
CONTACT_EMAIL=tresorerie.bde@localhost
NOTE_URL=localhost
DOMAIN=localhost
# Config for mails. Only used in production
NOTE_MAIL=notekfet@localhost
......
......@@ -279,7 +279,8 @@ Le cahier des charges initial est disponible [sur le Wiki Crans](https://wiki.cr
La documentation des classes et fonctions est directement dans le code et est explorable à partir de la partie documentation de l'interface d'administration de Django.
**Commentez votre code !**
La documentation plus haut niveau sur le développement est disponible sur [le Wiki associé au dépôt Git](https://gitlab.crans.org/bde/nk20/-/wikis/home).
La documentation plus haut niveau sur le développement et sur l'utilisation
est disponible sur <https://note.crans.org/doc> et également dans le dossier `docs`.
## FAQ
......
......@@ -4,10 +4,14 @@
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from rest_framework.serializers import ModelSerializer
from django.utils import timezone
from rest_framework import serializers
from member.api.serializers import ProfileSerializer, MembershipSerializer
from note.api.serializers import NoteSerializer
from note.models import Alias
class UserSerializer(ModelSerializer):
class UserSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
......@@ -22,7 +26,7 @@ class UserSerializer(ModelSerializer):
)
class ContentTypeSerializer(ModelSerializer):
class ContentTypeSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
......@@ -31,3 +35,42 @@ class ContentTypeSerializer(ModelSerializer):
class Meta:
model = ContentType
fields = '__all__'
class OAuthSerializer(serializers.ModelSerializer):
"""
Informations that are transmitted by OAuth.
For now, this includes user, profile and valid memberships.
This should be better managed later.
"""
normalized_name = serializers.SerializerMethodField()
profile = ProfileSerializer()
note = NoteSerializer()
memberships = serializers.SerializerMethodField()
def get_normalized_name(self, obj):
return Alias.normalize(obj.username)
def get_memberships(self, obj):
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now()))
class Meta:
model = User
fields = (
'id',
'username',
'normalized_name',
'first_name',
'last_name',
'email',
'is_superuser',
'is_active',
'is_staff',
'profile',
'note',
'memberships',
)
......@@ -3,6 +3,7 @@
import json
from datetime import datetime, date
from decimal import Decimal
from urllib.parse import quote_plus
from warnings import warn
......@@ -152,6 +153,8 @@ class TestAPI(TestCase):
value = value.isoformat()
elif isinstance(value, ImageFieldFile):
value = value.name
elif isinstance(value, Decimal):
value = str(value)
query = json.dumps({field.name: value})
# Create sample permission
......
......@@ -5,6 +5,7 @@ from django.conf import settings
from django.conf.urls import url, include
from rest_framework import routers
from .views import UserInformationView
from .viewsets import ContentTypeViewSet, UserViewSet
# Routers provide an easy way of automatically determining the URL conf.
......@@ -47,5 +48,6 @@ app_name = 'api'
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
url('^', include(router.urls)),
url('^me/', UserInformationView.as_view()),
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User
from rest_framework.generics import RetrieveAPIView
from .serializers import OAuthSerializer
class UserInformationView(RetrieveAPIView):
"""
These fields are give to OAuth authenticators.
"""
serializer_class = OAuthSerializer
def get_queryset(self):
return User.objects.filter(pk=self.request.user.pk)
def get_object(self):
return self.request.user
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from cas_server.auth import DjangoAuthUser # pragma: no cover
from note.models import Alias
class CustomAuthUser(DjangoAuthUser): # pragma: no cover
"""
Override Django Auth User model to define a custom Matrix username.
"""
def attributs(self):
d = super().attributs()
if self.user:
d["normalized_name"] = Alias.normalize(self.user.username)
return d
# Generated by Django 2.2.19 on 2021-03-13 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0006_create_note_account_bde_membership'),
]
operations = [
migrations.AlterField(
model_name='membership',
name='roles',
field=models.ManyToManyField(related_name='memberships', to='permission.Role', verbose_name='roles'),
),
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2021, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]
# Generated by Django 2.2.19 on 2021-03-13 11:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('note', '0004_remove_null_tag_on_charfields'),
]
operations = [
migrations.AlterField(
model_name='alias',
name='note',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='alias', to='note.Note'),
),
]
......@@ -134,8 +134,6 @@ class PermissionBackend(ModelBackend):
return False
sess = get_current_session()
if sess is not None and sess.session_key is None:
return False
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
return True
......
......@@ -3511,6 +3511,7 @@
56,
57,
58,
135,
137,
143,
147,
......@@ -3521,8 +3522,7 @@
176,
177,
180,
181,
182
181
]
}
},
......
Subproject commit 8ec7d68a169c1072aec427925f3bf2fd54eab5a3
Subproject commit 0c7070aea177e12fae099488e2ea6f8146b97e4d
# Generated by Django 2.2.19 on 2021-03-21 09:34
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('treasury', '0002_invoice_remove_png_extension'),
]
operations = [
migrations.AlterField(
model_name='product',
name='quantity',
field=models.DecimalField(decimal_places=2, max_digits=7, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'),
),
migrations.AlterField(
model_name='specialtransactionproxy',
name='remittance',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='transaction_proxies', to='treasury.Remittance', verbose_name='Remittance'),
),
]
......@@ -5,6 +5,7 @@ from datetime import date
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import Q
from django.template.loader import render_to_string
......@@ -131,12 +132,15 @@ class Product(models.Model):
verbose_name=_("Designation"),
)
quantity = models.PositiveIntegerField(
verbose_name=_("Quantity")
quantity = models.DecimalField(
decimal_places=2,
max_digits=7,
verbose_name=_("Quantity"),
validators=[MinValueValidator(0)],
)
amount = models.IntegerField(
verbose_name=_("Unit price")
verbose_name=_("Unit price"),
)
@property
......
{% load escape_tex %}
{% load escape_tex i18n %}
{% language fr %}
\documentclass[a4paper,11pt]{article}
\usepackage{fontspec}
......@@ -176,3 +177,4 @@ TVA non applicable, article 293 B du CGI.
\end{center}
\end{document}
{% endlanguage %}
# Generated by Django 2.2.19 on 2021-03-13 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='weiclub',
name='year',
field=models.PositiveIntegerField(default=2021, unique=True, verbose_name='year'),
),
migrations.AlterField(
model_name='weiregistration',
name='information_json',
field=models.TextField(default='{}', help_text='Information about the registration (buses for old members, survey for the new members), encoded in JSON', verbose_name='registration information'),
),
]
......@@ -114,7 +114,7 @@ Les filtres disponibles sont indiqués sur chacune des pages de documentation.
Le résultat est déjà par défaut filtré par droits : seuls les éléments que l'utilisateur à le droit de voir sont affichés.
Cela est possible grâce à la structure des permissions, générant justement des filtres de requêtes de base de données.
Une requête à l'adresse ``/api/<model>/pk/`` affiche directement les informations du modèle demandé au format JSON.
Une requête à l'adresse ``/api/<model>/<pk>/`` affiche directement les informations du modèle demandé au format JSON.
POST
~~~~
......
......@@ -74,7 +74,7 @@ ne dépende pas de cette application, on procède de cette manière.
Graphe
~~~~~~
.. image:: /_static/img/graphs/activity.svg
.. image:: ../_static/img/graphs/activity.svg
:alt: Graphe de l'application activités
UI
......
......@@ -46,7 +46,7 @@ Applications indispensables
Applications packagées
----------------------
* ``polymorphic``
Utiliser pour la création de models polymorphiques (``Note`` et ``Transaction`` notamment) cf [Note](Note).
Utiliser pour la création de models polymorphiques (``Note`` et ``Transaction`` notamment) cf `Note <note>`_.
L'utilisation des models polymorphiques est détaillé sur la documentation du package:
`<https://django-polymorphic.readthedocs.io/en/stable/>`_
......
......@@ -48,3 +48,10 @@ Exemple de Changelog, pour la création d'une transaction de 42424242 centimes d
S'il est préférable de passer en console Postgresql pour parcourir les logs, ils sont trouvables via l'API dans
``/api/logs``, sous réserve d'avoir les droits suffisants (ie. être respo info).
Graphe
~~~~~~
.. image:: ../_static/img/graphs/logs.svg
:alt: Logs graphe
......@@ -93,7 +93,7 @@ génère en effet automatiquement une transaction de l'utilisateur vers le club
Graphe
------
.. image:: /_static/img/graphs/member.svg
.. image:: ../_static/img/graphs/member.svg
:alt: Graphe de l'application member
Adhésions
......@@ -109,15 +109,15 @@ de fin d'adhésion.
On peut ajouter une adhésion à un utilisateur dans un club à tout non adhérent de ce club. La personne en charge
d'adhérer quelqu'un choisit l'utilisateur, les rôles au sein du club et la date de début d'adhésion. Cette date de
début d'adhésion doit se situer entre les champs ``club``.``membership_start`` et ``club``.``membership_end``,
si ces champs sont non nuls. Si ``club``.``parent_club`` n'est pas nul, l'utilisateur doit être membre de ce club.
Le montant de la cotisation est fixé en fonction du statut normalien de l'utilisateur (``club``.``membership_fee_paid``
centimes pour les élèves et ``club``.``membership_fee_unpaid`` centimes pour les étudiants). La date de fin est calculée
début d'adhésion doit se situer entre les champs ``club.membership_start`` et ``club.membership_end``,
si ces champs sont non nuls. Si ``club.parent_club`` n'est pas nul, l'utilisateur doit être membre de ce club.
Le montant de la cotisation est fixé en fonction du statut normalien de l'utilisateur (``club.membership_fee_paid``
centimes pour les élèves et ``club.membership_fee_unpaid`` centimes pour les étudiants). La date de fin est calculée
comme ce qui suit :
* Si ``club``.``membership_duration`` est non nul, alors ``date_end`` = ``date_start`` + ``club.membership_duration``
* Si ``club.membership_duration`` est non nul, alors ``date_end`` = ``date_start`` + ``club.membership_duration``
* Sinon ``club``, ``date_end`` = ``date_start`` + 424242 jours (suffisant pour tenir au moins une vie)
* Si ``club``.``membership_end`` est non nul, alors ``date_end`` = min(``date_end``, ``club``.``membership_end``)
* Si ``club.membership_end`` est non nul, alors ``date_end`` = min(``date_end``, ``club.membership_end``)
Si l'utilisateur n'est pas membre du club ``Kfet``, l'adhésion n'est pas possible si le solde disponible sur sa note est
insuffisant. Une fois toute ces contraintes vérifiées, l'adhésion est créée. Une transaction de type
......@@ -127,13 +127,14 @@ Réadhésions
~~~~~~~~~~~
Pour les clubs nécessitant des adhésions (de durée limitée), il est possible de réadhérer au bout d'un an. Dès lors
que le jour actuel est après ``club``.``membership_start`` + 1 an, ``club``.``membership_start`` et
``club``.membership_end`` sont incrémentés d'un an.
que le jour actuel est après ``club.membership_start`` + 1 an, ``club.membership_start`` et
``club.membership_end`` sont incrémentés d'un an.
Il est possible de réadhérer si :
* ``membership``.``date_start`` <= ``today`` <= ``membership``.``date_end`` (l'adhésion en cours est valide)
* ``membership``.``date_start`` < ``club``.``membership_start`` (si la date de début d'adhésion du club est postérieure à la date de début d'adhésion, qui a donc été mise à jour, on a changé d'année)
* Il n'y a pas encore de réadhésion (pas d'adhésion au même club vérifiant ``new_membership``.``date_start`` >= ``club``.``membership_start``)
* ``membership.date_start`` <= ``today`` <= ``membership.date_end`` (l'adhésion en cours est valide)
* ``membership.date_start`` < ``club.membership_start`` (si la date de début d'adhésion du club est postérieure à la date de début d'adhésion, qui a donc été mise à jour, on a changé d'année)
* Il n'y a pas encore de réadhésion (pas d'adhésion au même club vérifiant ``new_membership.date_start`` >= ``club.membership_start``)
Un bouton ``Réadhérer`` apparaît dans la liste des adhésions si le droit est permis et si ces contraintes sont vérifiées.
En réadhérant, une nouvelle adhésion est créée pour l'utilisateur avec les mêmes rôles, commençant le lendemain de la
......
......@@ -9,7 +9,8 @@ Elle est disponible à l'adresse ``/note/consos/``, et l'onglet n'est visible qu
moins un bouton. L'affichage, comme tout le reste de la page, est géré avec Boostrap 4.
Les boutons que l'utilisateur a le droit de voir sont triés par catégorie.
## Sélection des consommations
Sélection des consommations
---------------------------
Lorsque l'utilisateur commence à taper un nom de note, un appel à l'API sur la page ``/api/note/alias`` est fait,
récupérant les 20 premiers aliases en accord avec la requête. Quand l'utilisateur survole un alias, un appel à la page
......
......@@ -19,5 +19,6 @@ transferts/dons entre notes est détaillé sur la page `Transferts <transactions
Graphe
------
.. image:: /_static/img/graphs/note.svg
.. image:: ../../_static/img/graphs/note.svg
:width: 960
:alt: Graphe de l'application note
......@@ -19,8 +19,8 @@ Une permission est un Model Django dont les principaux attributs sont :
* ``model`` : Le model sur lequel cette permission va s'appliquer
* ``type`` : Les différents types d'interaction sont : voir (``view``), modifier (``change``), ajouter (``add``)
et supprimer (``delete``).
* ``query`` : Requete sur la cible, encodé en JSON, traduit en un Q object (cf [Query](#compilation-de-la-query)
* ``field`` : le champ cible qui pourra etre modifié. (tous les champs si vide)
* ``query`` : Requête sur la cible, encodé en JSON, traduit en un Q object (cf `Query <#compilation-de-la-query>`_)
* ``field`` : le champ cible qui pourra être modifié. (tous les champs si vide)
Pour savoir si un utilisateur a le droit sur un modèle ou non, la requête est compilée (voir ci-dessous) en un filtre
de requête dans la base de données, un objet de la classe ``Q`` (En SQL l'objet Q s'interprete comme tout ce qui suit
......@@ -147,5 +147,5 @@ modifiés en comparant l'ancienne et la nouvele instance.
Graphe des modèles
------------------
.. image:: /_static/img/graphs/permission.svg
.. image:: ../_static/img/graphs/permission.svg
:alt: Graphe de l'application permission
......@@ -217,5 +217,6 @@ Exemple de validation de crédit Société générale d'un étudiant non payé "
Diagramme des modèles
---------------------
.. image:: /_static/img/graphs/treasury.svg
:alt: Graphe de l'application trésorerie
\ No newline at end of file
.. image:: ../_static/img/graphs/treasury.svg
:width: 960
:alt: Graphe de l'application trésorerie
......@@ -113,7 +113,8 @@ Graphe des modèles
Pour une meilleure compréhension, le graphe des modèles de l'application ``member`` ont été ajoutés au schéma.
.. image:: /_static/img/graphs/wei.svg
.. image:: ../_static/img/graphs/wei.svg
:width: 960
:alt: Graphe des modèles de l'application WEI
Fonctionnement
......
......@@ -18,7 +18,7 @@
# -- Project information -----------------------------------------------------
project = 'Note Kfet 2020'
copyright = '2020, BDE ENS Paris-Saclay'
copyright = '2020-2021, BDE ENS Paris-Saclay'
author = 'BDE ENS Paris-Saclay'
# The full version, including alpha/beta/rc tags
......
Documentation
=============
La documentation est gérée grâce à Sphinx. Le thème est le thème officiel de
ReadTheDocs ``sphinx-rtd-theme``.
Générer localement la documentation
-----------------------------------
On commence par se rendre au bon endroit et installer les bonnes dépendances :
.. code:: bash
cd docs
pip install -r requirements.txt
La documentation se génère à partir d'appels à ``make``, selon le type de
documentation voulue.
Par exemple, ``make dirhtml`` construit la documentation web,
``make latexpdf`` construit un livre PDF avec cette documentation.
Documentation automatique
-------------------------
Ansible compile et déploie automatiquement la documentation du projet, dans
le rôle ``8-docs``. Le rôle installe dans le bon environnement les dépendances
nécessaires, puis appelle sphinx pour placer la documentation compilée dans
``/var/www/documentation`` :
.. code:: bash
/var/www/note_kfet/env/bin/sphinx-build -b dirhtml /var/www/note_kfet/docs/ /var/www/documentation/
Ce dossier est exposé par ``nginx`` sur le chemin
`/doc <https://note.crans.org/doc>`_.
Service d'Authentification Centralisé (CAS)
===========================================
Un `CAS <https://fr.wikipedia.org/wiki/Central_Authentication_Service>`_ est
déployé sur la Note Kfet. Il est accessible à l'adresse `<https://note.crans.org/cas/>`_.
Il a pour but uniquement d'authentifier les utilisateurs via la note et ne communique
que peu d'informations.
Configuration
-------------
Le serveur CAS utilisé est implémenté grâce au paquet ``django-cas-server``. Il peut être
installé soit par PIP soit sur une machine Debian via
``apt install python3-django-cas-server``.
On ajoute ensuite ``cas_server`` aux applications Django installées. On n'oublie pas ni
d'appliquer les migrations (``./manage.py migrate``) ni de collecter les fichiers
statiques (``./manage.py collectstatic``).
On enregistre les routes dans ``note_kfet/urls.py`` :
.. code:: python
urlpatterns.append(
path('cas/', include('cas_server.urls', namespace='cas_server'))
)
Le CAS est désormais déjà prêt à être utilisé. Toutefois, puisque l'on utilise un site
Django-admin personnalisé, on n'oublie pas d'enregistrer les pages d'administration :
.. code:: python
if "cas_server" in settings.INSTALLED_APPS:
from cas_server.admin import *
from cas_server.models import *
admin_site.register(ServicePattern, ServicePatternAdmin)
admin_site.register(FederatedIendityProvider, FederatedIendityProviderAdmin)
Enfin, on souhaite pouvoir fournir au besoin le pseudo normalisé. Pour cela, on crée une
classe dans ``member.auth`` :
.. code:: python
class CustomAuthUser(DjangoAuthUser):
def attributs(self):
d = super().attributs()
if self.user:
d["normalized_name"] = Alias.normalize(self.user.username)
return d
Puis on source ce fichier dans les paramètres :
.. code:: python
CAS_AUTH_CLASS = 'member.auth.CustomAuthUser'
Utilisation
-----------
Le service est accessible sur `<https://note.crans.org/cas/>`_. C'est ce lien qu'il faut
donner à votre application.
L'application doit néanmoins être autorisée à accéder au CAS. Pour cela, rendez-vous
dans Django-admin (`<https://note.crans.org/cas/admin/>`_), dans
``Service Central d'Authentification/Motifs de services``, ajoutez une nouvelle entrée.
Choisissez votre position favorite puis le nom de l'application.
Les champs importants sont les deux suivants :
* **Motif :** il s'agit d'une expression régulière qui doit reconnaitre le site voulu.
Par exemple, pour autoriser Belenios (`<https://belenios.crans.org>`_), on rentrera
le motif ``^https?://belenios\.crans\.org/.*$``.
* **Champ d'utilisateur :** C'est le pseudo que renverra le CAS. Par défaut, il s'agira
du nom de note principal, mais il arrive parfois que certains sites supportent mal
d'avoir des caractères UTF-8 dans le pseudo. C'est par exemple le cas de Belenios.
On rentrera alors ``normalized_name`` dans ce champ, qui correspond à la version
normalisée (sans accent ni espace ni aucun caractère non-ASCII) du pseudo, et qui
suffit à identifier une personne.
On peut également utiliser le ``Single log out`` si besoin.
Applications externes
=====================
.. toctree::
:maxdepth: 2
:caption: Applications externes