Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Re2o
re2o
Commits
ecc5ed0b
Commit
ecc5ed0b
authored
Jun 17, 2018
by
moamoak
Committed by
moamoak
Jun 20, 2018
Browse files
Docstrings, docstrings everywhere
parent
374dd8da
Changes
10
Expand all
Hide whitespace changes
Inline
Side-by-side
api/acl.py
View file @
ecc5ed0b
# -*- mode: python; coding: utf-8 -*-
# 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
# 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
...
...
@@ -19,34 +18,41 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 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.contrib.contenttypes.models
import
ContentType
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
# 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
()
_create_api_permission
()
def
can_view
(
user
):
...
...
@@ -64,4 +70,4 @@ def can_view(user):
'codename'
:
settings
.
API_PERMISSION_CODENAME
}
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."
)
api/authentication.py
View file @
ecc5ed0b
# 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
from
django.conf
import
settings
from
django.utils.translation
import
ugettext_lazy
as
_
from
rest_framework.authentication
import
TokenAuthentication
from
rest_framework
import
exceptions
class
ExpiringTokenAuthentication
(
TokenAuthentication
):
"""Authenticate a user if the provided token is valid and not expired.
"""
def
authenticate_credentials
(
self
,
key
):
model
=
self
.
get_model
()
try
:
token
=
model
.
objects
.
select_related
(
'user'
).
get
(
key
=
key
)
except
model
.
DoesNotExist
:
raise
exceptions
.
AuthenticationFailed
(
_
(
'Invalid token.'
))
if
not
token
.
user
.
is_active
:
raise
exceptions
.
AuthenticationFailed
(
_
(
'User inactive or deleted.'
))
"""See base class. Add the verification the token is not expired.
"""
base
=
super
(
ExpiringTokenAuthentication
,
self
)
user
,
token
=
base
.
authenticate_credentials
(
key
)
# Check that the genration time of the token is not too old
token_duration
=
datetime
.
timedelta
(
seconds
=
settings
.
API_TOKEN_DURATION
)
...
...
api/pagination.py
View file @
ecc5ed0b
# 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
class
PageSizedPagination
(
pagination
.
PageNumberPagination
):
"""
Pagination subclass to all to control the page size
"""Provide the possibility to control the page size by using the
'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'
all_pages_strings
=
(
'all'
,)
max_page_size
=
10000
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
:
page_size_str
=
request
.
query_params
[
self
.
page_size_query_param
]
if
page_size_str
in
self
.
all_pages_strings
:
...
...
api/permissions.py
View file @
ecc5ed0b
# 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
re2o.acl
import
can_create
,
can_edit
,
can_delete
,
can_view_all
from
.
import
acl
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
)
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
)
\
or
getattr
(
view
,
param_name
,
None
)
is
not
None
,
(
'cannot apply {} on a view that does not set '
...
...
@@ -24,15 +72,30 @@ def _get_param_in_view(view, param_name):
class
ACLPermission
(
permissions
.
BasePermission
):
"""
Permission subclass for views that requires a specific model-based
permission or don't define a queryset
"""A permission class used to check the ACL to validate the permissions
of a user.
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
):
"""
Given a list of models and an HTTP method, return the list
of acl functions that the user is required to verify.
"""Build the list of permissions required for the request to be
accepted.
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'
)
...
...
@@ -42,6 +105,22 @@ class ACLPermission(permissions.BasePermission):
return
[
can_see_api
()]
+
list
(
perms_map
[
method
])
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
# to the root view when using DefaultRouter.
if
getattr
(
view
,
'_ignore_model_permissions'
,
False
):
...
...
@@ -54,19 +133,20 @@ class ACLPermission(permissions.BasePermission):
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
):
"""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
=
{
'GET'
:
[
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):
}
def
get_required_permissions
(
self
,
method
,
model
):
"""
Given a model and an HTTP method, return the list of acl
functions that the user is required to verify.
"""Build the list of model-based permissions required for the
request to be accepted.
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
:
raise
exceptions
.
MethodNotAllowed
(
method
)
...
...
@@ -97,9 +188,20 @@ class AutodetectACLPermission(permissions.BasePermission):
return
[
perm
(
model
)
for
perm
in
self
.
perms_map
[
method
]]
def
get_required_object_permissions
(
self
,
method
,
obj
):
"""
Given an object and an HTTP method, return the list of acl
functions that the user is required to verify.
"""Build the list of object-based permissions required for the
request to be accepted.
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
:
raise
exceptions
.
MethodNotAllowed
(
method
)
...
...
@@ -107,13 +209,26 @@ class AutodetectACLPermission(permissions.BasePermission):
return
[
perm
(
obj
)
for
perm
in
self
.
perms_obj_map
[
method
]]
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'
)
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
# to the root view when using DefaultRouter.
if
getattr
(
view
,
'_ignore_model_permissions'
,
False
):
...
...
@@ -128,8 +243,22 @@ class AutodetectACLPermission(permissions.BasePermission):
return
all
(
perm
(
request
.
user
)[
0
]
for
perm
in
perms
)
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
queryset
=
self
.
_queryset
(
view
)
user
=
request
.
user
perms
=
self
.
get_required_object_permissions
(
request
.
method
,
obj
)
...
...
api/routers.py
View file @
ecc5ed0b
...
...
@@ -2,7 +2,7 @@
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2018
Mael Kervella
# Copyright © 2018 Mael 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
...
...
@@ -17,12 +17,12 @@
# 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.
"""api.routers
Defin
ition of
the custom routers to generate the URLs of the API
"""
Defin
es
the custom routers to generate the URLs of the API
.
"""
from
collections
import
OrderedDict
from
django.conf.urls
import
url
,
include
from
django.core.urlresolvers
import
NoReverseMatch
from
rest_framework
import
views
...
...
@@ -32,32 +32,60 @@ from rest_framework.reverse import reverse
from
rest_framework.schemas
import
SchemaGenerator
from
rest_framework.settings
import
api_settings
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
):
self
.
view_registry
=
[]
super
(
AllViewsRouter
,
self
).
__init__
(
*
args
,
**
kwargs
)
def
register_viewset
(
self
,
*
args
,
**
kwargs
):
"""
Register a viewset in the router
Alias of `register` for convenience
"""Register a viewset in the router. Alias of `register` for
convenience.
See `register` in the base class for details.
"""
return
self
.
register
(
*
args
,
**
kwargs
)
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
:
name
=
self
.
get_default_name
(
pattern
)
self
.
view_registry
.
append
((
pattern
,
view
,
name
))
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
]
def
get_api_root_view
(
self
,
schema_urls
=
None
):
"""
Return a view to use as the API root.
"""Create a class-based 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
()
list_name
=
self
.
routes
[
0
].
name
...
...
@@ -115,6 +143,12 @@ class AllViewsRouter(DefaultRouter):
return
APIRoot
.
as_view
()
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
()
for
pattern
,
view
,
name
in
self
.
view_registry
:
...
...
api/serializers.py
View file @
ecc5ed0b
This diff is collapsed.
Click to expand it.
api/settings.py
View file @
ecc5ed0b
# -*- mode: python; coding: utf-8 -*-
# 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 © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle
# 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
...
...
@@ -21,8 +18,7 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""api.settings
Django settings specific to the API.
"""Settings specific to the API.
"""
# RestFramework config for API
...
...
@@ -49,4 +45,6 @@ API_PERMISSION_CODENAME = 'use_api'
API_APPS
=
(
'rest_framework.authtoken'
,
)
# The expiration time for an authentication token
API_TOKEN_DURATION
=
86400
# 24 hours
api/tests.py
View file @
ecc5ed0b
...
...
@@ -2,9 +2,7 @@
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle
# 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
...
...
@@ -19,8 +17,7 @@
# 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.
"""api.tests
The tests for the API module.
"""Defines the test suite for the API
"""
import
json
...
...
@@ -34,13 +31,25 @@ import users.models as users
class
APIEndpointsTestCase
(
APITestCase
):
# URLs that don't require to be authenticated
"""Test case to test that all endpoints are reachable with respects to
authentication and permission checks.
Attributes:
no_auth_endpoints: A list of endpoints that should be reachable
without authentication.
auth_no_perm_endpoints: A list of endpoints that should be reachable
when being authenticated but without permissions.
auth_perm_endpoints: A list of endpoints that should be reachable
when being authenticated and having the correct permissions.
stduser: A standard user with no permission used for the tests and
initialized at the beggining of this test case.
superuser: A superuser (with all permissions) used for the tests and
initialized at the beggining of this test case.
"""
no_auth_endpoints
=
[
'/api/'
]
# URLs that require to be authenticated and have no special permissions
auth_no_perm_endpoints
=
[]
# URLs that require to be authenticated and have special permissions
auth_perm_endpoints
=
[
'/api/cotisations/articles/'
,
# '/api/cotisations/articles/<pk>/',
...
...
@@ -160,49 +169,62 @@ class APIEndpointsTestCase(APITestCase):
cls
.
superuser
.
delete
()
super
().
tearDownClass
()
def
check_responses_code
(
self
,
urls
,
expected_code
,
formats
=
[
None
]
,
def
check_responses_code
(
self
,
urls
,
expected_code
,
formats
=
None
,
assert_more
=
None
):
"""Utility function to test if a list of urls answer an expected code.
Args:
urls: The list of urls to test
expected_code: The HTTP return code expected
formats: The list of formats to use for the request. Default is to
only test `None` format.
assert_more: An optional function to assert more specific data in
the same test. The response object, the url and the format
used are passed as arguments.