Commit ecc5ed0b authored by Maël Kervella's avatar Maël Kervella Committed by Maël Kervella

Docstrings, docstrings everywhere

parent 374dd8da
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
# #
# Copyright © 2018 Maël Kervella # Copyright © 2018 Maël Kervella
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
...@@ -19,34 +18,41 @@ ...@@ -19,34 +18,41 @@
# with this program; if not, write to the Free Software Foundation, Inc., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""api.acl """Defines the ACL for the whole API.
Here are defined some functions to check acl on the application. Importing this module, creates the 'can view api' permission if not already
done.
""" """
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.utils.translation import ugettext_lazy as _
def _create_api_permission():
"""Creates the 'use_api' permission if not created.
The 'use_api' is a fake permission in the sense it is not associated with an
existing model and this ensure the permission is created every time this file
is imported.
"""
api_content_type, created = ContentType.objects.get_or_create(
app_label=settings.API_CONTENT_TYPE_APP_LABEL,
model=settings.API_CONTENT_TYPE_MODEL
)
if created:
api_content_type.save()
api_permission, created = Permission.objects.get_or_create(
name=settings.API_PERMISSION_NAME,
content_type=api_content_type,
codename=settings.API_PERMISSION_CODENAME
)
if created:
api_permission.save()
# Creates the 'use_api' permission if not created _create_api_permission()
# The 'use_api' is a fake permission in the sense
# it is not associated with an existing model and
# this ensure the permission is created every tun
api_content_type, created = ContentType.objects.get_or_create(
app_label=settings.API_CONTENT_TYPE_APP_LABEL,
model=settings.API_CONTENT_TYPE_MODEL
)
if created:
api_content_type.save()
api_permission, created = Permission.objects.get_or_create(
name=settings.API_PERMISSION_NAME,
content_type=api_content_type,
codename=settings.API_PERMISSION_CODENAME
)
if created:
api_permission.save()
def can_view(user): def can_view(user):
...@@ -64,4 +70,4 @@ def can_view(user): ...@@ -64,4 +70,4 @@ def can_view(user):
'codename': settings.API_PERMISSION_CODENAME 'codename': settings.API_PERMISSION_CODENAME
} }
can = user.has_perm('%(app_label)s.%(codename)s' % kwargs) can = user.has_perm('%(app_label)s.%(codename)s' % kwargs)
return can, None if can else "Vous ne pouvez pas voir cette application." return can, None if can else _("You cannot see this application.")
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2018 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Defines the authentication classes used in the API to authenticate a user.
"""
import datetime import datetime
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.authentication import TokenAuthentication from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions from rest_framework import exceptions
class ExpiringTokenAuthentication(TokenAuthentication): class ExpiringTokenAuthentication(TokenAuthentication):
"""Authenticate a user if the provided token is valid and not expired.
"""
def authenticate_credentials(self, key): def authenticate_credentials(self, key):
model = self.get_model() """See base class. Add the verification the token is not expired.
try: """
token = model.objects.select_related('user').get(key=key) base = super(ExpiringTokenAuthentication, self)
except model.DoesNotExist: user, token = base.authenticate_credentials(key)
raise exceptions.AuthenticationFailed(_('Invalid token.'))
if not token.user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
# Check that the genration time of the token is not too old
token_duration = datetime.timedelta( token_duration = datetime.timedelta(
seconds=settings.API_TOKEN_DURATION seconds=settings.API_TOKEN_DURATION
) )
......
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2018 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Defines the pagination classes used in the API to paginate the results.
"""
from rest_framework import pagination from rest_framework import pagination
class PageSizedPagination(pagination.PageNumberPagination): class PageSizedPagination(pagination.PageNumberPagination):
""" """Provide the possibility to control the page size by using the
Pagination subclass to all to control the page size 'page_size' parameter. The value 'all' can be used for this parameter
to retrieve all the results in a single page.
Attributes:
page_size_query_param: The string to look for in the parameters of
a query to get the page_size requested.
all_pages_strings: A set of strings that can be used in the query to
request all results in a single page.
max_page_size: The maximum number of results a page can output no
matter what is requested.
""" """
page_size_query_param = 'page_size' page_size_query_param = 'page_size'
all_pages_strings = ('all',) all_pages_strings = ('all',)
max_page_size = 10000 max_page_size = 10000
def get_page_size(self, request): def get_page_size(self, request):
"""Retrieve the size of the page according to the parameters of the
request.
Args:
request: the request of the user
Returns:
A integer between 0 and `max_page_size` that represent the size
of the page to use.
"""
try: try:
page_size_str = request.query_params[self.page_size_query_param] page_size_str = request.query_params[self.page_size_query_param]
if page_size_str in self.all_pages_strings: if page_size_str in self.all_pages_strings:
......
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2018 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Defines the permission classes used in the API.
"""
from rest_framework import permissions, exceptions from rest_framework import permissions, exceptions
from re2o.acl import can_create, can_edit, can_delete, can_view_all from re2o.acl import can_create, can_edit, can_delete, can_view_all
from . import acl from . import acl
def can_see_api(*_, **__): def can_see_api(*_, **__):
"""Check if a user can view the API.
Returns:
A function that takes a user as an argument and returns
an ACL tuple that assert this user can see the API.
"""
return lambda user: acl.can_view(user) return lambda user: acl.can_view(user)
def _get_param_in_view(view, param_name): def _get_param_in_view(view, param_name):
"""Utility function to retrieve an attribute in a view passed in argument.
Uses the result of `{view}.get_{param_name}()` if existing else uses the
value of `{view}.{param_name}` directly.
Args:
view: The view where to look into.
param_name: The name of the attribute to look for.
Returns:
The result of the getter function if found else the value of the
attribute itself.
Raises:
AssertionError: None of the getter function or the attribute are
defined in the view.
"""
assert hasattr(view, 'get_'+param_name) \ assert hasattr(view, 'get_'+param_name) \
or getattr(view, param_name, None) is not None, ( or getattr(view, param_name, None) is not None, (
'cannot apply {} on a view that does not set ' 'cannot apply {} on a view that does not set '
...@@ -24,15 +72,30 @@ def _get_param_in_view(view, param_name): ...@@ -24,15 +72,30 @@ def _get_param_in_view(view, param_name):
class ACLPermission(permissions.BasePermission): class ACLPermission(permissions.BasePermission):
""" """A permission class used to check the ACL to validate the permissions
Permission subclass for views that requires a specific model-based of a user.
permission or don't define a queryset
The view must define a `.get_perms_map()` or a `.perms_map` attribute.
See the wiki for the syntax of this attribute.
""" """
def get_required_permissions(self, method, view): def get_required_permissions(self, method, view):
""" """Build the list of permissions required for the request to be
Given a list of models and an HTTP method, return the list accepted.
of acl functions that the user is required to verify.
Args:
method: The HTTP method name used for the request.
view: The view which is responding to the request.
Returns:
The list of ACL functions to apply to a user in order to check
if he has the right permissions.
Raises:
AssertionError: None of `.get_perms_map()` or `.perms_map` are
defined in the view.
rest_framework.exception.MethodNotAllowed: The requested method
is not allowed for this view.
""" """
perms_map = _get_param_in_view(view, 'perms_map') perms_map = _get_param_in_view(view, 'perms_map')
...@@ -42,6 +105,22 @@ class ACLPermission(permissions.BasePermission): ...@@ -42,6 +105,22 @@ class ACLPermission(permissions.BasePermission):
return [can_see_api()] + list(perms_map[method]) return [can_see_api()] + list(perms_map[method])
def has_permission(self, request, view): def has_permission(self, request, view):
"""Check that the user has the permissions to perform the request.
Args:
request: The request performed.
view: The view which is responding to the request.
Returns:
A boolean indicating if the user has the permission to
perform the request.
Raises:
AssertionError: None of `.get_perms_map()` or `.perms_map` are
defined in the view.
rest_framework.exception.MethodNotAllowed: The requested method
is not allowed for this view.
"""
# Workaround to ensure ACLPermissions are not applied # Workaround to ensure ACLPermissions are not applied
# to the root view when using DefaultRouter. # to the root view when using DefaultRouter.
if getattr(view, '_ignore_model_permissions', False): if getattr(view, '_ignore_model_permissions', False):
...@@ -54,19 +133,20 @@ class ACLPermission(permissions.BasePermission): ...@@ -54,19 +133,20 @@ class ACLPermission(permissions.BasePermission):
return all(perm(request.user)[0] for perm in perms) return all(perm(request.user)[0] for perm in perms)
def has_object_permission(self, request, view, obj):
# Should never be called here but documentation
# requires to implement this function
return False
class AutodetectACLPermission(permissions.BasePermission): class AutodetectACLPermission(permissions.BasePermission):
"""A permission class used to autodetect the ACL needed to validate the
permissions of a user based on the queryset of the view.
The view must define a `.get_queryset()` or a `.queryset` attribute.
Attributes:
perms_map: The mapping of each valid HTTP method to the required
model-based ACL permissions.
perms_obj_map: The mapping of each valid HTTP method to the required
object-based ACL permissions.
""" """
Permission subclass in charge of checking the ACL to determine
if a user can access the models. Autodetect which ACL are required
based on a queryset. Requires `.queryset` or `.get_queryset()`
to be defined in the view.
"""
perms_map = { perms_map = {
'GET': [can_see_api, lambda model: model.can_view_all], 'GET': [can_see_api, lambda model: model.can_view_all],
'OPTIONS': [can_see_api, lambda model: model.can_view_all], 'OPTIONS': [can_see_api, lambda model: model.can_view_all],
...@@ -87,9 +167,20 @@ class AutodetectACLPermission(permissions.BasePermission): ...@@ -87,9 +167,20 @@ class AutodetectACLPermission(permissions.BasePermission):
} }
def get_required_permissions(self, method, model): def get_required_permissions(self, method, model):
""" """Build the list of model-based permissions required for the
Given a model and an HTTP method, return the list of acl request to be accepted.
functions that the user is required to verify.
Args:
method: The HTTP method name used for the request.
view: The view which is responding to the request.
Returns:
The list of ACL functions to apply to a user in order to check
if he has the right permissions.
Raises:
rest_framework.exception.MethodNotAllowed: The requested method
is not allowed for this view.
""" """
if method not in self.perms_map: if method not in self.perms_map:
raise exceptions.MethodNotAllowed(method) raise exceptions.MethodNotAllowed(method)
...@@ -97,9 +188,20 @@ class AutodetectACLPermission(permissions.BasePermission): ...@@ -97,9 +188,20 @@ class AutodetectACLPermission(permissions.BasePermission):
return [perm(model) for perm in self.perms_map[method]] return [perm(model) for perm in self.perms_map[method]]
def get_required_object_permissions(self, method, obj): def get_required_object_permissions(self, method, obj):
""" """Build the list of object-based permissions required for the
Given an object and an HTTP method, return the list of acl request to be accepted.
functions that the user is required to verify.
Args:
method: The HTTP method name used for the request.
view: The view which is responding to the request.
Returns:
The list of ACL functions to apply to a user in order to check
if he has the right permissions.
Raises:
rest_framework.exception.MethodNotAllowed: The requested method
is not allowed for this view.
""" """
if method not in self.perms_obj_map: if method not in self.perms_obj_map:
raise exceptions.MethodNotAllowed(method) raise exceptions.MethodNotAllowed(method)
...@@ -107,13 +209,26 @@ class AutodetectACLPermission(permissions.BasePermission): ...@@ -107,13 +209,26 @@ class AutodetectACLPermission(permissions.BasePermission):
return [perm(obj) for perm in self.perms_obj_map[method]] return [perm(obj) for perm in self.perms_obj_map[method]]
def _queryset(self, view): def _queryset(self, view):
"""
Return the queryset associated with view and raise an error
is there is none.
"""
return _get_param_in_view(view, 'queryset') return _get_param_in_view(view, 'queryset')
def has_permission(self, request, view): def has_permission(self, request, view):
"""Check that the user has the model-based permissions to perform
the request.
Args:
request: The request performed.
view: The view which is responding to the request.
Returns:
A boolean indicating if the user has the permission to
perform the request.
Raises:
AssertionError: None of `.get_queryset()` or `.queryset` are
defined in the view.
rest_framework.exception.MethodNotAllowed: The requested method
is not allowed for this view.
"""
# Workaround to ensure ACLPermissions are not applied # Workaround to ensure ACLPermissions are not applied
# to the root view when using DefaultRouter. # to the root view when using DefaultRouter.
if getattr(view, '_ignore_model_permissions', False): if getattr(view, '_ignore_model_permissions', False):
...@@ -128,8 +243,22 @@ class AutodetectACLPermission(permissions.BasePermission): ...@@ -128,8 +243,22 @@ class AutodetectACLPermission(permissions.BasePermission):
return all(perm(request.user)[0] for perm in perms) return all(perm(request.user)[0] for perm in perms)
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
"""Check that the user has the object-based permissions to perform
the request.
Args:
request: The request performed.
view: The view which is responding to the request.
Returns:
A boolean indicating if the user has the permission to
perform the request.
Raises:
rest_framework.exception.MethodNotAllowed: The requested method
is not allowed for this view.
"""
# authentication checks have already executed via has_permission # authentication checks have already executed via has_permission
queryset = self._queryset(view)
user = request.user user = request.user
perms = self.get_required_object_permissions(request.method, obj) perms = self.get_required_object_permissions(request.method, obj)
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# se veut agnostique au réseau considéré, de manière à être installable en # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
# #
# Copyright © 2018 Mael Kervella # Copyright © 2018 Mael Kervella
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
...@@ -17,12 +17,12 @@ ...@@ -17,12 +17,12 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""api.routers
Definition of the custom routers to generate the URLs of the API """Defines the custom routers to generate the URLs of the API.
""" """
from collections import OrderedDict from collections import OrderedDict
from django.conf.urls import url, include from django.conf.urls import url, include
from django.core.urlresolvers import NoReverseMatch from django.core.urlresolvers import NoReverseMatch
from rest_framework import views from rest_framework import views
...@@ -32,32 +32,60 @@ from rest_framework.reverse import reverse ...@@ -32,32 +32,60 @@ from rest_framework.reverse import reverse
from rest_framework.schemas import SchemaGenerator from rest_framework.schemas import SchemaGenerator
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
class AllViewsRouter(DefaultRouter): class AllViewsRouter(DefaultRouter):
"""A router that can register both viewsets and views and generates
a full API root page with all the generated URLs.
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.view_registry = [] self.view_registry = []
super(AllViewsRouter, self).__init__(*args, **kwargs) super(AllViewsRouter, self).__init__(*args, **kwargs)
def register_viewset(self, *args, **kwargs): def register_viewset(self, *args, **kwargs):
""" """Register a viewset in the router. Alias of `register` for
Register a viewset in the router convenience.
Alias of `register` for convenience
See `register` in the base class for details.
""" """
return self.register(*args, **kwargs) return self.register(*args, **kwargs)
def register_view(self, pattern, view, name=None): def register_view(self, pattern, view, name=None):
""" """Register a view in the router.
Register a view in the router
Args:
pattern: The URL pattern to use for this view.
view: The class-based view to register.
name: An optional name for the route generated. Defaults is
based on the pattern last section (delimited by '/').
""" """
if name is None: if name is None:
name = self.get_default_name(pattern) name = self.get_default_name(pattern)
self.view_registry.append((pattern, view, name)) self.view_registry.append((pattern, view, name))
def get_default_name(self, pattern): def get_default_name(self, pattern):
"""Returns the name to use for the route if none was specified.
Args:
pattern: The pattern for this route.
Returns:
The name to use for this route.
"""
return pattern.split('/')[-1] return pattern.split('/')[-1]
def get_api_root_view(self, schema_urls=None): def get_api_root_view(self, schema_urls=None):
""" """Create a class-based view to use as the API root.
Return a view to use as the API root.
Highly inspired by the base class. See details on the implementation
in the base class. The only difference is that registered view URLs
are added after the registered viewset URLs on this root API page.
Args:
schema_urls: A schema to use for the URLs.
Returns:
The view to use to display the root API page.
""" """
api_root_dict = OrderedDict() api_root_dict = OrderedDict()
list_name = self.routes[0].name list_name = self.routes[0].name
...@@ -115,6 +143,12 @@ class AllViewsRouter(DefaultRouter): ...@@ -115,6 +143,12 @@ class AllViewsRouter(DefaultRouter):
return APIRoot.as_view() return APIRoot.as_view()
def get_urls(self): def get_urls(self):
"""Builds the list of URLs to register.
Returns:
A list of the URLs generated based on the viewsets registered
followed by the URLs generated based on the views registered.
"""
urls = super(AllViewsRouter, self).get_urls() urls = super(AllViewsRouter, self).get_urls()
for pattern, view, name in self.view_registry: for pattern, view, name in self.view_registry:
......
This diff is collapsed.
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
# #
# Copyright © 2017 Gabriel Détraz # Copyright © 2018 Maël Kervella
# Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
...@@ -21,8 +18,7 @@ ...@@ -21,8 +18,7 @@
# with this program; if not, write to the Free Software Foundation, Inc., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""api.settings """Settings specific to the API.
Django settings specific to the API.
""" """
# RestFramework config for API # RestFramework config for API
...@@ -49,4 +45,6 @@ API_PERMISSION_CODENAME = 'use_api' ...@@ -49,4 +45,6 @@ API_PERMISSION_CODENAME = 'use_api'
API_APPS = ( API_APPS = (
'rest_framework.authtoken', 'rest_framework.authtoken',
) )
# The expiration time for an authentication token
API_TOKEN_DURATION = 86400 # 24 hours API_TOKEN_DURATION = 86400 # 24 hours
This diff is collapsed.
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# se veut agnostique au réseau considéré, de manière à être installable en # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
# #
# Copyright © 2018 Mael Kervella # Copyright © 2018 Maël Kervella
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
...@@ -17,27 +17,30 @@ ...@@ -17,27 +17,30 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc., # with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""api.urls
Urls de l'api, pointant vers les fonctions de views """Defines the URLs of the API
"""
from __future__ import unicode_literals A custom router is used to register all the routes. That allows to register
all the URL patterns from the viewsets as a standard router but, the views
can also be register. That way a complete API root page presenting all URLs
can be generated automatically.
"""
from django.conf.urls import url, include from django.conf.urls import url, include
from .routers import AllViewsRouter
from . import views from . import views
from .routers import AllViewsRouter
router = AllViewsRouter() router = AllViewsRouter()
# COTISATIONS APP # COTISATIONS
router.register_viewset(r'cotisations/factures', views.FactureViewSet) router.register_viewset(r'cotisations/factures', views.FactureViewSet)
router.register_viewset(r'cotisations/ventes', views.VenteViewSet) router.register_viewset(r'cotisations/ventes', views.VenteViewSet)
router.register_viewset(r'cotisations/articles', views.ArticleViewSet) router.register_viewset(r'cotisations/articles', views.ArticleViewSet)
router.register_viewset(r'cotisations/banques', views.BanqueViewSet) router.register_viewset(r'cotisations/banques', views.BanqueViewSet)
router.register_viewset(r'cotisations/paiements', views.PaiementViewSet) router.register_viewset(r'cotisations/paiements', views.PaiementViewSet)
router.register_viewset(r'cotisations/cotisations', views.Cotis