From 14d53b45e3cca951e060d8ae5fb0271ca84e08fc Mon Sep 17 00:00:00 2001
From: Kevin Alberts
Date: Mon, 12 May 2025 18:33:19 +0200
Subject: [PATCH 1/2] Update packages to Django 4.x versions, rewrite API to
use ModernRPC because django-json-rpc is broken, fix other small bugs
---
alexia/api/decorators.py | 17 +++++-
alexia/api/exceptions.py | 32 ++++++----
alexia/api/handlers.py | 53 ++++++++++++++++
alexia/api/urls.py | 13 ++--
alexia/api/v1/__init__.py | 2 -
alexia/api/v1/config.py | 4 --
alexia/api/v1/methods/authorization.py | 30 ++++-----
alexia/api/v1/methods/billing.py | 31 ++++++----
alexia/api/v1/methods/event.py | 12 ++--
alexia/api/v1/methods/generic.py | 36 +++++------
alexia/api/v1/methods/juliana.py | 41 ++++++++-----
alexia/api/v1/methods/organization.py | 26 +++++---
alexia/api/v1/methods/rfid.py | 28 +++++----
alexia/api/v1/methods/scheduling.py | 16 ++---
alexia/api/v1/methods/user.py | 50 +++++++--------
alexia/api/v1/test/test_authorization.py | 5 +-
alexia/api/v1/test/test_billing.py | 5 +-
alexia/api/v1/test/test_juliana.py | 30 ++++-----
alexia/api/v1/views.py | 18 ------
alexia/api/views.py | 72 ----------------------
alexia/apps/billing/forms.py | 2 +-
alexia/apps/billing/models.py | 9 ++-
alexia/apps/billing/urls.py | 78 ++++++++++++------------
alexia/apps/billing/views.py | 10 +--
alexia/apps/config.py | 2 +-
alexia/apps/consumption/forms.py | 2 +-
alexia/apps/consumption/models.py | 2 +-
alexia/apps/consumption/urls.py | 20 +++---
alexia/apps/consumption/views.py | 2 +-
alexia/apps/general/views.py | 6 +-
alexia/apps/organization/admin.py | 4 +-
alexia/apps/organization/forms.py | 2 +-
alexia/apps/organization/models.py | 2 +-
alexia/apps/organization/urls.py | 24 ++++----
alexia/apps/profile/urls.py | 16 ++---
alexia/apps/scheduling/admin.py | 2 +-
alexia/apps/scheduling/forms.py | 6 +-
alexia/apps/scheduling/models.py | 2 +-
alexia/apps/scheduling/urls.py | 41 ++++++-------
alexia/apps/scheduling/views.py | 13 ++--
alexia/auth/mixins.py | 10 +--
alexia/conf/settings/base.py | 12 +++-
alexia/conf/settings/environ.py | 2 +
alexia/conf/urls.py | 58 +++++++++---------
alexia/core/validators.py | 2 +-
alexia/forms/mixins.py | 2 +-
alexia/test/testcases.py | 13 ++--
alexia/utils/request.py | 7 +++
requirements.txt | 19 +++---
49 files changed, 448 insertions(+), 443 deletions(-)
create mode 100644 alexia/api/handlers.py
delete mode 100644 alexia/api/v1/config.py
delete mode 100644 alexia/api/v1/views.py
create mode 100644 alexia/utils/request.py
diff --git a/alexia/api/decorators.py b/alexia/api/decorators.py
index 399b811..9a24fd4 100644
--- a/alexia/api/decorators.py
+++ b/alexia/api/decorators.py
@@ -1,14 +1,25 @@
from functools import wraps
from django.core.exceptions import PermissionDenied
+from modernrpc.core import REQUEST_KEY
def manager_required(f):
@wraps(f)
- def wrap(request, *args, **kwargs):
+ def wrap(*args, **kwargs):
+ request = kwargs.get(REQUEST_KEY)
if not request.user.is_authenticated or not request.user.is_superuser and (
- not request.organization or not request.user.profile.is_manager(request.organization)):
+ not request.organization or not request.user.profile.is_manager(request.organization)
+ ):
raise PermissionDenied
- return f(request, *args, **kwargs)
+ return f(*args, **kwargs)
+ return wrap
+def login_required(f):
+ @wraps(f)
+ def wrap(*args, **kwargs):
+ request = kwargs.get(REQUEST_KEY)
+ if not request.user.is_authenticated:
+ raise PermissionDenied
+ return f(*args, **kwargs)
return wrap
diff --git a/alexia/api/exceptions.py b/alexia/api/exceptions.py
index dfb632f..b20fff0 100644
--- a/alexia/api/exceptions.py
+++ b/alexia/api/exceptions.py
@@ -1,22 +1,28 @@
-from jsonrpc.exceptions import Error
+from modernrpc.exceptions import RPCException, RPC_INVALID_PARAMS, RPC_CUSTOM_ERROR_BASE
-class ForbiddenError(Error):
+class ForbiddenError(RPCException):
""" The token was not recognized. """
- code = 403
- status = 403
- message = 'Forbidden.'
+ def __init__(self, message=None):
+ super(ForbiddenError, self).__init__(
+ code=(RPC_CUSTOM_ERROR_BASE + 3),
+ message='Forbidden.' if message is None else message
+ )
-class ObjectNotFoundError(Error):
+class ObjectNotFoundError(RPCException):
""" The requested object does not exist. """
- code = 404
- message = 'Object not found.'
- status = 404
+ def __init__(self, message=None):
+ super(ObjectNotFoundError, self).__init__(
+ code=404,
+ message='Object not found.' if message is None else message
+ )
-class InvalidParamsError(Error):
+class InvalidParamsError(RPCException):
""" Invalid method parameters. """
- code = -32602
- message = 'Invalid params.'
- status = 422
+ def __init__(self, message=None):
+ super(InvalidParamsError, self).__init__(
+ code=RPC_INVALID_PARAMS,
+ message='Invalid params.' if message is None else message
+ )
diff --git a/alexia/api/handlers.py b/alexia/api/handlers.py
new file mode 100644
index 0000000..3094d62
--- /dev/null
+++ b/alexia/api/handlers.py
@@ -0,0 +1,53 @@
+import json
+
+from modernrpc.handlers import JSONRPCHandler
+from modernrpc.handlers.jsonhandler import JsonResult, JsonSuccessResult, JsonErrorResult
+
+from alexia.api.v1 import version
+
+
+class AlexiaJSONRPCHandler(JSONRPCHandler):
+
+ # Override content types because older apps send an initialization request with the content type
+ # text/plain, so we need to add that to the allowed content types for JSON-RPC. -- albertskja 2025-05-12
+ @staticmethod
+ def valid_content_types():
+ return [
+ "text/plain",
+ "application/json",
+ "application/json-rpc",
+ "application/jsonrequest",
+ ]
+
+ def process_single_request(self, request_data, context):
+ # Older apps send the jsonrpc version as a float, but the library expects it as a string.
+ # So we need to overwrite that attribute to the correct type if it is a float. -- albertskja 2025-05-12
+ if request_data and isinstance(request_data, dict) and "jsonrpc" in request_data and isinstance(request_data['jsonrpc'], float):
+ request_data['jsonrpc'] = str(request_data['jsonrpc'])
+
+ # Older apps may set the jsonrpc attribute to "1.0", but the library expects "2.0".
+ # Because there is no functional difference otherwise, and to maintain backwards compatibility
+ # we can just override it to "2.0" in those cases. -- albertskja 2025-05-12
+ version_overridden = False
+ if request_data and isinstance(request_data, dict) and "jsonrpc" in request_data and request_data['jsonrpc'] == "1.0":
+ request_data['jsonrpc'] = "2.0"
+ version_overridden = True
+
+ result_data = super().process_single_request(request_data=request_data, context=context)
+
+ # Put back the version "1.0" if it was previously overridden -- albertskja 2025-05-12
+ if version_overridden:
+ result_data.version = "1.0"
+
+ return result_data
+
+ def dumps_result(self, result: JsonResult) -> str:
+ # The old API backend included the "error" attribute set to None, even if the request was successful.
+ # Also, it included the "result" attribute set to None, even if the request failed.
+ # ModernRPC does not, so we need to add those back to maintain backwards compatibility -- albertskja 2025-05-12
+ result_json = json.loads(super().dumps_result(result))
+ if isinstance(result, JsonSuccessResult) and "error" not in result_json.keys():
+ result_json["error"] = None
+ if isinstance(result, JsonErrorResult) and "result" not in result_json.keys():
+ result_json["result"] = None
+ return json.dumps(result_json)
diff --git a/alexia/api/urls.py b/alexia/api/urls.py
index a2814b1..3994c32 100644
--- a/alexia/api/urls.py
+++ b/alexia/api/urls.py
@@ -1,12 +1,13 @@
-from django.conf.urls import url
+from django.urls import path
-from .v1 import APIv1BrowserView, APIv1DocumentationView, api_v1_site
from .views import APIInfoView
+from modernrpc.core import Protocol
+from modernrpc.views import RPCEntryPoint
+
urlpatterns = [
- url(r'^$', APIInfoView.as_view(), name='api'),
+ path('', APIInfoView.as_view(), name='api'),
- url(r'^1/$', api_v1_site.dispatch, name='api_v1_mountpoint'),
- url(r'^1/browse/$', APIv1BrowserView.as_view(), name='api_v1_browse'),
- url(r'^1/doc/$', APIv1DocumentationView.as_view(), name='api_v1_doc'),
+ path('1/', RPCEntryPoint.as_view(protocol=Protocol.JSON_RPC, entry_point="v1"), name="jsonrpc_mountpoint"),
+ path('1/doc/', RPCEntryPoint.as_view(enable_doc=True, enable_rpc=False, template_name="api/v1/doc.html", entry_point="v1")),
]
diff --git a/alexia/api/v1/__init__.py b/alexia/api/v1/__init__.py
index d92502c..0761daa 100644
--- a/alexia/api/v1/__init__.py
+++ b/alexia/api/v1/__init__.py
@@ -1,3 +1 @@
-from .config import * # NOQA
from .methods import * # NOQA
-from .views import * # NOQA
diff --git a/alexia/api/v1/config.py b/alexia/api/v1/config.py
deleted file mode 100644
index 66faa79..0000000
--- a/alexia/api/v1/config.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from jsonrpc.site import JSONRPCSite
-
-api_v1_site = JSONRPCSite()
-api_v1_site.name = 'Alexia API v1'
diff --git a/alexia/api/v1/methods/authorization.py b/alexia/api/v1/methods/authorization.py
index c0baa05..28b6027 100644
--- a/alexia/api/v1/methods/authorization.py
+++ b/alexia/api/v1/methods/authorization.py
@@ -1,7 +1,9 @@
+from typing import List, Dict, Optional
+
from django.contrib.auth.models import User
from django.db import transaction
from django.utils import timezone
-from jsonrpc import jsonrpc_method
+from modernrpc.core import rpc_method, REQUEST_KEY
from alexia.api.decorators import manager_required
from alexia.api.exceptions import InvalidParamsError
@@ -9,12 +11,11 @@
from alexia.auth.backends import OIDC_BACKEND_NAME
from ..common import format_authorization
-from ..config import api_v1_site
-@jsonrpc_method('authorization.list(radius_username=String) -> Array', site=api_v1_site, authenticated=True, safe=True)
+@rpc_method(name='authorization.list', entry_point='v1')
@manager_required
-def authorization_list(request, radius_username=None):
+def authorization_list(radius_username: Optional[str] = None, **kwargs) -> List[Dict]:
"""
Retrieve registered authorizations for the current selected organization.
@@ -38,6 +39,7 @@ def authorization_list(request, radius_username=None):
Raises error -32602 (Invalid params) if the username does not exist.
"""
+ request = kwargs.get(REQUEST_KEY)
result = []
authorizations = Authorization.objects.filter(organization=request.organization)
@@ -58,9 +60,9 @@ def authorization_list(request, radius_username=None):
return result
-@jsonrpc_method('authorization.get(radius_username=String) -> Array', site=api_v1_site, authenticated=True, safe=True)
+@rpc_method(name='authorization.get', entry_point='v1')
@manager_required
-def authorization_get(request, radius_username):
+def authorization_get(radius_username: str, **kwargs) -> List[Dict]:
"""
Retrieve registered authorizations for a specified user and current selected
organization.
@@ -82,6 +84,7 @@ def authorization_get(request, radius_username):
Raises error -32602 (Invalid params) if the username does not exist.
"""
+ request = kwargs.get(REQUEST_KEY)
result = []
try:
@@ -102,14 +105,10 @@ def authorization_get(request, radius_username):
return result
-@jsonrpc_method(
- 'authorization.add(radius_username=String, account=String) -> Object',
- site=api_v1_site,
- authenticated=True
-)
+@rpc_method(name='authorization.add', entry_point='v1')
@manager_required
@transaction.atomic
-def authorization_add(request, radius_username, account):
+def authorization_add(radius_username: str, account: str, **kwargs) -> Dict:
"""
Add a new authorization to the specified user.
@@ -129,6 +128,7 @@ def authorization_add(request, radius_username, account):
Raises error -32602 (Invalid params) if the username does not exist.
"""
+ request = kwargs.get(REQUEST_KEY)
try:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
authenticationdata__username=radius_username)
@@ -141,11 +141,10 @@ def authorization_add(request, radius_username, account):
return format_authorization(authorization)
-@jsonrpc_method('authorization.end(radius_username=String, authorization_id=Number) -> Boolean', site=api_v1_site,
- authenticated=True)
+@rpc_method(name='authorization.end', entry_point='v1')
@manager_required
@transaction.atomic
-def authorization_end(request, radius_username, authorization_id):
+def authorization_end(radius_username: str, authorization_id: int, **kwargs) -> bool:
"""
End an authorization from the specified user.
@@ -159,6 +158,7 @@ def authorization_end(request, radius_username, authorization_id):
Raises error -32602 (Invalid params) if the username does not exist.
Raises error -32602 (Invalid params) if provided authorization cannot be found.
"""
+ request = kwargs.get(REQUEST_KEY)
try:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
authenticationdata__username=radius_username)
diff --git a/alexia/api/v1/methods/billing.py b/alexia/api/v1/methods/billing.py
index 0527c70..63db470 100644
--- a/alexia/api/v1/methods/billing.py
+++ b/alexia/api/v1/methods/billing.py
@@ -1,6 +1,8 @@
+from typing import List, Dict, Optional
+
from django.contrib.auth.models import User
from django.db import transaction
-from jsonrpc import jsonrpc_method
+from modernrpc.core import rpc_method, REQUEST_KEY
from alexia.api.decorators import manager_required
from alexia.api.exceptions import InvalidParamsError, ObjectNotFoundError
@@ -8,12 +10,11 @@
from alexia.auth.backends import OIDC_BACKEND_NAME
from ..common import format_order
-from ..config import api_v1_site
-@jsonrpc_method('order.unsynchronized(unused=Number) -> Array', site=api_v1_site, safe=True, authenticated=True)
+@rpc_method(name='order.unsynchronized', entry_point='v1')
@manager_required
-def order_unsynchronized(request, unused=0):
+def order_unsynchronized(unused: int = 0, **kwargs) -> List[Dict]:
"""
Return a list of unsynchronized orders.
@@ -35,7 +36,7 @@ def order_unsynchronized(request, unused=0):
},
{
"price": "1.00",
- "product": {"name": "Coca Cola"},
+ "product": {"name": "Coca-Cola"},
"amount": 2
}
],
@@ -58,7 +59,7 @@ def order_unsynchronized(request, unused=0):
"purchases": [
{
"price": "1.00",
- "product": {"name": "Coca Cola"},
+ "product": {"name": "Coca-Cola"},
"amount": 2
}, {
"price": "0.50",
@@ -83,6 +84,7 @@ def order_unsynchronized(request, unused=0):
}
]
"""
+ request = kwargs.get(REQUEST_KEY)
result = []
orders = Order.objects.filter(authorization__organization=request.organization, synchronized=False)
@@ -94,9 +96,9 @@ def order_unsynchronized(request, unused=0):
return result
-@jsonrpc_method('order.get(order_id=Number) -> Object', site=api_v1_site, safe=True, authenticated=True)
+@rpc_method(name='order.get', entry_point='v1')
@manager_required
-def order_get(request, order_id):
+def order_get(order_id: int, **kwargs) -> Dict:
"""
Return a specific order.
@@ -137,6 +139,7 @@ def order_get(request, order_id):
}
}
"""
+ request = kwargs.get(REQUEST_KEY)
try:
order = Order.objects.get(authorization__organization=request.organization, pk=order_id)
except Order.DoesNotExist:
@@ -145,9 +148,9 @@ def order_get(request, order_id):
return format_order(order)
-@jsonrpc_method('order.list(radius_username=String) -> Array', site=api_v1_site, safe=True, authenticated=True)
+@rpc_method(name='order.list', entry_point='v1')
@manager_required
-def order_list(request, radius_username=None):
+def order_list(radius_username: Optional[str] = None, **kwargs) -> List[Dict]:
"""
Retrieve a list of orders for the currently selected organization.
@@ -165,7 +168,7 @@ def order_list(request, radius_username=None):
"purchases": [
{
"price": "1.00",
- "product": {"name": "Coca Cola"},
+ "product": {"name": "Coca-Cola"},
"amount": 2
}, {
"price": "0.50",
@@ -213,6 +216,7 @@ def order_list(request, radius_username=None):
}
]
"""
+ request = kwargs.get(REQUEST_KEY)
result = []
orders = Order.objects.filter(event__organizer=request.organization)
@@ -232,10 +236,10 @@ def order_list(request, radius_username=None):
return result
-@jsonrpc_method('order.marksynchronized(order_id=Number) -> Boolean', site=api_v1_site, authenticated=True)
+@rpc_method(name='order.marksynchronized', entry_point='v1')
@manager_required
@transaction.atomic
-def order_marksynchronized(request, order_id):
+def order_marksynchronized(order_id: int, **kwargs) -> bool:
"""
Mark an order as synchronized.
@@ -247,6 +251,7 @@ def order_marksynchronized(request, order_id):
Raises error 422 if provided order id cannot be found.
"""
+ request = kwargs.get(REQUEST_KEY)
try:
order = Order.objects.select_for_update().get(authorization__organization=request.organization, pk=order_id)
except Order.DoesNotExist:
diff --git a/alexia/api/v1/methods/event.py b/alexia/api/v1/methods/event.py
index a9783f1..9a2a230 100644
--- a/alexia/api/v1/methods/event.py
+++ b/alexia/api/v1/methods/event.py
@@ -1,13 +1,13 @@
+from typing import List, Dict
+
from django.utils import timezone
-from jsonrpc import jsonrpc_method
+from modernrpc.core import rpc_method
from alexia.apps.scheduling.models import Event
-from ..config import api_v1_site
-
-@jsonrpc_method('event.upcoming_list(include_ongoing=Boolean) -> Array', site=api_v1_site, safe=True)
-def upcoming_events_list(request, include_ongoing=False):
+@rpc_method(name='event.upcoming_list', entry_point='v1')
+def upcoming_events_list(include_ongoing: bool = False, **kwargs) -> List[Dict]:
"""
List all current and upcoming events.
@@ -39,4 +39,4 @@ def upcoming_events_list(request, include_ongoing=False):
'ends_at': e.ends_at,
'kegs': e.kegs,
'is_risky': e.is_risky,
- } for e in Event.objects.filter(**{filter_key: timezone.now()})]
\ No newline at end of file
+ } for e in Event.objects.filter(**{filter_key: timezone.now()})]
diff --git a/alexia/api/v1/methods/generic.py b/alexia/api/v1/methods/generic.py
index eb8b56e..bb1118c 100644
--- a/alexia/api/v1/methods/generic.py
+++ b/alexia/api/v1/methods/generic.py
@@ -1,11 +1,11 @@
-from django.db import transaction
-from jsonrpc import jsonrpc_method
+from typing import List
-from ..config import api_v1_site
+from django.db import transaction
+from modernrpc.core import rpc_method, registry, REQUEST_KEY, ENTRY_POINT_KEY, PROTOCOL_KEY
-@jsonrpc_method('version() -> Number', site=api_v1_site, safe=True)
-def version(request):
+@rpc_method(name='version', entry_point='v1')
+def version(**kwargs) -> int:
"""
Returns the current API version.
@@ -20,8 +20,8 @@ def version(request):
return 1
-@jsonrpc_method('methods() -> Array', site=api_v1_site, safe=True)
-def methods(request):
+@rpc_method(name='methods', entry_point='v1')
+def methods(**kwargs) -> List[str]:
"""
Introspect the API and return all callable methods.
@@ -29,29 +29,28 @@ def methods(request):
Returns an array with the methods.
"""
- result = []
-
- for proc in api_v1_site.describe(request)['procs']:
- result.append(proc['name'])
+ entry_point = kwargs.get(ENTRY_POINT_KEY)
+ protocol = kwargs.get(PROTOCOL_KEY)
- return result
+ return registry.get_all_method_names(entry_point, protocol, sort_methods=True)
-@jsonrpc_method('login(username=String, password=String) -> Boolean', site=api_v1_site)
+@rpc_method(name='login', entry_point='v1')
@transaction.atomic
-def login(request, username, password):
+def login(username: str, password: str, **kwargs) -> bool:
"""
- Authenticate an user to use the API.
+ Authenticate a user to use the API.
Required user level: None
- Returns true when an user has successful signed in. A session will be
+ Returns true when a user has successfully signed in. A session will be
started and stored. Cookies must be supported by the client.
username -- Username of user
password -- Password of the user
"""
from django.contrib.auth import authenticate, login
+ request = kwargs.get(REQUEST_KEY)
# TODO: Authenticating for the API will be hard once RADIUS shuts down. As a stopgap, we can give each association
# a local Alexia account to use for the API, but in due time we will probably want to move to something
@@ -65,9 +64,9 @@ def login(request, username, password):
return True
-@jsonrpc_method('logout() -> Nil', site=api_v1_site)
+@rpc_method(name='logout', entry_point='v1')
@transaction.atomic
-def logout(request):
+def logout(**kwargs) -> None:
"""
Sign out the current user, even if no one was signed in.
@@ -76,5 +75,6 @@ def logout(request):
Destroys the current session.
"""
from django.contrib.auth import logout
+ request = kwargs.get(REQUEST_KEY)
logout(request)
diff --git a/alexia/api/v1/methods/juliana.py b/alexia/api/v1/methods/juliana.py
index c8ed11c..ab6f8e5 100644
--- a/alexia/api/v1/methods/juliana.py
+++ b/alexia/api/v1/methods/juliana.py
@@ -1,10 +1,11 @@
from decimal import Decimal
+from typing import Dict, List
from django.contrib.auth.models import User
from django.db import transaction
from django.db.models import Sum
-from jsonrpc import jsonrpc_method
-from jsonrpc.exceptions import OtherError
+from modernrpc.core import rpc_method, REQUEST_KEY
+from modernrpc.exceptions import RPCInternalError
from alexia.api.exceptions import (
ForbiddenError, InvalidParamsError, ObjectNotFoundError,
@@ -15,7 +16,7 @@
from alexia.apps.scheduling.models import Event
from ..common import format_authorization
-from ..config import api_v1_site
+from ...decorators import login_required
def rfid_to_identifier(rfid):
@@ -78,8 +79,10 @@ def _get_validate_event(request, event_id, safe=False):
return event
-@jsonrpc_method('juliana.rfid.get(Number,Object) -> Object', site=api_v1_site, safe=True, authenticated=True)
-def juliana_rfid_get(request, event_id, rfid):
+@rpc_method(name='juliana.rfid.get', entry_point='v1')
+@login_required
+def juliana_rfid_get(event_id: int, rfid: Dict, **kwargs) -> Dict:
+ request = kwargs.get(REQUEST_KEY)
event = _get_validate_event(request, event_id, True)
identifier = rfid_to_identifier(rfid=rfid)
@@ -108,10 +111,12 @@ def juliana_rfid_get(request, event_id, rfid):
return res
-@jsonrpc_method('juliana.order.save(Number,Number,Array,Object) -> Nil', site=api_v1_site, authenticated=True)
+@rpc_method(name='juliana.order.save', entry_point='v1')
+@login_required
@transaction.atomic
-def juliana_order_save(request, event_id, user_id, purchases, rfid_data):
+def juliana_order_save(event_id: int, user_id: int, purchases: List[Dict], rfid_data: Dict, **kwargs) -> None:
"""Saves a new order in the database"""
+ request = kwargs.get(REQUEST_KEY)
event = _get_validate_event(request, event_id)
rfid_identifier = rfid_to_identifier(rfid=rfid_data)
@@ -150,7 +155,7 @@ def juliana_order_save(request, event_id, user_id, purchases, rfid_data):
if event != product.event:
raise InvalidParamsError('Product %s is not available for this event' % p['product'])
else:
- raise OtherError('Product %s is broken' % p['product'])
+ raise RPCInternalError('Product %s is broken' % p['product'])
amount = p['amount']
@@ -169,8 +174,10 @@ def juliana_order_save(request, event_id, user_id, purchases, rfid_data):
return True
-@jsonrpc_method('juliana.user.check(Number, Number) -> Number', site=api_v1_site, safe=True, authenticated=True)
-def juliana_user_check(request, event_id, user_id):
+@rpc_method(name='juliana.user.check', entry_point='v1')
+@login_required
+def juliana_user_check(event_id: int, user_id: int, **kwargs) -> int:
+ request = kwargs.get(REQUEST_KEY)
event = _get_validate_event(request, event_id, True)
try:
@@ -187,12 +194,14 @@ def juliana_user_check(request, event_id, user_id):
else:
return 0
-@jsonrpc_method('juliana.writeoff.save(Number,Number,Array) -> Nil', site=api_v1_site, authenticated=True)
+@rpc_method(name='juliana.writeoff.save', entry_point='v1')
+@login_required
@transaction.atomic
-def juliana_writeoff_save(request, event_id, writeoff_id, purchases):
+def juliana_writeoff_save(event_id: int, writeoff_id: int, purchases: List[Dict], **kwargs) -> bool:
"""Saves a writeoff order in the Database"""
+ request = kwargs.get(REQUEST_KEY)
event = _get_validate_event(request, event_id)
-
+
try:
writeoff_cat = WriteoffCategory.objects.get(id=writeoff_id)
except WriteoffCategory.DoesNotExist:
@@ -217,7 +226,7 @@ def juliana_writeoff_save(request, event_id, writeoff_id, purchases):
if event != product.event:
raise InvalidParamsError('Product %s is not available for this event' % p['product'])
else:
- raise OtherError('Product %s is broken' % p['product'])
+ raise RPCInternalError('Product %s is broken' % p['product'])
amount = p['amount']
@@ -228,9 +237,9 @@ def juliana_writeoff_save(request, event_id, writeoff_id, purchases):
if price != p['price'] / Decimal(100):
raise InvalidParamsError('Price for product %s is incorrect' % p['product'])
-
+
purchase = WriteOffPurchase(order=order, product=product.name, amount=amount, price=price)
purchase.save()
order.save(force_update=True) # ensure order.amount is correct
- return True
\ No newline at end of file
+ return True
diff --git a/alexia/api/v1/methods/organization.py b/alexia/api/v1/methods/organization.py
index 8676d55..eee6901 100644
--- a/alexia/api/v1/methods/organization.py
+++ b/alexia/api/v1/methods/organization.py
@@ -1,13 +1,15 @@
-from jsonrpc import jsonrpc_method
+from typing import Optional, List
+from modernrpc.core import rpc_method, REQUEST_KEY
+
+from alexia.api.decorators import login_required
from alexia.api.exceptions import ObjectNotFoundError
from alexia.apps.organization.models import Organization
-from ..config import api_v1_site
-
-@jsonrpc_method('organization.current.get() -> String', site=api_v1_site, safe=True, authenticated=True)
-def organization_current_get(request):
+@rpc_method(name='organization.current.get', entry_point='v1')
+@login_required
+def organization_current_get(**kwargs) -> Optional[str]:
"""
Return the current organization slug.
@@ -20,14 +22,16 @@ def organization_current_get(request):
Example return value:
"inter-actief"
"""
+ request = kwargs.get(REQUEST_KEY)
if request.organization:
return request.organization.slug
else:
return None
-@jsonrpc_method('organization.current.set(organization=String) -> Boolean', site=api_v1_site, authenticated=True)
-def organization_current_set(request, organization):
+@rpc_method(name='organization.current.set', entry_point='v1')
+@login_required
+def organization_current_set(organization: str, **kwargs) -> bool:
"""
Set the current organization.
@@ -42,6 +46,7 @@ def organization_current_set(request, organization):
Raises error 404 if provided organization cannot be found.
"""
+ request = kwargs.get(REQUEST_KEY)
if not organization:
if 'organization_pk' in request.session:
del request.session['organization_pk']
@@ -61,14 +66,15 @@ def organization_current_set(request, organization):
return False
-@jsonrpc_method('organization.list() -> Array', site=api_v1_site, safe=True)
-def organization_list(request):
+@rpc_method(name='organization.list', entry_point='v1')
+@login_required
+def organization_list(**kwargs) -> List[str]:
"""
List all public organizations.
Required user level: None
- Returns a array with zero or more organizations.
+ Returns an array with zero or more organizations.
Example return value:
[
diff --git a/alexia/api/v1/methods/rfid.py b/alexia/api/v1/methods/rfid.py
index d47316e..8cb13ee 100644
--- a/alexia/api/v1/methods/rfid.py
+++ b/alexia/api/v1/methods/rfid.py
@@ -1,6 +1,8 @@
+from typing import List, Dict
+
from django.contrib.auth.models import User
from django.db import transaction
-from jsonrpc import jsonrpc_method
+from modernrpc.core import rpc_method, REQUEST_KEY
from alexia.api.decorators import manager_required
from alexia.api.exceptions import InvalidParamsError
@@ -8,12 +10,11 @@
from alexia.auth.backends import OIDC_BACKEND_NAME
from ..common import format_rfidcard
-from ..config import api_v1_site
-@jsonrpc_method('rfid.list(radius_username=String) -> Array', site=api_v1_site, safe=True, authenticated=True)
+@rpc_method(name='rfid.list', entry_point='v1')
@manager_required
-def rfid_list(request, radius_username=None):
+def rfid_list(radius_username: str = None, **kwargs) -> List[Dict]:
"""
Retrieve registered RFID cards for the current selected organization.
@@ -49,6 +50,7 @@ def rfid_list(request, radius_username=None):
}
]
"""
+ request = kwargs.get(REQUEST_KEY)
result = []
rfidcards = RfidCard.objects.filter(managed_by=request.organization)
@@ -68,9 +70,9 @@ def rfid_list(request, radius_username=None):
return result
-@jsonrpc_method('rfid.get(radius_username=String) -> Array', site=api_v1_site, safe=True, authenticated=True)
+@rpc_method(name='rfid.get', entry_point='v1')
@manager_required
-def rfid_get(request, radius_username):
+def rfid_get(radius_username: str, **kwargs) -> List[str]:
"""
Retrieve registered RFID cards for a specified user and current selected
organization.
@@ -91,6 +93,7 @@ def rfid_get(request, radius_username):
Raises error -32602 (Invalid params) if the username does not exist.
"""
+ request = kwargs.get(REQUEST_KEY)
result = []
try:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
@@ -106,10 +109,10 @@ def rfid_get(request, radius_username):
return result
-@jsonrpc_method('rfid.add(radius_username=String, identifier=String) -> Object', site=api_v1_site, authenticated=True)
+@rpc_method(name='rfid.add', entry_point='v1')
@manager_required
@transaction.atomic
-def rfid_add(request, radius_username, identifier):
+def rfid_add(radius_username: str, identifier: str, **kwargs) -> Dict:
"""
Add a new RFID card to the specified user.
@@ -131,7 +134,7 @@ def rfid_add(request, radius_username, identifier):
Raises error -32602 (Invalid params) if the RFID card already exists for this person.
Raises error -32602 (Invalid params) if the RFID card is already registered by someone else.
"""
-
+ request = kwargs.get(REQUEST_KEY)
try:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
authenticationdata__username=radius_username)
@@ -155,12 +158,12 @@ def rfid_add(request, radius_username, identifier):
raise InvalidParamsError('RFID card with provided identifier already exists for this person')
-@jsonrpc_method('rfid.remove(radius_username=String, identifier=String) -> Nil', site=api_v1_site, authenticated=True)
+@rpc_method(name='rfid.remove', entry_point='v1')
@manager_required
@transaction.atomic
-def rfid_remove(request, radius_username, identifier):
+def rfid_remove(radius_username: str, identifier: str, **kwargs) -> None:
"""
- Remove a RFID card from the specified user.
+ Remove an RFID card from the specified user.
Required user level: Manager
@@ -170,6 +173,7 @@ def rfid_remove(request, radius_username, identifier):
Raises error -32602 (Invalid params) if the username does not exist.
Raises error -32602 (Invalid params) if the RFID card does not exist for this person/organization.
"""
+ request = kwargs.get(REQUEST_KEY)
try:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
authenticationdata__username=radius_username)
diff --git a/alexia/api/v1/methods/scheduling.py b/alexia/api/v1/methods/scheduling.py
index 435876e..01b1644 100644
--- a/alexia/api/v1/methods/scheduling.py
+++ b/alexia/api/v1/methods/scheduling.py
@@ -1,21 +1,16 @@
-from jsonrpc import jsonrpc_method
+from typing import List, Dict
+
+from modernrpc.core import rpc_method, REQUEST_KEY
from alexia.api.decorators import manager_required
from alexia.api.exceptions import InvalidParamsError
from alexia.apps.scheduling.models import BartenderAvailability
from alexia.auth.backends import OIDC_BACKEND_NAME, User
-from ..config import api_v1_site
-
-@jsonrpc_method(
- 'user.get_availabilities(radius_username=String) -> Array',
- site=api_v1_site,
- safe=True,
- authenticated=True
-)
+@rpc_method(name='user.get_availabilities', entry_point='v1')
@manager_required
-def user_get_availabilities(request, radius_username):
+def user_get_availabilities(radius_username: str, **kwargs) -> List[Dict]:
"""
Retrieve the availabilities entered by a specific user for the current organization.
@@ -45,6 +40,7 @@ def user_get_availabilities(request, radius_username):
}
]
"""
+ request = kwargs.get(REQUEST_KEY)
try:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
authenticationdata__username=radius_username)
diff --git a/alexia/api/v1/methods/user.py b/alexia/api/v1/methods/user.py
index 9c751f5..0a20194 100644
--- a/alexia/api/v1/methods/user.py
+++ b/alexia/api/v1/methods/user.py
@@ -1,8 +1,10 @@
+from typing import Dict
+
from django.contrib.auth.models import User
from django.db import transaction
-from jsonrpc import jsonrpc_method
+from modernrpc.core import rpc_method, REQUEST_KEY
-from alexia.api.decorators import manager_required
+from alexia.api.decorators import manager_required, login_required
from alexia.api.exceptions import InvalidParamsError, ObjectNotFoundError
from alexia.apps.organization.models import (
AuthenticationData, Certificate, Membership, Profile,
@@ -10,18 +12,16 @@
from alexia.auth.backends import OIDC_BACKEND_NAME
from ..common import format_certificate, format_user
-from ..config import api_v1_site
-@jsonrpc_method('user.add(radius_username=String, first_name=String, last_name=String, email=String) -> Object',
- site=api_v1_site, authenticated=True)
+@rpc_method(name='user.add', entry_point='v1')
@manager_required
@transaction.atomic
-def user_add(request, radius_username, first_name, last_name, email):
+def user_add(radius_username: str, first_name: str, last_name: str, email: str, **kwargs) -> Dict:
"""
Add a new user to Alexia.
- An user must have an unique username and a valid email address.
+ A user must have a unique username and a valid email address.
Returns the user information on success.
@@ -55,8 +55,9 @@ def user_add(request, radius_username, first_name, last_name, email):
return format_user(user)
-@jsonrpc_method('user.exists(radius_username=String) -> Boolean', site=api_v1_site, authenticated=True, safe=True)
-def user_exists(request, radius_username):
+@rpc_method(name='user.exists', entry_point='v1')
+@login_required
+def user_exists(radius_username: str, **kwargs) -> bool:
"""
Check if a user exists by his or her username.
@@ -68,12 +69,13 @@ def user_exists(request, radius_username):
authenticationdata__username=radius_username).exists()
-@jsonrpc_method('user.get(radius_username=String) -> Object', site=api_v1_site, authenticated=True, safe=True)
-def user_get(request, radius_username):
+@rpc_method(name='user.get', entry_point='v1')
+@login_required
+def user_get(radius_username: str, **kwargs) -> Dict:
"""
Retrieve information about a specific user.
- Returns a object representing the user. Result contains first_name,
+ Returns an object representing the user. Result contains first_name,
last_name and radius_username.
radius_username -- Username to search for.
@@ -96,8 +98,9 @@ def user_get(request, radius_username):
return format_user(user)
-@jsonrpc_method('user.get_by_id(user_id=Number) -> Object', site=api_v1_site, authenticated=True, safe=True)
-def user_get_by_id(request, user_id):
+@rpc_method(name='user.get_by_id', entry_point='v1')
+@login_required
+def user_get_by_id(user_id: int, **kwargs) -> Dict:
"""
Retrieve information about a specific user.
@@ -120,14 +123,9 @@ def user_get_by_id(request, user_id):
return format_user(user)
-@jsonrpc_method(
- 'user.get_membership(radius_username=String) -> Object',
- site=api_v1_site,
- safe=True,
- authenticated=True
-)
+@rpc_method(name='user.get_membership', entry_point='v1')
@manager_required
-def user_get_membership(request, radius_username):
+def user_get_membership(radius_username: str, **kwargs) -> Dict:
"""
Retrieve the membership details for a specific user for the current organization.
@@ -149,6 +147,7 @@ def user_get_membership(request, radius_username):
"is_active": True
}
"""
+ request = kwargs.get(REQUEST_KEY)
try:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
authenticationdata__username=radius_username)
@@ -174,14 +173,9 @@ def user_get_membership(request, radius_username):
}
-@jsonrpc_method(
- 'user.get_iva_certificate(radius_username=String) -> Object',
- site=api_v1_site,
- safe=True,
- authenticated=True
-)
+@rpc_method(name='user.get_iva_certificate', entry_point='v1')
@manager_required
-def user_get_iva_certificate(request, radius_username):
+def user_get_iva_certificate(radius_username: str, **kwargs) -> Dict:
"""
Retrieve the IVA certificate file for a specific user.
diff --git a/alexia/api/v1/test/test_authorization.py b/alexia/api/v1/test/test_authorization.py
index 4229a04..a8edd92 100644
--- a/alexia/api/v1/test/test_authorization.py
+++ b/alexia/api/v1/test/test_authorization.py
@@ -31,9 +31,8 @@ def test_authorization_list_invalid_user(self):
"""
# Invalid user
self.send_and_compare_request_error('authorization.list', ['invalidusername'],
- status_code=422,
+ status_code=200,
error_code=-32602,
- error_name='InvalidParamsError',
- error_message='InvalidParamsError: User with provided ' +
+ error_message='User with provided ' +
'username does not exist',
)
diff --git a/alexia/api/v1/test/test_billing.py b/alexia/api/v1/test/test_billing.py
index 9c91ba8..6e7e1b8 100644
--- a/alexia/api/v1/test/test_billing.py
+++ b/alexia/api/v1/test/test_billing.py
@@ -80,8 +80,7 @@ def test_order_marksynchronized_invalid_order(self):
self.load_billing_order_data()
self.send_and_compare_request_error('order.marksynchronized', [self.data['order1'].id*10],
- status_code=422,
+ status_code=200,
error_code=-32602,
- error_name='InvalidParamsError',
- error_message='InvalidParamsError: Order with id not found',
+ error_message='Order with id not found',
)
diff --git a/alexia/api/v1/test/test_juliana.py b/alexia/api/v1/test/test_juliana.py
index af16d43..1358735 100644
--- a/alexia/api/v1/test/test_juliana.py
+++ b/alexia/api/v1/test/test_juliana.py
@@ -244,10 +244,9 @@ def test_rfid_get_no_rfid(self):
self.send_and_compare_request_error(
'juliana.rfid.get', [event_id, rfid_data],
- status_code=404,
+ status_code=200,
error_code=404,
- error_name='ObjectNotFoundError',
- error_message='ObjectNotFoundError: RFID card not found',
+ error_message='RFID card not found',
)
def test_rfid_get_no_authorization(self):
@@ -264,10 +263,9 @@ def test_rfid_get_no_authorization(self):
self.send_and_compare_request_error(
'juliana.rfid.get', [event_id, rfid_data],
- status_code=404,
+ status_code=200,
error_code=404,
- error_name='ObjectNotFoundError',
- error_message='ObjectNotFoundError: No authorization found for user',
+ error_message='No authorization found for user',
)
def test_rfid_get_other_authorization(self):
@@ -284,10 +282,9 @@ def test_rfid_get_other_authorization(self):
self.send_and_compare_request_error(
'juliana.rfid.get', [event_id, rfid_data],
- status_code=404,
+ status_code=200,
error_code=404,
- error_name='ObjectNotFoundError',
- error_message='ObjectNotFoundError: No authorization found for user',
+ error_message='No authorization found for user',
)
def test_rfid_get_invalid_event(self):
@@ -301,10 +298,9 @@ def test_rfid_get_invalid_event(self):
self.send_and_compare_request_error(
'juliana.rfid.get', [event_id, rfid_data],
- status_code=404,
+ status_code=200,
error_code=404,
- error_name='ObjectNotFoundError',
- error_message='ObjectNotFoundError: Event does not exist',
+ error_message='Event does not exist',
)
def test_user_check_no_orders(self):
@@ -330,10 +326,9 @@ def test_user_check_invalid_event(self):
self.send_and_compare_request_error(
'juliana.user.check', [event_id, user_id],
- status_code=404,
+ status_code=200,
error_code=404,
- error_name='ObjectNotFoundError',
- error_message='ObjectNotFoundError: Event does not exist',
+ error_message='Event does not exist',
)
def test_user_check_invalid_user(self):
@@ -342,8 +337,7 @@ def test_user_check_invalid_user(self):
self.send_and_compare_request_error(
'juliana.user.check', [event_id, user_id],
- status_code=404,
+ status_code=200,
error_code=404,
- error_name='ObjectNotFoundError',
- error_message='ObjectNotFoundError: User does not exist',
+ error_message='User does not exist',
)
diff --git a/alexia/api/v1/views.py b/alexia/api/v1/views.py
deleted file mode 100644
index 0f728d3..0000000
--- a/alexia/api/v1/views.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from ..views import APIBrowserView, APIDocumentationView
-from .config import api_v1_site
-
-
-class APIv1BrowserView(APIBrowserView):
- site = api_v1_site
- mountpoint = 'api_v1_mountpoint'
- template_name = 'api/v1/browse.html'
-
-
-class APIv1DocumentationView(APIDocumentationView):
- site = api_v1_site
- mountpoint = 'api_v1_mountpoint'
- template_name = 'api/v1/doc.html'
- methods = ['authorization.add', 'authorization.end', 'authorization.list', 'event.upcoming_list', 'login',
- 'logout', 'order.get', 'order.marksynchronized', 'order.unsynchronized', 'organization.current.get',
- 'organization.current.set', 'rfid.add', 'rfid.list', 'rfid.remove', 'user.add',
- 'user.exists', 'user.get']
diff --git a/alexia/api/views.py b/alexia/api/views.py
index 5a0ce8c..389cd2d 100644
--- a/alexia/api/views.py
+++ b/alexia/api/views.py
@@ -1,76 +1,4 @@
-from json import dumps
-
-from django.http import HttpResponse
-from django.urls import reverse
from django.views.generic.base import TemplateView
-from jsonrpc import mochikit
-from jsonrpc.site import jsonrpc_site
-
-
-class APIBrowserView(TemplateView):
- template_name = 'api/browse.html'
- site = jsonrpc_site
- mountpoint = 'jsonrpc_mountpoint'
-
- def get(self, request, *args, **kwargs):
- if request.GET.get('f', None) == 'mochikit.js':
- return HttpResponse(mochikit.mochikit, content_type='application/javascript')
- if request.GET.get('f', None) == 'interpreter.js':
- return HttpResponse(mochikit.interpreter, content_type='application/javascript')
- return super(APIBrowserView, self).get(request, *args, **kwargs)
-
- def get_context_data(self, **kwargs):
- context = super(APIBrowserView, self).get_context_data(**kwargs)
-
- desc = self.site.service_desc()
- context['methods'] = sorted(desc['procs'], key=lambda x: x['name'])
- context['method_names_str'] = dumps([m['name'] for m in desc['procs']])
- context['mountpoint'] = reverse(self.mountpoint)
-
- return context
-
-
-class APIDocumentationView(TemplateView):
- template_name = 'api/documentation.html'
- site = jsonrpc_site
- mountpoint = 'jsonrpc_mountpoint'
- methods = None
-
- def get_context_data(self, **kwargs):
- context = super(APIDocumentationView, self).get_context_data(**kwargs)
-
- desc = self.site.service_desc()
- methods = sorted(desc['procs'], key=lambda x: x['name'])
-
- # Filter methods if filter is provided
- if self.methods is not None:
- methods = [method for method in methods if method['name'] in self.methods]
-
- # Strip leading spaces if first line starts with spaces
- for method in methods:
- summarylines = method['summary'].splitlines()
-
- # Strip first line if emtpy
- if not summarylines[0].strip():
- del summarylines[0]
-
- # Strip last line if emtpy
- if not summarylines[-1].strip():
- del summarylines[-1]
-
- if summarylines[0].startswith(' '):
- result = []
- for line in summarylines:
- if line.startswith(' '):
- result.append(line[4:])
- else:
- result.append(line)
- method['summary'] = '\n'.join(result)
-
- context['methods'] = methods
- context['mountpoint'] = reverse(self.mountpoint)
-
- return context
class APIInfoView(TemplateView):
diff --git a/alexia/apps/billing/forms.py b/alexia/apps/billing/forms.py
index a6428ee..715df54 100644
--- a/alexia/apps/billing/forms.py
+++ b/alexia/apps/billing/forms.py
@@ -4,7 +4,7 @@
from crispy_forms.helper import FormHelper
from django import forms
from django.utils import timezone
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from alexia.apps.billing.models import (
PermanentProduct, PriceGroup, ProductGroup, SellingPrice,
diff --git a/alexia/apps/billing/models.py b/alexia/apps/billing/models.py
index 76e2223..6b992dc 100644
--- a/alexia/apps/billing/models.py
+++ b/alexia/apps/billing/models.py
@@ -8,7 +8,7 @@
from django.db.models import Q
from django.urls import reverse
from django.utils import timezone
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from alexia.apps.organization.models import Organization
from alexia.apps.scheduling.models import Event
@@ -297,6 +297,11 @@ def __str__(self):
)
def save(self, *args, **kwargs):
+ # Set amount to 0, save the order, and then update the amount.
+ # This needs to be done in 2 steps because the get_price() method uses a
+ # relationship that can only be used after the model has been saved
+ self.amount = Decimal('0.0')
+ super(Order, self).save(*args, **kwargs)
self.amount = self.get_price()
super(Order, self).save(*args, **kwargs)
@@ -357,7 +362,7 @@ class WriteOffOrder(models.Model):
on_delete=models.PROTECT, # We cannot delete purchases
verbose_name=_('writeoff category')
)
-
+
class Meta:
ordering = ['-placed_at']
verbose_name = _('writeoff order')
diff --git a/alexia/apps/billing/urls.py b/alexia/apps/billing/urls.py
index 7f2937b..db88e09 100644
--- a/alexia/apps/billing/urls.py
+++ b/alexia/apps/billing/urls.py
@@ -1,53 +1,53 @@
-from django.conf.urls import url
+from django.urls import re_path, path
from . import views
urlpatterns = [
- url(r'^order/$', views.OrderListView.as_view(), name='orders'),
- url(r'^order/(?P[0-9]+)/$', views.OrderDetailView.as_view(), name='event-orders'),
- url(r'^order/writeoff/(?P[0-9]+)/$', views.WriteOffExportView.as_view(), name='writeoff_export'),
- url(r'^writeoff/(?P[0-9]+)/$', views.WriteOffDetailView.as_view(), name='writeoff-order'),
- url(r'^order/export/$', views.OrderExportView.as_view(), name='export-orders'),
- url(r'^stats/(?P[0-9]{4})/$', views.OrderYearView.as_view(), name='year-orders'),
- url(r'^stats/(?P[0-9]{4})/(?P[0-9]{1,2})/$', views.OrderMonthView.as_view(), name='month-orders'),
- url(r'^payment/(?P[0-9]+)/$', views.PaymentDetailView.as_view(), name='order'),
-
- url(r'^pricegroup/$', views.PriceGroupListView.as_view(), name='pricegroup_list'),
- url(r'^pricegroup/create/$', views.PriceGroupCreateView.as_view(), name='pricegroup_create'),
- url(r'^pricegroup/(?P[0-9]+)/$', views.PriceGroupDetailView.as_view(), name='pricegroup_detail'),
- url(r'^pricegroup/(?P[0-9]+)/update/$', views.PriceGroupUpdateView.as_view(), name='pricegroup_update'),
- url(r'^pricegroup/(?P[0-9]+)/delete/$', views.PriceGroupDeleteView.as_view(), name='pricegroup_delete'),
-
- url(r'^productgroup/$', views.ProductGroupListView.as_view(), name='productgroup_list'),
- url(r'^productgroup/create/$', views.ProductGroupCreateView.as_view(), name='productgroup_create'),
- url(r'^productgroup/(?P[0-9]+)/$', views.ProductGroupDetailView.as_view(), name='productgroup_detail'),
- url(r'^productgroup/(?P[0-9]+)/update/$', views.ProductGroupUpdateView.as_view(), name='productgroup_update'),
- url(r'^productgroup/(?P[0-9]+)/delete/$', views.ProductGroupDeleteView.as_view(), name='productgroup_delete'),
-
- url(r'^product/(?P[0-9]+)/$', views.ProductRedirectView.as_view(), name='product_detail'),
-
- url(r'^product/permanent/$', views.ProductListView.as_view(), name='product_list'),
- url(r'^product/permanent/create/$', views.ProductCreateView.as_view(), name='product_create'),
- url(r'^product/permanent/create/productgroup/(?P[0-9]+)/$',
+ path('order/', views.OrderListView.as_view(), name='orders'),
+ path('order//', views.OrderDetailView.as_view(), name='event-orders'),
+ path('order/writeoff//', views.WriteOffExportView.as_view(), name='writeoff_export'),
+ path('writeoff//', views.WriteOffDetailView.as_view(), name='writeoff-order'),
+ path('order/export/', views.OrderExportView.as_view(), name='export-orders'),
+ re_path(r'^stats/(?P[0-9]{4})/$', views.OrderYearView.as_view(), name='year-orders'),
+ re_path(r'^stats/(?P[0-9]{4})/(?P[0-9]{1,2})/$', views.OrderMonthView.as_view(), name='month-orders'),
+ path('payment//', views.PaymentDetailView.as_view(), name='order'),
+
+ path('pricegroup/', views.PriceGroupListView.as_view(), name='pricegroup_list'),
+ path('pricegroup/create/', views.PriceGroupCreateView.as_view(), name='pricegroup_create'),
+ path('pricegroup//', views.PriceGroupDetailView.as_view(), name='pricegroup_detail'),
+ path('pricegroup//update/', views.PriceGroupUpdateView.as_view(), name='pricegroup_update'),
+ path('pricegroup//delete/', views.PriceGroupDeleteView.as_view(), name='pricegroup_delete'),
+
+ path('productgroup/', views.ProductGroupListView.as_view(), name='productgroup_list'),
+ path('productgroup/create/', views.ProductGroupCreateView.as_view(), name='productgroup_create'),
+ path('productgroup//', views.ProductGroupDetailView.as_view(), name='productgroup_detail'),
+ path('productgroup//update/', views.ProductGroupUpdateView.as_view(), name='productgroup_update'),
+ path('productgroup//delete/', views.ProductGroupDeleteView.as_view(), name='productgroup_delete'),
+
+ path('product//', views.ProductRedirectView.as_view(), name='product_detail'),
+
+ path('product/permanent/', views.ProductListView.as_view(), name='product_list'),
+ path('product/permanent/create/', views.ProductCreateView.as_view(), name='product_create'),
+ path('product/permanent/create/productgroup//',
views.ProductCreateView.as_view(), name='product_create'),
- url(r'^product/permanent/(?P[0-9]+)/$', views.ProductDetailView.as_view(), name='product_detail'),
- url(r'^product/permanent/(?P[0-9]+)/update/$', views.ProductUpdateView.as_view(), name='product_update'),
- url(r'^product/permanent/(?P[0-9]+)/delete/$', views.ProductDeleteView.as_view(), name='product_delete'),
+ path('product/permanent//', views.ProductDetailView.as_view(), name='product_detail'),
+ path('product/permanent//update/', views.ProductUpdateView.as_view(), name='product_update'),
+ path('product/permanent//delete/', views.ProductDeleteView.as_view(), name='product_delete'),
- url(r'^product/temporary/create/event/(?P[0-9]+)/$', views.TemporaryProductCreateView.as_view(),
+ path('product/temporary/create/event//', views.TemporaryProductCreateView.as_view(),
name='temporaryproduct_create'),
- url(r'^product/temporary/(?P[0-9]+)/update/$', views.TemporaryProductUpdateView.as_view(),
+ path('product/temporary//update/', views.TemporaryProductUpdateView.as_view(),
name='temporaryproduct_update'),
- url(r'^product/temporary/(?P[0-9]+)/delete/$', views.TemporaryProductDeleteView.as_view(),
+ path('product/temporary//delete/', views.TemporaryProductDeleteView.as_view(),
name='temporaryproduct_delete'),
- url(r'^sellingprice/$', views.SellingPriceListView.as_view(), name='sellingprice_list'),
- url(r'^sellingprice/create/pricegroup/(?P[0-9]+)/$', views.SellingPriceCreateView.as_view(),
+ path('sellingprice/', views.SellingPriceListView.as_view(), name='sellingprice_list'),
+ path('sellingprice/create/pricegroup//', views.SellingPriceCreateView.as_view(),
name='sellingprice_create'),
- url(r'^sellingprice/create/productgroup/(?P[0-9]+)/$', views.SellingPriceCreateView.as_view(),
+ path('sellingprice/create/productgroup//', views.SellingPriceCreateView.as_view(),
name='sellingprice_create'),
- url(r'^sellingprice/create/pricegroup/(?P[0-9]+)/productgroup/(?P[0-9]+)/$',
+ path('sellingprice/create/pricegroup//productgroup//',
views.SellingPriceCreateView.as_view(), name='sellingprice_create'),
- url(r'^sellingprice/(?P[0-9]+)/update/$', views.SellingPriceUpdateView.as_view(), name='sellingprice_update'),
- url(r'^sellingprice/(?P[0-9]+)/delete/$', views.SellingPriceDeleteView.as_view(), name='sellingprice_delete'),
+ path('sellingprice//update/', views.SellingPriceUpdateView.as_view(), name='sellingprice_update'),
+ path('sellingprice//delete/', views.SellingPriceDeleteView.as_view(), name='sellingprice_delete'),
]
diff --git a/alexia/apps/billing/views.py b/alexia/apps/billing/views.py
index 7053bef..a747b15 100644
--- a/alexia/apps/billing/views.py
+++ b/alexia/apps/billing/views.py
@@ -8,7 +8,7 @@
from django.shortcuts import get_object_or_404, render
from django.urls import reverse, reverse_lazy
from django.utils.dates import MONTHS
-from django.utils.translation import ugettext as _
+from django.utils.translation import gettext as _
from django.views import View
from django.views.generic.base import RedirectView, TemplateView
from django.views.generic.detail import DetailView, SingleObjectMixin
@@ -132,7 +132,7 @@ def get_context_data(self, **kwargs):
.annotate(amount=Sum('amount'), price=Sum('price'))
writeoff_exists = self.object.writeoff_orders.exists
-
+
grouped_writeoff_products = None
writeoff_orders = None
if writeoff_exists:
@@ -179,9 +179,9 @@ def get(self, request, *args, **kwargs):
if not writeoff_exists:
raise Http404
-
+
grouped_writeoff_products = WriteOffPurchase.get_writeoff_products(event=event)
-
+
return JsonResponse(grouped_writeoff_products)
@@ -273,7 +273,7 @@ class PriceGroupUpdateView(ManagerRequiredMixin, OrganizationFilterMixin, Crispy
fields = ['name']
-class PriceGroupDeleteView(ManagerRequiredMixin, OrganizationFilterMixin, FormMixin, DeleteView):
+class PriceGroupDeleteView(ManagerRequiredMixin, OrganizationFilterMixin, DeleteView):
model = PriceGroup
success_url = reverse_lazy('pricegroup_list')
form_class = DeletePriceGroupForm
diff --git a/alexia/apps/config.py b/alexia/apps/config.py
index 176cba4..2b483b8 100644
--- a/alexia/apps/config.py
+++ b/alexia/apps/config.py
@@ -1,5 +1,5 @@
from django.apps import AppConfig
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
class BillingConfig(AppConfig):
diff --git a/alexia/apps/consumption/forms.py b/alexia/apps/consumption/forms.py
index 2d953c4..e55d983 100644
--- a/alexia/apps/consumption/forms.py
+++ b/alexia/apps/consumption/forms.py
@@ -4,7 +4,7 @@
from django.forms import inlineformset_factory
from django.utils import timezone
from django.utils.dates import MONTHS
-from django.utils.translation import ugettext as _
+from django.utils.translation import gettext as _
from alexia.forms import AlexiaForm, EmptyInlineFormSet
diff --git a/alexia/apps/consumption/models.py b/alexia/apps/consumption/models.py
index 6d82ad3..bbf5935 100644
--- a/alexia/apps/consumption/models.py
+++ b/alexia/apps/consumption/models.py
@@ -1,7 +1,7 @@
from django.conf import settings
from django.core.validators import MinValueValidator
from django.db import models
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from alexia.apps.scheduling.models import Event
diff --git a/alexia/apps/consumption/urls.py b/alexia/apps/consumption/urls.py
index ad70d41..6cee533 100644
--- a/alexia/apps/consumption/urls.py
+++ b/alexia/apps/consumption/urls.py
@@ -1,19 +1,19 @@
-from django.conf.urls import url
+from django.urls import path
from . import views
urlpatterns = [
- url(r'^products/$', views.ConsumptionProductListView.as_view(), name='consumptionproduct_list'),
- url(r'^products/new/$', views.ConsumptionProductCreateView.as_view(), name='consumptionproduct_create'),
- url(r'^products/new/weight/$', views.WeightConsumptionProductCreateView.as_view(),
+ path('products/', views.ConsumptionProductListView.as_view(), name='consumptionproduct_list'),
+ path('products/new/', views.ConsumptionProductCreateView.as_view(), name='consumptionproduct_create'),
+ path('products/new/weight/', views.WeightConsumptionProductCreateView.as_view(),
name='weightconsumptionproduct_create'),
- url(r'^products/(?P\d+)/edit/$', views.ConsumptionProductUpdateView.as_view(),
+ path('products//edit/', views.ConsumptionProductUpdateView.as_view(),
name='consumptionproduct_update'),
- url(r'^products/(?P\d+)/edit/weight/$', views.WeightConsumptionProductUpdateView.as_view(),
+ path('products//edit/weight/', views.WeightConsumptionProductUpdateView.as_view(),
name='weightconsumptionproduct_update'),
- url(r'^forms/$', views.ConsumptionFormListView.as_view(), name='consumptionform_list'),
- url(r'^forms/export/$', views.ConsumptionFormExportView.as_view(), name='consumptionform_export'),
- url(r'^forms/(?P\d+)/$', views.ConsumptionFormDetailView.as_view(), name='consumptionform_detail'),
- url(r'^forms/(?P\d+)/pdf/$', views.ConsumptionFormPDFView.as_view(), name='consumptionform_pdf'),
+ path('forms/', views.ConsumptionFormListView.as_view(), name='consumptionform_list'),
+ path('forms/export/', views.ConsumptionFormExportView.as_view(), name='consumptionform_export'),
+ path('forms//', views.ConsumptionFormDetailView.as_view(), name='consumptionform_detail'),
+ path('forms//pdf/', views.ConsumptionFormPDFView.as_view(), name='consumptionform_pdf'),
]
diff --git a/alexia/apps/consumption/views.py b/alexia/apps/consumption/views.py
index 7db243c..f943a28 100644
--- a/alexia/apps/consumption/views.py
+++ b/alexia/apps/consumption/views.py
@@ -8,7 +8,7 @@
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse_lazy
from django.utils import timezone
-from django.utils.translation import ugettext as _
+from django.utils.translation import gettext as _
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, FormView, UpdateView
from django.views.generic.list import ListView
diff --git a/alexia/apps/general/views.py b/alexia/apps/general/views.py
index 578a0e3..3822775 100644
--- a/alexia/apps/general/views.py
+++ b/alexia/apps/general/views.py
@@ -11,13 +11,13 @@
from django.shortcuts import get_object_or_404, resolve_url
from django.template.response import TemplateResponse
from django.urls import reverse_lazy
-from django.utils.http import is_safe_url
+from django.utils.http import url_has_allowed_host_and_scheme
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import RedirectView, TemplateView
from django.views.generic.edit import UpdateView
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from alexia.apps.organization.models import Location, Membership, Organization
from alexia.apps.scheduling.models import (
@@ -42,7 +42,7 @@
def _get_login_redirect_url(request, redirect_to):
# Ensure the user-originating redirection URL is safe.
- if not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
+ if not url_has_allowed_host_and_scheme(url=redirect_to, allowed_hosts=request.get_host()):
return resolve_url(settings.LOGIN_REDIRECT_URL)
return redirect_to
diff --git a/alexia/apps/organization/admin.py b/alexia/apps/organization/admin.py
index 62cb6ad..17c7938 100644
--- a/alexia/apps/organization/admin.py
+++ b/alexia/apps/organization/admin.py
@@ -1,7 +1,7 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import Group, User
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from alexia.apps.billing.models import Authorization, RfidCard, WriteoffCategory
from alexia.apps.scheduling.models import Availability
@@ -74,7 +74,7 @@ class LocationAdmin(admin.ModelAdmin):
class AvailabilityInline(admin.TabularInline):
model = Availability
extra = 0
-
+
class WriteoffCategoryInline(admin.TabularInline):
model = WriteoffCategory
extra = 0
diff --git a/alexia/apps/organization/forms.py b/alexia/apps/organization/forms.py
index abbb4d7..1effadb 100644
--- a/alexia/apps/organization/forms.py
+++ b/alexia/apps/organization/forms.py
@@ -1,5 +1,5 @@
from django import forms
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from alexia.apps.organization.models import Certificate
from alexia.apps.scheduling.models import Availability, BartenderAvailability
diff --git a/alexia/apps/organization/models.py b/alexia/apps/organization/models.py
index 5970f24..a96f613 100644
--- a/alexia/apps/organization/models.py
+++ b/alexia/apps/organization/models.py
@@ -6,7 +6,7 @@
from django.urls import reverse
from django.utils import timezone
from django.utils.text import slugify
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from alexia.apps.organization import managers
from alexia.apps.scheduling.models import Availability, BartenderAvailability
diff --git a/alexia/apps/organization/urls.py b/alexia/apps/organization/urls.py
index a906236..1fa8d72 100644
--- a/alexia/apps/organization/urls.py
+++ b/alexia/apps/organization/urls.py
@@ -1,19 +1,19 @@
-from django.conf.urls import url
+from django.urls import re_path, path
from . import views
urlpatterns = [
- url(r'^membership/$', views.MembershipListView.as_view(), name='memberships'),
- url(r'^membership/iva/$', views.IvaListView.as_view(), name='iva-memberships'),
- url(r'^membership/create/$', views.MembershipCreateView.as_view(), name='new-membership'),
- url(r'^membership/create/(?P[ms][0-9]{7})/$', views.UserCreateView.as_view(), name='add-membership'),
- url(r'^membership/(?P[0-9]+)/$', views.MembershipDetailView.as_view(), name='membership'),
- url(r'^membership/(?P[0-9]+)/update/$', views.MembershipUpdate.as_view(), name='edit-membership'),
- url(r'^membership/(?P[0-9]+)/delete/$', views.MembershipDelete.as_view(), name='delete-membership'),
- url(r'^membership/(?P[0-9]+)/iva/$', views.MembershipIvaView.as_view(), name='iva-membership'),
- url(r'^membership/(?P[0-9]+)/iva/upload/$', views.MembershipIvaUpdate.as_view(), name='upload-iva-membership'),
- url(r'^membership/(?P[0-9]+)/iva/approve/$', views.MembershipIvaApprove.as_view(),
+ path('membership/', views.MembershipListView.as_view(), name='memberships'),
+ path('membership/iva/', views.IvaListView.as_view(), name='iva-memberships'),
+ path('membership/create/', views.MembershipCreateView.as_view(), name='new-membership'),
+ re_path(r'^membership/create/(?P[ms][0-9]{7})/$', views.UserCreateView.as_view(), name='add-membership'),
+ path('membership//', views.MembershipDetailView.as_view(), name='membership'),
+ path('membership//update/', views.MembershipUpdate.as_view(), name='edit-membership'),
+ path('membership//delete/', views.MembershipDelete.as_view(), name='delete-membership'),
+ path('membership//iva/', views.MembershipIvaView.as_view(), name='iva-membership'),
+ path('membership//iva/upload/', views.MembershipIvaUpdate.as_view(), name='upload-iva-membership'),
+ path('membership//iva/approve/', views.MembershipIvaApprove.as_view(),
name='approve-iva-membership'),
- url(r'^membership/(?P[0-9]+)/iva/decline/$', views.MembershipIvaDecline.as_view(),
+ path('membership//iva/decline/', views.MembershipIvaDecline.as_view(),
name='decline-iva-membership'),
]
diff --git a/alexia/apps/profile/urls.py b/alexia/apps/profile/urls.py
index 9c73615..116804a 100644
--- a/alexia/apps/profile/urls.py
+++ b/alexia/apps/profile/urls.py
@@ -1,13 +1,13 @@
-from django.conf.urls import url
+from django.urls import path
from . import views
urlpatterns = [
- url(r'^$', views.ProfileView.as_view(), name='profile'),
- url(r'^update/$', views.ProfileUpdate.as_view(), name='edit-profile'),
- url(r'^ical/$', views.GenerateIcalView.as_view(), name='ical-gen-profile'),
- url(r'^iva/$', views.IvaView.as_view(), name='view-iva-profile'),
- url(r'^iva/update/$', views.IvaUpdate.as_view(), name='iva-profile'),
- url(r'^expenditures/$', views.ExpenditureListView.as_view(), name='expenditures-profile'),
- url(r'^expenditures/(?P\d+)/$', views.ExpenditureDetailView.as_view(), name='event-expenditures-profile')
+ path('', views.ProfileView.as_view(), name='profile'),
+ path('update/', views.ProfileUpdate.as_view(), name='edit-profile'),
+ path('ical/', views.GenerateIcalView.as_view(), name='ical-gen-profile'),
+ path('iva/', views.IvaView.as_view(), name='view-iva-profile'),
+ path('iva/update/', views.IvaUpdate.as_view(), name='iva-profile'),
+ path('expenditures/', views.ExpenditureListView.as_view(), name='expenditures-profile'),
+ path('expenditures//', views.ExpenditureDetailView.as_view(), name='event-expenditures-profile')
]
diff --git a/alexia/apps/scheduling/admin.py b/alexia/apps/scheduling/admin.py
index a5b7e25..6b818aa 100644
--- a/alexia/apps/scheduling/admin.py
+++ b/alexia/apps/scheduling/admin.py
@@ -1,5 +1,5 @@
from django.contrib import admin
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from alexia.apps.billing.models import TemporaryProduct
diff --git a/alexia/apps/scheduling/forms.py b/alexia/apps/scheduling/forms.py
index 5bb042d..205f8a7 100644
--- a/alexia/apps/scheduling/forms.py
+++ b/alexia/apps/scheduling/forms.py
@@ -1,6 +1,6 @@
from django import forms
from django.utils import timezone
-from django.utils.translation import ugettext, ugettext_lazy as _
+from django.utils.translation import gettext, gettext_lazy as _
from alexia.apps.organization.models import Location, Organization
from alexia.forms import AlexiaForm, AlexiaModelForm
@@ -32,7 +32,7 @@ def clean_ends_at(self):
ends_at = self.cleaned_data.get('ends_at')
if starts_at and ends_at and starts_at > ends_at:
raise forms.ValidationError(
- ugettext('The end time is earlier than the start time.'),
+ gettext('The end time is earlier than the start time.'),
code='invalid',
)
return ends_at
@@ -49,7 +49,7 @@ def clean_location(self):
conflicting_events = conflicting_events.exclude(pk=self.instance.pk)
if conflicting_events.exists():
raise forms.ValidationError(
- ugettext('There is already an event in %(location)s.'),
+ gettext('There is already an event in %(location)s.'),
code='conflicting_event',
params={'location': location},
)
diff --git a/alexia/apps/scheduling/models.py b/alexia/apps/scheduling/models.py
index c6fa855..6151dea 100644
--- a/alexia/apps/scheduling/models.py
+++ b/alexia/apps/scheduling/models.py
@@ -6,7 +6,7 @@
from django.db.models.signals import pre_save
from django.urls import reverse
from django.utils import timezone
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from .tools import notify_tenders
diff --git a/alexia/apps/scheduling/urls.py b/alexia/apps/scheduling/urls.py
index a686ab5..0536496 100644
--- a/alexia/apps/scheduling/urls.py
+++ b/alexia/apps/scheduling/urls.py
@@ -1,32 +1,31 @@
-from django.conf.urls import url
+from django.urls import re_path, path
from . import views
urlpatterns = [
- url(r'^$', views.event_list_view, name='event-list'),
- url(r'^bartender/$', views.EventBartenderView.as_view(), name='bartender-schedule'),
- url(r'^calendar/$', views.EventCalendarView.as_view(), name='calendar-schedule'),
- url(r'^calendar/fetch/$', views.EventCalendarFetch.as_view(), name='fetch-calendar-schedule'),
- url(r'^matrix/$', views.EventMatrixView.as_view(), name='event_matrix'),
+ path('', views.event_list_view, name='event-list'),
+ path('bartender/', views.EventBartenderView.as_view(), name='bartender-schedule'),
+ path('calendar/', views.EventCalendarView.as_view(), name='calendar-schedule'),
+ path('calendar/fetch/', views.EventCalendarFetch.as_view(), name='fetch-calendar-schedule'),
+ path('matrix/', views.EventMatrixView.as_view(), name='event_matrix'),
- url(r'^event/create/$', views.EventCreateView.as_view(), name='new-event'),
- url(r'^event/(?P\d+)/$', views.EventDetailView.as_view(), name='event'),
- url(r'^event/(?P\d+)/update/$', views.EventUpdateView.as_view(), name='edit-event'),
- url(r'^event/(?P\d+)/update/bartender_availability/(?P\d+)/$',
+ path('event/create/', views.EventCreateView.as_view(), name='new-event'),
+ path('event//', views.EventDetailView.as_view(), name='event'),
+ path('event//update/', views.EventUpdateView.as_view(), name='edit-event'),
+ path('event//update/bartender_availability//',
views.event_edit_bartender_availability, name='edit-event-bartender-availability'),
- url(r'^event/(?P\d+)/delete/$', views.EventDelete.as_view(), name='delete-event'),
+ path('event//delete/', views.EventDelete.as_view(), name='delete-event'),
- url(r'^mailtemplate/$', views.MailTemplateListView.as_view(), name='mailtemplate_list'),
- url(r'^mailtemplate/(?P[a-z]+)/$', views.MailTemplateDetailView.as_view(), name='mailtemplate_detail'),
- url(r'^mailtemplate/(?P[a-z]+)/update/$', views.MailTemplateUpdateView.as_view(),
+ path('mailtemplate/', views.MailTemplateListView.as_view(), name='mailtemplate_list'),
+ re_path(r'^mailtemplate/(?P[a-z]+)/$', views.MailTemplateDetailView.as_view(), name='mailtemplate_detail'),
+ re_path(r'^mailtemplate/(?P[a-z]+)/update/$', views.MailTemplateUpdateView.as_view(),
name='mailtemplate_update'),
- url(r'^availability/$', views.AvailabilityListView.as_view(), name='availability_list'),
- url(r'^availability/create/$', views.AvailabilityCreateView.as_view(), name='availability_create'),
- url(r'^availability/(?P\d+)/update/$', views.AvailabilityUpdateView.as_view(), name='availability_update'),
+ path('availability/', views.AvailabilityListView.as_view(), name='availability_list'),
+ path('availability/create/', views.AvailabilityCreateView.as_view(), name='availability_create'),
+ path('availability//update/', views.AvailabilityUpdateView.as_view(), name='availability_update'),
- # AJAT (Asynchroon Javascript en Tekst... wie gebruikt er nog in
- # hemelsnaam XML!?)
- url(r'^ajax/bartender_availability/$', views.set_bartender_availability),
- url(r'^ajax/bartender_availability/comment/$', views.set_bartender_availability_comment),
+ # AJAT (Asynchroon Javascript en Tekst... wie gebruikt er nog in hemelsnaam XML!?)
+ path('ajax/bartender_availability/', views.set_bartender_availability),
+ path('ajax/bartender_availability/comment/', views.set_bartender_availability_comment),
]
diff --git a/alexia/apps/scheduling/views.py b/alexia/apps/scheduling/views.py
index ffcace2..c9df8db 100644
--- a/alexia/apps/scheduling/views.py
+++ b/alexia/apps/scheduling/views.py
@@ -31,6 +31,7 @@
from .forms import EventForm, FilterEventForm
from .models import Availability, BartenderAvailability, Event, MailTemplate
+from ...utils.request import is_ajax
def event_list_view(request):
@@ -109,7 +110,7 @@ def event_list_view(request):
# Net als onze BartenderAvailabilities
bartender_availabilities = BartenderAvailability.objects.filter(
user_id=request.user.pk).values('event_id', 'availability_id', 'comment')
-
+
bartender_availabilities = {ba['event_id']: ba for ba in bartender_availabilities}
return render(request, 'scheduling/event_list.html', locals())
@@ -135,7 +136,7 @@ def get(self, request, *args, **kwargs):
start = request.GET.get('start', None)
end = request.GET.get('end', None)
- if not (start and end) or not request.is_ajax():
+ if not (start and end) or not is_ajax(request):
raise SuspiciousOperation('Bad calendar fetch request')
from_time = datetime.fromtimestamp(float(start), tz=timezone.utc)
@@ -337,10 +338,10 @@ def set_bartender_availability_comment(request):
if (request.organization not in event.participants.all()) or \
not request.user.profile.is_tender(request.organization):
raise PermissionDenied
-
- if not (request.method == 'POST' and request.is_ajax()):
+
+ if not (request.method == 'POST' and is_ajax(request)):
return HttpResponseBadRequest("NOTOK")
-
+
comment = request.POST.get('comment')
if len(comment) > 100:
return HttpResponseBadRequest("TOOLONG")
@@ -378,7 +379,7 @@ def set_bartender_availability(request):
request.user in event.get_assigned_bartenders():
raise PermissionDenied
- if request.method == 'POST' and request.is_ajax():
+ if request.method == 'POST' and is_ajax(request):
bartender_availability, is_new_record = \
BartenderAvailability.objects.get_or_create(
user=request.user, event=event,
diff --git a/alexia/auth/mixins.py b/alexia/auth/mixins.py
index b58df9a..72492f7 100644
--- a/alexia/auth/mixins.py
+++ b/alexia/auth/mixins.py
@@ -9,8 +9,10 @@
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.http.response import HttpResponseRedirect
from django.shortcuts import render
-from django.utils.http import urlquote
-from django.utils.translation import ugettext_lazy as _
+from urllib.parse import quote
+from django.utils.translation import gettext_lazy as _
+
+from alexia.utils.request import is_ajax
class PassesTestMixin(object):
@@ -31,7 +33,7 @@ def test_requirement(self, request):
return True
def dispatch(self, request, *args, **kwargs):
- url = u'%s?%s=%s' % (settings.LOGIN_URL, REDIRECT_FIELD_NAME, urlquote(request.get_full_path()))
+ url = u'%s?%s=%s' % (settings.LOGIN_URL, REDIRECT_FIELD_NAME, quote(request.get_full_path()))
if self.needs_login and not request.user.is_authenticated:
return HttpResponseRedirect(url)
else:
@@ -45,7 +47,7 @@ class RequireAjaxMixin(PassesTestMixin):
reason = _('AJAX-request required')
def test_requirement(self, request):
- return request.is_ajax()
+ return is_ajax(request)
class TenderRequiredMixin(PassesTestMixin):
diff --git a/alexia/conf/settings/base.py b/alexia/conf/settings/base.py
index a8a4e21..352cb81 100644
--- a/alexia/conf/settings/base.py
+++ b/alexia/conf/settings/base.py
@@ -1,6 +1,6 @@
import os
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
BASE_DIR = os.path.normpath(os.path.join(os.path.abspath(__file__), '..', '..', '..', '..'))
@@ -74,7 +74,7 @@
'crispy_forms',
'crispy_bootstrap3',
'debug_toolbar',
- 'jsonrpc',
+ 'modernrpc',
'wkhtmltopdf',
# OIDC Client (authentication via auth.ia)
@@ -150,6 +150,14 @@
# See https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+# ModernRPC API
+MODERNRPC_HANDLERS = [
+ "alexia.api.handlers.AlexiaJSONRPCHandler"
+]
+MODERNRPC_METHODS_MODULES = [
+ 'alexia.api.v1.methods'
+]
+
# Single Sign On via https://auth.ia.utwente.nl/
OIDC_OP_AUTHORIZATION_ENDPOINT = "https://auth.ia.utwente.nl/realms/inter-actief/protocol/openid-connect/auth"
OIDC_OP_TOKEN_ENDPOINT = "https://auth.ia.utwente.nl/realms/inter-actief/protocol/openid-connect/token"
diff --git a/alexia/conf/settings/environ.py b/alexia/conf/settings/environ.py
index a82e19b..30de237 100644
--- a/alexia/conf/settings/environ.py
+++ b/alexia/conf/settings/environ.py
@@ -155,6 +155,8 @@ def get_random_secret_key_no_dollar():
},
# Set OIDC logging to at least info due to process_request log flooding
'mozilla_django_oidc.middleware': {'level': 'INFO'},
+ # Set ModernRPC logging to at least info due to "register_method" log flooding for API
+ 'modernrpc.core': {'level': 'INFO'},
},
}
diff --git a/alexia/conf/urls.py b/alexia/conf/urls.py
index cd23f5e..71707c7 100644
--- a/alexia/conf/urls.py
+++ b/alexia/conf/urls.py
@@ -1,8 +1,8 @@
from django.conf import settings
-from django.conf.urls import include, url
+from django.conf.urls import include
from django.contrib import admin
from django.contrib.auth import views as auth_views
-from django.urls import re_path
+from django.urls import re_path, path
from django.views.generic import RedirectView, TemplateView
from django.views.static import serve
@@ -13,56 +13,56 @@
urlpatterns = [
# Root
- url(r'^$', RedirectView.as_view(pattern_name='event-list', permanent=True)),
+ path(r'', RedirectView.as_view(pattern_name='event-list', permanent=True)),
# Short urls to 'subsystems'
- url(r'^dcf/(?P\d+)/$', dcf, name='dcf'),
- url(r'^dcf/(?P\d+)/check/$', complete_dcf, name='dcf-complete'),
- url(r'^juliana/(?P\d+)/$', JulianaView.as_view(), name='juliana'),
+ path('dcf//', dcf, name='dcf'),
+ path('dcf//check/', complete_dcf, name='dcf-complete'),
+ path('juliana//', JulianaView.as_view(), name='juliana'),
# Apps
- url(r'^billing/', include('alexia.apps.billing.urls')),
- url(r'^consumption/', include('alexia.apps.consumption.urls')),
- url(r'^organization/', include('alexia.apps.organization.urls')),
- url(r'^profile/', include('alexia.apps.profile.urls')),
- url(r'^scheduling/', include('alexia.apps.scheduling.urls')),
- url(r'^ical$', scheduling_views.ical),
- url(r'^ical/(?P[^/]+)$', scheduling_views.personal_ical, name='ical'),
+ path('billing/', include('alexia.apps.billing.urls')),
+ path('consumption/', include('alexia.apps.consumption.urls')),
+ path('organization/', include('alexia.apps.organization.urls')),
+ path('profile/', include('alexia.apps.profile.urls')),
+ path('scheduling/', include('alexia.apps.scheduling.urls')),
+ path('ical', scheduling_views.ical),
+ path('ical/', scheduling_views.personal_ical, name='ical'),
- url(r'^api/', include('alexia.api.urls')),
+ path('api/', include('alexia.api.urls')),
# "Static" general_views
- url(r'^healthz/$', general_views.healthz_view, name='healthz_simple'),
- url(r'^about/$', general_views.AboutView.as_view(), name='about'),
- url(r'^help/$', general_views.HelpView.as_view(), name='help'),
- url(r'^login_complete/$', general_views.login_complete, name='login_complete'),
- url(r'^legacy_login/$', general_views.login, name='login'),
- url(r'^legacy_logout/$', auth_views.LogoutView.as_view(), name='logout'),
- url(r'^register/$', general_views.RegisterView.as_view(), name='register'),
- url(r'^change_current_organization/(?P[-\w]+)/$',
+ path('healthz/', general_views.healthz_view, name='healthz_simple'),
+ path('about/', general_views.AboutView.as_view(), name='about'),
+ path('help/', general_views.HelpView.as_view(), name='help'),
+ path('login_complete/', general_views.login_complete, name='login_complete'),
+ path('legacy_login/', general_views.login, name='login'),
+ path('legacy_logout/', auth_views.LogoutView.as_view(), name='logout'),
+ path('register/', general_views.RegisterView.as_view(), name='register'),
+ path('change_current_organization//',
general_views.ChangeCurrentOrganizationView.as_view(), name='change-current-organization'),
- url(r'^oidc/', include('mozilla_django_oidc.urls')),
+ path('oidc/', include('mozilla_django_oidc.urls')),
# Django Admin
- url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
- url(r'^admin/', admin.site.urls),
+ path('admin/doc/', include('django.contrib.admindocs.urls')),
+ path('admin/', admin.site.urls),
# Internationalization
- url(r'^i18n/', include('django.conf.urls.i18n')),
+ path('i18n/', include('django.conf.urls.i18n')),
# Robots
- url(r'^robots\.txt$', TemplateView.as_view(template_name='robots.txt', content_type='text/plain')),
+ path('robots.txt', TemplateView.as_view(template_name='robots.txt', content_type='text/plain')),
]
# Debug toolbar
if settings.DEBUG:
import debug_toolbar
urlpatterns += [
- url(r'^__debug__/', include(debug_toolbar.urls)),
+ path('__debug__/', include(debug_toolbar.urls)),
]
# Translation application for development
urlpatterns += [
- url(r'^translations/', include('rosetta.urls'), name='translations')
+ path('translations/', include('rosetta.urls'), name='translations')
]
# Static and media files in development mode
urlpatterns += [
diff --git a/alexia/core/validators.py b/alexia/core/validators.py
index f04ff70..c270577 100644
--- a/alexia/core/validators.py
+++ b/alexia/core/validators.py
@@ -1,5 +1,5 @@
from django.core.validators import RegexValidator
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
color_validator = RegexValidator(
r'^[0-9a-zA-Z]{6}$',
diff --git a/alexia/forms/mixins.py b/alexia/forms/mixins.py
index 5f9c333..2fe5afb 100644
--- a/alexia/forms/mixins.py
+++ b/alexia/forms/mixins.py
@@ -1,7 +1,7 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from django.forms import Form, ModelForm
-from django.utils.translation import ugettext as _
+from django.utils.translation import gettext as _
class BaseCrispyFormMixin(object):
diff --git a/alexia/test/testcases.py b/alexia/test/testcases.py
index 0f118e2..807e331 100644
--- a/alexia/test/testcases.py
+++ b/alexia/test/testcases.py
@@ -203,7 +203,7 @@ def send_request(self, method, params):
:param params: Parameters for JSON RPC call.
:rtype : django.http.response.HttpResponse
"""
- path = reverse('api_v1_mountpoint')
+ path = reverse('jsonrpc_mountpoint')
req = {
'jsonrpc': '1.0',
@@ -225,7 +225,7 @@ def send_and_compare_request(self, method, params, expected_result):
response = self.send_request(method, params)
- self.assertEqual(response['Content-Type'], 'application/json-rpc')
+ self.assertEqual(response['Content-Type'], 'application/json')
content = response.content.decode('utf-8')
@@ -238,14 +238,13 @@ def send_and_compare_request(self, method, params, expected_result):
self.assertJSONEqual(content, expected_data)
- def send_and_compare_request_error(self, method, params, error_code, error_name, error_message, error_data=None,
+ def send_and_compare_request_error(self, method, params, error_code, error_message, error_data=None,
status_code=200):
"""
Send JSON RPC method call and compare actual error result with expected error result.
:param method: Name of method to call.
:param params: Parameters for JSON RPC call.
:param error_code: Expected error code.
- :param error_name: Expected error name.
:param error_message: Expected error message.
:param error_data: Expected error data.
:param status_code: Expected HTTP status code.
@@ -257,7 +256,7 @@ def send_and_compare_request_error(self, method, params, error_code, error_name,
self.assertEqual(response.status_code, status_code, 'HTTP status code')
- self.assertEqual(response['Content-Type'], 'application/json-rpc')
+ self.assertEqual(response['Content-Type'], 'application/json')
content = response.content.decode('utf-8')
@@ -266,11 +265,11 @@ def send_and_compare_request_error(self, method, params, error_code, error_name,
'id': 'jsonrpc',
'error': {
'code': error_code,
- 'name': error_name,
'message': error_message,
- 'data': error_data,
},
'result': None,
}
+ if error_data is not None:
+ expected_data['error']['data'] = error_data
self.assertJSONEqual(content, expected_data, 'JSON RPC result')
diff --git a/alexia/utils/request.py b/alexia/utils/request.py
new file mode 100644
index 0000000..ec620b5
--- /dev/null
+++ b/alexia/utils/request.py
@@ -0,0 +1,7 @@
+def is_ajax(request):
+ """
+ Copied from Django because HttpRequest.is_ajax() was deprecated in 3.1 and removed in 4.x
+ See: https://docs.djangoproject.com/en/3.2/ref/request-response/#django.http.HttpRequest.is_ajax
+ and: https://docs.djangoproject.com/en/3.2/_modules/django/http/request/#HttpRequest.is_ajax
+ """
+ return request.headers.get('X-Requested-With') == 'XMLHttpRequest'
diff --git a/requirements.txt b/requirements.txt
index 9ee7df9..3a7365a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,20 +1,21 @@
-Django>=3.2.25,<3.3
-django-compressor>=4.4,<4.5
-django-crispy-forms>=2.0,<2.1
+Django>=4.2.20,<4.3
+django-compressor>=4.5.1,<4.6
+django-crispy-forms>=2.4,<2.5
crispy-bootstrap3>=2024.1,<2024.2
-django-debug-toolbar>=4.3.0,<4.4
+django-debug-toolbar>=5.1.0,<5.2
django-wkhtmltopdf>=3.4.0,<3.5
-mysqlclient>=2.2.7,<2.3
+mysqlclient>=2.1.1
icalendar==4.1.0
jsonfield>=3.1.0,<3.2
-git+https://github.com/samuraisam/django-json-rpc@a88d744d960e828f3eb21265da0f10a694b8ebcf
+# Django Modern RPC -- For our JSONRPC API
+django-modern-rpc>=1.0.1,<1.1
raven==6.10.0
# Translations -- development
-django-rosetta>=0.9.8,<0.10.0
+django-rosetta>=0.10.2,<0.11.0
# Django-extensions (runserver_plus command) -- development
-django-extensions>=3.1.5,<4.0
+django-extensions>=4.1,<4.2
# Werkzeug needed for runserver_plus command) -- development
-Werkzeug>=3.0,<3.1
+Werkzeug>=3.1,<3.2
# Single Sign On - OIDC Client
mozilla-django-oidc>=4.0.1,<4.1
# Sentry error logging
From 2f39c56ad0469eac922a1076a426e27fea909f4c Mon Sep 17 00:00:00 2001
From: Kevin Alberts
Date: Mon, 12 May 2025 20:05:41 +0200
Subject: [PATCH 2/2] Update API docs for new generated docs page of ModernRPC
---
alexia/api/urls.py | 2 +-
alexia/api/v1/methods/authorization.py | 122 ++++++++++-----
alexia/api/v1/methods/billing.py | 197 +++++++++++++++----------
alexia/api/v1/methods/event.py | 41 +++--
alexia/api/v1/methods/generic.py | 70 +++++++--
alexia/api/v1/methods/juliana.py | 26 +++-
alexia/api/v1/methods/organization.py | 80 +++++++---
alexia/api/v1/methods/rfid.py | 135 +++++++++++------
alexia/api/v1/methods/scheduling.py | 45 +++---
alexia/api/v1/methods/user.py | 190 +++++++++++++++++-------
alexia/conf/settings/base.py | 8 +-
requirements.txt | 2 +
templates/api/doc.html | 39 +----
templates/api/info.html | 3 +-
templates/api/v1/doc.html | 2 +-
15 files changed, 651 insertions(+), 311 deletions(-)
diff --git a/alexia/api/urls.py b/alexia/api/urls.py
index 3994c32..cb99510 100644
--- a/alexia/api/urls.py
+++ b/alexia/api/urls.py
@@ -9,5 +9,5 @@
path('', APIInfoView.as_view(), name='api'),
path('1/', RPCEntryPoint.as_view(protocol=Protocol.JSON_RPC, entry_point="v1"), name="jsonrpc_mountpoint"),
- path('1/doc/', RPCEntryPoint.as_view(enable_doc=True, enable_rpc=False, template_name="api/v1/doc.html", entry_point="v1")),
+ path('1/doc/', RPCEntryPoint.as_view(enable_doc=True, enable_rpc=False, template_name="api/v1/doc.html", entry_point="v1"), name="jsonrpc_docs"),
]
diff --git a/alexia/api/v1/methods/authorization.py b/alexia/api/v1/methods/authorization.py
index 28b6027..a8d11c0 100644
--- a/alexia/api/v1/methods/authorization.py
+++ b/alexia/api/v1/methods/authorization.py
@@ -17,27 +17,40 @@
@manager_required
def authorization_list(radius_username: Optional[str] = None, **kwargs) -> List[Dict]:
"""
- Retrieve registered authorizations for the current selected organization.
+ **Signature**: `authorization.list(radius_username)`
+
+ **Arguments**:
+
+ - `radius_username` : `str` -- *(optional)* Username to search for.
+
+ **Return type**: List of `dict`
+
+ **Idempotent**: yes
+
+ **Required user level**: Manager
- Required user level: Manager
+ **Documentation**:
+
+ Retrieve registered authorizations for the current selected organization.
Provide radius_username to select only authorizations of the provided user.
Returns an array of accounts of registered authorizations.
- radius_username -- (optional) Username to search for.
+ **Example return value**:
- Example return value:
- [
- {
+ [
+ {
"id": 1,
"end_date": null,
"start_date": "2014-09-21T14:16:06+00:00",
"user": "s0000000"
- }
- ]
+ }
+ ]
+
+ **Raises errors**:
- Raises error -32602 (Invalid params) if the username does not exist.
+ - `-32602` (Invalid params) if the username does not exist.
"""
request = kwargs.get(REQUEST_KEY)
result = []
@@ -64,25 +77,38 @@ def authorization_list(radius_username: Optional[str] = None, **kwargs) -> List[
@manager_required
def authorization_get(radius_username: str, **kwargs) -> List[Dict]:
"""
+ **Signature**: `authorization.get(radius_username)`
+
+ **Arguments**:
+
+ - `radius_username` : `str` -- Username to search for.
+
+ **Return type**: `dict`
+
+ **Idempotent**: yes
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
Retrieve registered authorizations for a specified user and current selected
organization.
- Required user level: Manager
-
Returns an array of accounts of registered authorizations.
- radius_username -- Username to search for.
+ **Example return value**:
- Example return value:
- [
- {
+ [
+ {
"id": 1,
"end_date": null,
"start_date": "2014-09-21T14:16:06+00:00"
- }
- ]
+ }
+ ]
- Raises error -32602 (Invalid params) if the username does not exist.
+ **Raises errors**:
+
+ - `-32602` (Invalid params) if the username does not exist.
"""
request = kwargs.get(REQUEST_KEY)
result = []
@@ -110,23 +136,37 @@ def authorization_get(radius_username: str, **kwargs) -> List[Dict]:
@transaction.atomic
def authorization_add(radius_username: str, account: str, **kwargs) -> Dict:
"""
- Add a new authorization to the specified user.
+ **Signature**: `authorization.add(radius_username, account)`
+
+ **Arguments**:
+
+ - `radius_username` : `str` -- Username to search for.
+ - `account` : `str` -- Unused
+
+ **Return type**: `dict`
- Required user level: Manager
+ **Idempotent**: no
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
+ Add a new authorization to the specified user.
Returns the authorization on success.
- radius_username -- Username to search for.
+ **Example return value**:
+
+ {
+ "id": 1,
+ "end_date": null,
+ "start_date": "2014-09-21T14:16:06+00:00",
+ "user": "s0000000"
+ }
- Example return value:
- {
- "id": 1,
- "end_date": null,
- "start_date": "2014-09-21T14:16:06+00:00",
- "user": "s0000000"
- }
+ **Raises errors**:
- Raises error -32602 (Invalid params) if the username does not exist.
+ - `-32602` (Invalid params) if the username does not exist.
"""
request = kwargs.get(REQUEST_KEY)
try:
@@ -146,17 +186,29 @@ def authorization_add(radius_username: str, account: str, **kwargs) -> Dict:
@transaction.atomic
def authorization_end(radius_username: str, authorization_id: int, **kwargs) -> bool:
"""
- End an authorization from the specified user.
+ **Signature**: `authorization.end(radius_username, authorization_id)`
+
+ **Arguments**:
+
+ - `radius_username` : `str` -- Username to search for.
+ - `authorization_id` : `int` -- ID of the authorization to end
- Required user level: Manager
+ **Return type**: `bool`
+
+ **Idempotent**: no
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
+ End an authorization from the specified user.
Returns true when successful. Returns false when the authorization was already ended.
- radius_username -- Username to search for.
- identifier -- RFID card hardware identifier (max. 16 chars)
+ **Raises errors**:
- Raises error -32602 (Invalid params) if the username does not exist.
- Raises error -32602 (Invalid params) if provided authorization cannot be found.
+ - `-32602` (Invalid params) if the username does not exist.
+ - `-32602` (Invalid params) if provided authorization cannot be found.
"""
request = kwargs.get(REQUEST_KEY)
try:
diff --git a/alexia/api/v1/methods/billing.py b/alexia/api/v1/methods/billing.py
index 63db470..4c9186c 100644
--- a/alexia/api/v1/methods/billing.py
+++ b/alexia/api/v1/methods/billing.py
@@ -16,18 +16,31 @@
@manager_required
def order_unsynchronized(unused: int = 0, **kwargs) -> List[Dict]:
"""
- Return a list of unsynchronized orders.
+ **Signature**: `order.unsynchronized(unused)`
+
+ **Arguments**:
+
+ - `unused` : `int` -- Unused parameter
+
+ **Return type**: List of `dict`
+
+ **Idempotent**: yes
- Required user level: Manager
+ **Required user level**: Manager
+
+ **Documentation**:
+
+ Return a list of unsynchronized orders.
Optionally gets an unused parameter because of a former compatibility issue
between the jsonrpc server en jsonrpclib client.
Returns a list of Order objects.
- Example return value:
- [
- {
+ **Example return value**:
+
+ [
+ {
"purchases": [
{
"price": "5.00",
@@ -54,8 +67,8 @@ def order_unsynchronized(unused: int = 0, **kwargs) -> List[Dict]:
"start_date": "2014-09-21T14:16:06+00:00",
"user": "s0000000"
}
- },
- {
+ },
+ {
"purchases": [
{
"price": "1.00",
@@ -81,8 +94,8 @@ def order_unsynchronized(unused: int = 0, **kwargs) -> List[Dict]:
"start_date": "2014-09-21T14:16:06+00:00",
"user": "s0000000"
}
- }
- ]
+ }
+ ]
"""
request = kwargs.get(REQUEST_KEY)
result = []
@@ -100,44 +113,57 @@ def order_unsynchronized(unused: int = 0, **kwargs) -> List[Dict]:
@manager_required
def order_get(order_id: int, **kwargs) -> Dict:
"""
- Return a specific order.
+ **Signature**: `order.get(order_id)`
- Required user level: Manager
+ **Arguments**:
- Returns an order object.
+ - `order_id` : `int` -- ID of the Order object.
+
+ **Return type**: `dict`
- order_id -- ID of the Order object.
+ **Idempotent**: yes
- Raises error 404 if provided order id cannot be found.
+ **Required user level**: Manager
+
+ **Documentation**:
+
+ Return a specific order.
+
+ Returns an order object.
- Example return value:
- {
- "purchases": [
+ **Example return value**:
+
+ {
+ "purchases": [
{
- "price": "1.00",
- "product": {"name": "Coca Cola"},
- "amount": 2
+ "price": "1.00",
+ "product": {"name": "Coca-Cola"},
+ "amount": 2
}, {
- "price": "0.50",
- "product": {"name": "Grolsch"},
- "amount": 1
+ "price": "0.50",
+ "product": {"name": "Grolsch"},
+ "amount": 1
}
- ],
- "synchronized": false,
- "event": {
+ ],
+ "synchronized": false,
+ "event": {
"id": 4210,
"name": "Testborrel"
- },
- "placed_at": "2015-03-11T15:24:06+00:00",
- "id": 1255,
- "rfid": "02,06:65:74:49",
- "authorization": {
+ },
+ "placed_at": "2015-03-11T15:24:06+00:00",
+ "id": 1255,
+ "rfid": "02,06:65:74:49",
+ "authorization": {
"id": 1,
"end_date": null,
"start_date": "2014-09-21T14:16:06+00:00",
"user": "s0000000"
+ }
}
- }
+
+ **Raises errors**:
+
+ - `404` (Object not found) if provided order id cannot be found.
"""
request = kwargs.get(REQUEST_KEY)
try:
@@ -152,69 +178,80 @@ def order_get(order_id: int, **kwargs) -> Dict:
@manager_required
def order_list(radius_username: Optional[str] = None, **kwargs) -> List[Dict]:
"""
- Retrieve a list of orders for the currently selected organization.
+ **Signature**: `order.list(radius_username)`
- Required user level: Manager
+ **Arguments**:
- Povide a username to select only orders made by the provided user.
+ - `radius_username` : `str` -- *(optional)* Username to search for.
+
+ **Return type**: List of `dict`
+
+ **Idempotent**: yes
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
+ Retrieve a list of orders for the currently selected organization.
+
+ Provide a username to select only orders made by the provided user.
Returns an array of orders.
- radius_username -- (optional) Username to search for.
+ **Example return value**:
- Example return value:
- [
- {
+ [
+ {
"purchases": [
- {
- "price": "1.00",
- "product": {"name": "Coca-Cola"},
- "amount": 2
- }, {
- "price": "0.50",
- "product": {"name": "Grolsch"},
- "amount": 1
- }
+ {
+ "price": "1.00",
+ "product": {"name": "Coca-Cola"},
+ "amount": 2
+ }, {
+ "price": "0.50",
+ "product": {"name": "Grolsch"},
+ "amount": 1
+ }
],
"synchronized": false,
"event": {
- "id": 4210,
- "name": "Testborrel"
+ "id": 4210,
+ "name": "Testborrel"
},
"placed_at": "2015-03-11T15:24:06+00:00",
"id": 1255,
"rfid": "02,06:65:74:49",
"authorization": {
- "id": 1,
- "end_date": null,
- "start_date": "2014-09-21T14:16:06+00:00",
- "user": "s0000000"
+ "id": 1,
+ "end_date": null,
+ "start_date": "2014-09-21T14:16:06+00:00",
+ "user": "s0000000"
}
- },
- {
+ },
+ {
"purchases": [
- {
- "price": "1.50",
- "product": {"name": "Grolsch"},
- "amount": 3
- }
+ {
+ "price": "1.50",
+ "product": {"name": "Grolsch"},
+ "amount": 3
+ }
],
"synchronized": true,
"event": {
- "id": 4210,
- "name": "Testborrel"
+ "id": 4210,
+ "name": "Testborrel"
},
"placed_at": "2015-03-11T16:47:21+00:00",
"id": 1271,
"rfid": "02,06:65:74:49",
"authorization": {
- "id": 1,
- "end_date": null,
- "start_date": "2014-09-21T14:16:06+00:00",
- "user": "s0000000"
+ "id": 1,
+ "end_date": null,
+ "start_date": "2014-09-21T14:16:06+00:00",
+ "user": "s0000000"
}
- }
- ]
+ }
+ ]
"""
request = kwargs.get(REQUEST_KEY)
result = []
@@ -241,15 +278,27 @@ def order_list(radius_username: Optional[str] = None, **kwargs) -> List[Dict]:
@transaction.atomic
def order_marksynchronized(order_id: int, **kwargs) -> bool:
"""
- Mark an order as synchronized.
+ **Signature**: `order.marksynchronized(order_id)`
+
+ **Arguments**:
+
+ - `order_id` : `int` -- ID of the Order object.
+
+ **Return type**: `bool`
- Required user level: Manager
+ **Idempotent**: no
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
+ Mark an order as synchronized.
- Returns True if the operation succeeded. Returns False if the order is already marked as synchronized.
+ Returns `True` if the operation succeeded. Returns `False` if the order is already marked as synchronized.
- order_id -- ID of the Order object.
+ **Raises errors**:
- Raises error 422 if provided order id cannot be found.
+ - `-32602` (Invalid params) if provided order id cannot be found.
"""
request = kwargs.get(REQUEST_KEY)
try:
diff --git a/alexia/api/v1/methods/event.py b/alexia/api/v1/methods/event.py
index 9a2a230..32b976d 100644
--- a/alexia/api/v1/methods/event.py
+++ b/alexia/api/v1/methods/event.py
@@ -9,25 +9,38 @@
@rpc_method(name='event.upcoming_list', entry_point='v1')
def upcoming_events_list(include_ongoing: bool = False, **kwargs) -> List[Dict]:
"""
- List all current and upcoming events.
+ **Signature**: `event.upcoming_list(include_current)`
+
+ **Arguments**:
+
+ - `include_current` : `bool` -- Whether to include ongoing events, defaults to false
+
+ **Return type**: List of `dict`
- Required user level: None
+ **Idempotent**: yes
- include_current -- Whether to include ongoing events, defaults to false
+ **Required user level**: *None*
+
+ **Documentation**:
+
+ List all current and upcoming events.
Returns an array with zero or more events.
- Example output:
- [{
- 'name': 'Test Drink',
- 'locations': 'Abscint',
- 'organizer': 'Inter-Actief',
- 'participants': ['Inter-Actief'],
- 'starts_at': 2017-09-12T14:00:00Z,
- 'ends_at': 2017-09-12T16:00:00Z,
- 'kegs': 2,
- 'is_risky': false,
- }]
+ **Example return value**:
+
+ [
+ {
+ 'name': 'Test Drink',
+ 'locations': 'Abscint',
+ 'organizer': 'Inter-Actief',
+ 'participants': ['Inter-Actief'],
+ 'starts_at': 2017-09-12T14:00:00Z,
+ 'ends_at': 2017-09-12T16:00:00Z,
+ 'kegs': 2,
+ 'is_risky': false,
+ }
+ ]
"""
filter_key = 'ends_at__gte' if include_ongoing else 'starts_at__gte'
return [{
diff --git a/alexia/api/v1/methods/generic.py b/alexia/api/v1/methods/generic.py
index bb1118c..33226fc 100644
--- a/alexia/api/v1/methods/generic.py
+++ b/alexia/api/v1/methods/generic.py
@@ -7,9 +7,21 @@
@rpc_method(name='version', entry_point='v1')
def version(**kwargs) -> int:
"""
- Returns the current API version.
+ **Signature**: `version()`
+
+ **Arguments**:
+
+ - *None*
+
+ **Return type**: `int`
+
+ **Idempotent**: yes
- Required user level: None
+ **Required user level**: *None*
+
+ **Documentation**:
+
+ Returns the current API version.
The client will then be able to determine which methods it can use and
which methods it cannot use.
@@ -23,9 +35,21 @@ def version(**kwargs) -> int:
@rpc_method(name='methods', entry_point='v1')
def methods(**kwargs) -> List[str]:
"""
- Introspect the API and return all callable methods.
+ **Signature**: `methods()`
- Required user level: None
+ **Arguments**:
+
+ - *None*
+
+ **Return type**: List of `str`
+
+ **Idempotent**: yes
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
+ Introspect the API and return all callable methods.
Returns an array with the methods.
"""
@@ -39,15 +63,25 @@ def methods(**kwargs) -> List[str]:
@transaction.atomic
def login(username: str, password: str, **kwargs) -> bool:
"""
- Authenticate a user to use the API.
+ **Signature**: `login()`
- Required user level: None
+ **Arguments**:
- Returns true when a user has successfully signed in. A session will be
- started and stored. Cookies must be supported by the client.
+ - `username` : `str` -- Username of user.
+ - `password` : `str` -- Password of the user.
+
+ **Return type**: `bool`
+
+ **Idempotent**: no
+
+ **Required user level**: *None*
- username -- Username of user
- password -- Password of the user
+ **Documentation**:
+
+ Authenticate a user to use the API.
+
+ Returns `True` when a user has successfully signed in. A session will be
+ started and stored. Cookies must be supported by the client.
"""
from django.contrib.auth import authenticate, login
request = kwargs.get(REQUEST_KEY)
@@ -68,9 +102,21 @@ def login(username: str, password: str, **kwargs) -> bool:
@transaction.atomic
def logout(**kwargs) -> None:
"""
- Sign out the current user, even if no one was signed in.
+ **Signature**: `logout()`
+
+ **Arguments**:
+
+ - *None*
+
+ **Return type**: `None`
- Required user level: None
+ **Idempotent**: no
+
+ **Required user level**: *None*
+
+ **Documentation**:
+
+ Sign out the current user, even if no one was signed in.
Destroys the current session.
"""
diff --git a/alexia/api/v1/methods/juliana.py b/alexia/api/v1/methods/juliana.py
index ab6f8e5..1400df5 100644
--- a/alexia/api/v1/methods/juliana.py
+++ b/alexia/api/v1/methods/juliana.py
@@ -82,6 +82,11 @@ def _get_validate_event(request, event_id, safe=False):
@rpc_method(name='juliana.rfid.get', entry_point='v1')
@login_required
def juliana_rfid_get(event_id: int, rfid: Dict, **kwargs) -> Dict:
+ """
+ Internal API method for the Point of Sale module.
+
+ *No documentation available yet.*
+ """
request = kwargs.get(REQUEST_KEY)
event = _get_validate_event(request, event_id, True)
@@ -115,7 +120,13 @@ def juliana_rfid_get(event_id: int, rfid: Dict, **kwargs) -> Dict:
@login_required
@transaction.atomic
def juliana_order_save(event_id: int, user_id: int, purchases: List[Dict], rfid_data: Dict, **kwargs) -> None:
- """Saves a new order in the database"""
+ """
+ Internal API method for the Point of Sale module.
+
+ Saves a new order in the database
+
+ *No documentation available yet.*
+ """
request = kwargs.get(REQUEST_KEY)
event = _get_validate_event(request, event_id)
@@ -177,6 +188,11 @@ def juliana_order_save(event_id: int, user_id: int, purchases: List[Dict], rfid_
@rpc_method(name='juliana.user.check', entry_point='v1')
@login_required
def juliana_user_check(event_id: int, user_id: int, **kwargs) -> int:
+ """
+ Internal API method for the Point of Sale module.
+
+ *No documentation available yet.*
+ """
request = kwargs.get(REQUEST_KEY)
event = _get_validate_event(request, event_id, True)
@@ -198,7 +214,13 @@ def juliana_user_check(event_id: int, user_id: int, **kwargs) -> int:
@login_required
@transaction.atomic
def juliana_writeoff_save(event_id: int, writeoff_id: int, purchases: List[Dict], **kwargs) -> bool:
- """Saves a writeoff order in the Database"""
+ """
+ Internal API method for the Point of Sale module.
+
+ Saves a writeoff order in the Database
+
+ *No documentation available yet.*
+ """
request = kwargs.get(REQUEST_KEY)
event = _get_validate_event(request, event_id)
diff --git a/alexia/api/v1/methods/organization.py b/alexia/api/v1/methods/organization.py
index eee6901..0c7393a 100644
--- a/alexia/api/v1/methods/organization.py
+++ b/alexia/api/v1/methods/organization.py
@@ -11,16 +11,29 @@
@login_required
def organization_current_get(**kwargs) -> Optional[str]:
"""
- Return the current organization slug.
+ **Signature**: `organization.current.get()`
+
+ **Arguments**:
+
+ - *None*
+
+ **Return type**: *(optional)* `str`
- Required user level: None
+ **Idempotent**: yes
+
+ **Required user level**: *None*
+
+ **Documentation**:
+
+ Return the current organization slug.
All operations performed will be performed by this organization.
- If no organization has been chosen, it will return None.
+ If no organization has been chosen, it will return `None`.
+
+ **Example return value**:
- Example return value:
- "inter-actief"
+ "inter-actief"
"""
request = kwargs.get(REQUEST_KEY)
if request.organization:
@@ -33,18 +46,30 @@ def organization_current_get(**kwargs) -> Optional[str]:
@login_required
def organization_current_set(organization: str, **kwargs) -> bool:
"""
- Set the current organization.
+ **Signature**: `organization.current.set(organization)`
+
+ **Arguments**:
+
+ - `organization` : `str` -- slug of the organization or empty string to deselect organization.
+
+ **Return type**: `bool`
- Required user level: None
+ **Idempotent**: no
+
+ **Required user level**: *None*
+
+ **Documentation**:
+
+ Set the current organization.
All further operations performed will be performed by this organization.
- Return true if the organization is switched. Returns false if the current
+ Return `True` if the organization is switched. Returns `False` if the current
organization equals the provided organization.
- organization -- slug of the organization or empty string to deselect organization.
+ **Raises errors**:
- Raises error 404 if provided organization cannot be found.
+ - `404` (Object not found) if provided organization cannot be found.
"""
request = kwargs.get(REQUEST_KEY)
if not organization:
@@ -70,20 +95,33 @@ def organization_current_set(organization: str, **kwargs) -> bool:
@login_required
def organization_list(**kwargs) -> List[str]:
"""
- List all public organizations.
+ **Signature**: `organization.list()`
+
+ **Arguments**:
+
+ - *None*
- Required user level: None
+ **Return type**: List of `str`
+
+ **Idempotent**: no
+
+ **Required user level**: *None*
+
+ **Documentation**:
+
+ List all public organizations.
Returns an array with zero or more organizations.
- Example return value:
- [
- "abacus",
- "inter-actief",
- "proto",
- "scintilla",
- "sirius",
- "stress"
- ]
+ **Example return value**:
+
+ [
+ "abacus",
+ "inter-actief",
+ "proto",
+ "scintilla",
+ "sirius",
+ "stress"
+ ]
"""
return [o.slug for o in Organization.objects.all()]
diff --git a/alexia/api/v1/methods/rfid.py b/alexia/api/v1/methods/rfid.py
index 8cb13ee..f5d95db 100644
--- a/alexia/api/v1/methods/rfid.py
+++ b/alexia/api/v1/methods/rfid.py
@@ -16,39 +16,50 @@
@manager_required
def rfid_list(radius_username: str = None, **kwargs) -> List[Dict]:
"""
- Retrieve registered RFID cards for the current selected organization.
+ **Signature**: `rfid.list(radius_username)`
+
+ **Arguments**:
+
+ - `radius_username` : `str` -- *(optional)* Username to search for.
- Required user level: Manager
+ **Return type**: List of `dict`
- Provide radius_username to select only RFID cards registered by the provided user.
+ **Idempotent**: yes
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
+ Retrieve registered RFID cards for the current selected organization.
+
+ Provide `radius_username` to select only RFID cards registered by the provided user.
Returns an array of registered RFID cards.
- radius_username -- (optional) Username to search for.
+ **Example return value**:
- Example return value:
- [
- {
+ [
+ {
"identifier": "02,98:76:54:32",
"registered_at": "2014-09-21T14:16:06+00:00"
"user": "s0000000"
- },
- {
+ },
+ {
"identifier": "02,dd:ee:ff:00",
"registered_at": "2014-09-21T14:16:06+00:00"
"user": "s0000000"
- },
- {
+ },
+ {
"identifier": "03,fe:dc:ba:98",
"registered_at": "2014-09-21T14:16:06+00:00"
"user": "s0000000"
- },
- {
+ },
+ {
"identifier": "05,01:23:45:67:89:ab:cd",
"registered_at": "2014-09-21T14:16:06+00:00"
"user": "s0000019"
- }
- ]
+ }
+ ]
"""
request = kwargs.get(REQUEST_KEY)
result = []
@@ -74,22 +85,33 @@ def rfid_list(radius_username: str = None, **kwargs) -> List[Dict]:
@manager_required
def rfid_get(radius_username: str, **kwargs) -> List[str]:
"""
+ **Signature**: `rfid.get(radius_username)`
+
+ **Arguments**:
+
+ - `radius_username` : `str` -- Username to search for.
+
+ **Return type**: List of `str`
+
+ **Idempotent**: yes
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
Retrieve registered RFID cards for a specified user and current selected
organization.
- Required user level: Manager
-
Returns an array of registered RFID cards.
- radius_username -- Username to search for.
+ **Example return value**:
- Example return value:
- [
- "02,98:76:54:32",
- "02,dd:ee:ff:00",
- "03,fe:dc:ba:98",
- "05,01:23:45:67:89:ab:cd"
- ]
+ [
+ "02,98:76:54:32",
+ "02,dd:ee:ff:00",
+ "03,fe:dc:ba:98",
+ "05,01:23:45:67:89:ab:cd"
+ ]
Raises error -32602 (Invalid params) if the username does not exist.
"""
@@ -114,25 +136,38 @@ def rfid_get(radius_username: str, **kwargs) -> List[str]:
@transaction.atomic
def rfid_add(radius_username: str, identifier: str, **kwargs) -> Dict:
"""
- Add a new RFID card to the specified user.
+ **Signature**: `rfid.add(radius_username, identifier)`
+
+ **Arguments**:
+
+ - `radius_username` : `str` -- Username to search for.
+ - `identifier` : `str` -- RFID card hardware identifier.
+
+ **Return type**: `dict`
+
+ **Idempotent**: no
+
+ **Required user level**: Manager
- Required user level: Manager
+ **Documentation**:
+
+ Add a new RFID card to the specified user.
Returns the RFID card on success.
- radius_username -- Username to search for.
- identifier -- RFID card hardware identiefier.
+ **Example return value**:
- Example return value:
- {
- "identifier": "02,98:76:54:32",
- "registered_at": "2014-09-21T14:16:06+00:00"
- "user": "s0000000"
- }
+ {
+ "identifier": "02,98:76:54:32",
+ "registered_at": "2014-09-21T14:16:06+00:00"
+ "user": "s0000000"
+ }
- Raises error -32602 (Invalid params) if the username does not exist.
- Raises error -32602 (Invalid params) if the RFID card already exists for this person.
- Raises error -32602 (Invalid params) if the RFID card is already registered by someone else.
+ **Raises errors**:
+
+ - `-32602` (Invalid params) if the username does not exist.
+ - `-32602` (Invalid params) if the RFID card already exists for this person.
+ - `-32602` (Invalid params) if the RFID card is already registered by someone else.
"""
request = kwargs.get(REQUEST_KEY)
try:
@@ -163,15 +198,27 @@ def rfid_add(radius_username: str, identifier: str, **kwargs) -> Dict:
@transaction.atomic
def rfid_remove(radius_username: str, identifier: str, **kwargs) -> None:
"""
- Remove an RFID card from the specified user.
+ **Signature**: `rfid.remove(radius_username, identifier)`
- Required user level: Manager
+ **Arguments**:
- radius_username -- Username to search for.
- identifier -- RFID card hardware identiefier.
+ - `radius_username` : `str` -- Username to search for.
+ - `identifier` : `str` -- RFID card hardware identifier.
- Raises error -32602 (Invalid params) if the username does not exist.
- Raises error -32602 (Invalid params) if the RFID card does not exist for this person/organization.
+ **Return type**: `dict`
+
+ **Idempotent**: no
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
+ Remove an RFID card from the specified user.
+
+ **Raises errors**:
+
+ - `-32602` (Invalid params) if the username does not exist.
+ - `-32602` (Invalid params) if the RFID card does not exist for this person/organization.
"""
request = kwargs.get(REQUEST_KEY)
try:
diff --git a/alexia/api/v1/methods/scheduling.py b/alexia/api/v1/methods/scheduling.py
index 01b1644..b4c1992 100644
--- a/alexia/api/v1/methods/scheduling.py
+++ b/alexia/api/v1/methods/scheduling.py
@@ -12,33 +12,44 @@
@manager_required
def user_get_availabilities(radius_username: str, **kwargs) -> List[Dict]:
"""
- Retrieve the availabilities entered by a specific user for the current organization.
+ **Signature**: `user.get_availabilities(radius_username)`
- Required user level: Manager
+ **Arguments**:
- radius_username -- Username to search for.
+ - `radius_username` : `str` -- Username to search for.
- Raises error -32602 (Invalid params) if the username does not exist.
+ **Return type**: `dict`
- Example result value:
- [
- {
- "event": {
- "name": "Test Drink",
- "date": "2017-09-12T14:00:00Z",
+ **Idempotent**: yes
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
+ Retrieve the availabilities entered by a specific user for the current organization.
+ **Example return value**:
+
+ [
+ {
+ "event": {
+ "name": "Test Drink",
+ "date": "2017-09-12T14:00:00Z",
},
"availability": "Yes"
- },
- {
+ },
+ {
"event": {
- "name": "Test Drink 2",
- "date": "2017-09-18T16:00:00Z",
-
+ "name": "Test Drink 2",
+ "date": "2017-09-18T16:00:00Z",
},
"availability": "Maybe"
- }
- ]
+ }
+ ]
+
+ **Raises errors**:
+
+ - `-32602` (Invalid params) if the username does not exist.
"""
request = kwargs.get(REQUEST_KEY)
try:
diff --git a/alexia/api/v1/methods/user.py b/alexia/api/v1/methods/user.py
index 0a20194..0972d9d 100644
--- a/alexia/api/v1/methods/user.py
+++ b/alexia/api/v1/methods/user.py
@@ -19,25 +19,40 @@
@transaction.atomic
def user_add(radius_username: str, first_name: str, last_name: str, email: str, **kwargs) -> Dict:
"""
+ **Signature**: `user.add(radius_username, first_name, last_name, email)`
+
+ **Arguments**:
+
+ - `radius_username` : `str` -- Unique username
+ - `first_name` : `str` -- First name
+ - `last_name` : `str` -- Last name
+ - `email` : `str` -- Valid email address
+
+ **Return type**: `dict`
+
+ **Idempotent**: no
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
Add a new user to Alexia.
A user must have a unique username and a valid email address.
Returns the user information on success.
- radius_username -- Unique username
- first_name -- First name
- last_name -- Last name
- email -- Valid email address
+ **Example return value**:
- Example result value:
- {
- "first_name": "John",
- "last_name": "Doe",
- "radius_username": "s0000000"
- }
+ {
+ "first_name": "John",
+ "last_name": "Doe",
+ "radius_username": "s0000000"
+ }
+
+ **Raises errors**:
- Raises error -32602 (Invalid params) if the username already exists.
+ - `-32602` (Invalid params) if the username already exists.
"""
if User.objects.filter(username=radius_username).exists() or \
AuthenticationData.objects.filter(backend=OIDC_BACKEND_NAME, username__iexact=radius_username).exists():
@@ -59,11 +74,23 @@ def user_add(radius_username: str, first_name: str, last_name: str, email: str,
@login_required
def user_exists(radius_username: str, **kwargs) -> bool:
"""
- Check if a user exists by his or her username.
+ **Signature**: `user.exists(radius_username)`
+
+ **Arguments**:
+
+ - `radius_username` : `str` -- Username to search for
+
+ **Return type**: `bool`
- Returns true when the username exists, false otherwise.
+ **Idempotent**: yes
- radius_username -- Username to search for.
+ **Required user level**: User
+
+ **Documentation**:
+
+ Check if a user exists by his or her username.
+
+ Returns `True` when the username exists, `False` otherwise.
"""
return User.objects.filter(authenticationdata__backend=OIDC_BACKEND_NAME,
authenticationdata__username=radius_username).exists()
@@ -73,21 +100,35 @@ def user_exists(radius_username: str, **kwargs) -> bool:
@login_required
def user_get(radius_username: str, **kwargs) -> Dict:
"""
+ **Signature**: `user.get(radius_username)`
+
+ **Arguments**:
+
+ - `radius_username` : `str` -- Username to search for.
+
+ **Return type**: `dict`
+
+ **Idempotent**: yes
+
+ **Required user level**: User
+
+ **Documentation**:
+
Retrieve information about a specific user.
- Returns an object representing the user. Result contains first_name,
- last_name and radius_username.
+ Returns an object representing the user. Result contains `first_name`, `last_name` and `radius_username`.
- radius_username -- Username to search for.
+ **Example return value**:
- Raises error 404 if provided username cannot be found.
+ {
+ "first_name": "John",
+ "last_name": "Doe",
+ "radius_username": "s0000000"
+ }
- Example result value:
- {
- "first_name": "John",
- "last_name": "Doe",
- "radius_username": "s0000000"
- }
+ **Raises errors**:
+
+ - `404` (Object not found) if the provided username cannot be found.
"""
try:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
@@ -102,18 +143,33 @@ def user_get(radius_username: str, **kwargs) -> Dict:
@login_required
def user_get_by_id(user_id: int, **kwargs) -> Dict:
"""
+ **Signature**: `user.get_by_id(radius_username)`
+
+ **Arguments**:
+
+ - `user_id` : `int` -- User id to search for.
+
+ **Return type**: `dict`
+
+ **Idempotent**: yes
+
+ **Required user level**: User
+
+ **Documentation**:
+
Retrieve information about a specific user.
- user_id -- User id to search for.
+ **Example return value**:
- Raises error 404 if provided username cannot be found.
+ {
+ "first_name": "John",
+ "last_name": "Doe",
+ "radius_username": "s0000000"
+ }
- Example result value:
- {
- "first_name": "John",
- "last_name": "Doe",
- "radius_username": "s0000000"
- }
+ **Raises errors**:
+
+ - `404` (Object not found) if the provided username cannot be found.
"""
try:
user = User.objects.get(id=user_id)
@@ -127,25 +183,37 @@ def user_get_by_id(user_id: int, **kwargs) -> Dict:
@manager_required
def user_get_membership(radius_username: str, **kwargs) -> Dict:
"""
+ **Signature**: `user.get_membership(radius_username)`
+
+ **Arguments**:
+
+ - `radius_username` : `str` -- Username to search for.
+
+ **Return type**: `dict`
+
+ **Idempotent**: yes
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
Retrieve the membership details for a specific user for the current organization.
- Required user level: Manager
+ **Example return value**:
- radius_username -- Username to search for.
+ {
+ "user": "s0000000",
+ "organization": "Inter-Actief",
+ "comments": "",
+ "is_tender": True,
+ "is_planner": False,
+ "is_manager": False,
+ "is_active": True
+ }
- Raises error 404 if the provided username cannot be found or the user has no membership with the current
- organization.
+ **Raises errors**:
- Example result value:
- {
- "user": "s0000000",
- "organization": "Inter-Actief",
- "comments": "",
- "is_tender": True,
- "is_planner": False,
- "is_manager": False,
- "is_active": True
- }
+ - `404` (Object not found) if the provided username cannot be found or the user has no membership with the current organization.
"""
request = kwargs.get(REQUEST_KEY)
try:
@@ -177,19 +245,33 @@ def user_get_membership(radius_username: str, **kwargs) -> Dict:
@manager_required
def user_get_iva_certificate(radius_username: str, **kwargs) -> Dict:
"""
+ **Signature**: `user.get_iva_certificate(radius_username)`
+
+ **Arguments**:
+
+ - `radius_username` : `str` -- Username to search for.
+
+ **Return type**: `dict`
+
+ **Idempotent**: yes
+
+ **Required user level**: Manager
+
+ **Documentation**:
+
Retrieve the IVA certificate file for a specific user.
- Required user level: Manager
+ **Example return value**:
- radius_username -- Username to search for.
+ {
+ "user": "s0000000",
+ "certificate_data": "U29tZSBiYXNlIDY0IHRleHQgdGhhdCBtaWdodCBiZ.........BhIGxvdCBsb25nZXIgdGhhbiB0aGlzIGlzLi4u"
+ }
- Raises error 404 if provided username cannot be found or the user has no IVA certificate.
+ **Raises errors**:
+
+ - `404` (Object not found) if provided username cannot be found or the user has no IVA certificate.
- Example result value:
- {
- "user": "s0000000",
- "certificate_data": "U29tZSBiYXNlIDY0IHRleHQgdGhhdCBtaWdodCBiZ.........BhIGxvdCBsb25nZXIgdGhhbiB0aGlzIGlzLi4u"
- }
"""
try:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
diff --git a/alexia/conf/settings/base.py b/alexia/conf/settings/base.py
index 352cb81..4143169 100644
--- a/alexia/conf/settings/base.py
+++ b/alexia/conf/settings/base.py
@@ -151,12 +151,14 @@
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# ModernRPC API
-MODERNRPC_HANDLERS = [
- "alexia.api.handlers.AlexiaJSONRPCHandler"
-]
MODERNRPC_METHODS_MODULES = [
'alexia.api.v1.methods'
]
+MODERNRPC_HANDLERS = [
+ "alexia.api.handlers.AlexiaJSONRPCHandler"
+]
+# API documentation strings are formatted with markdown
+MODERNRPC_DOC_FORMAT = 'markdown'
# Single Sign On via https://auth.ia.utwente.nl/
OIDC_OP_AUTHORIZATION_ENDPOINT = "https://auth.ia.utwente.nl/realms/inter-actief/protocol/openid-connect/auth"
diff --git a/requirements.txt b/requirements.txt
index 3a7365a..c05c07c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,6 +9,8 @@ icalendar==4.1.0
jsonfield>=3.1.0,<3.2
# Django Modern RPC -- For our JSONRPC API
django-modern-rpc>=1.0.1,<1.1
+# Markdown formatting is used in the API docstrings
+Markdown>=3.8,<4
raven==6.10.0
# Translations -- development
django-rosetta>=0.10.2,<0.11.0
diff --git a/templates/api/doc.html b/templates/api/doc.html
index 87a5ee7..fe1d7dd 100644
--- a/templates/api/doc.html
+++ b/templates/api/doc.html
@@ -20,38 +20,15 @@ {% block headertitle %}API documentation{% endblock %}
Methods
{% for method in methods %}
{{ method.name }}
+
-
-
- | Signature |
-
- {{ method.name }}({% for param in method.params %}{{ param.name }}{% if not forloop.last %}, {% endif %}{% endfor %})
- |
-
-
- | Arguments |
-
- {% for param in method.params %}
- {{ param.name }} : {{ param.type }}
- {% if not forloop.last %} {% endif %}
- {% empty %}
- None specified
- {% endfor %}
- |
-
-
- | Return type |
- {{ method.return.type }} |
-
-
- | Idempotent |
- {{ method.idempotent|yesno:'yes,no' }} |
-
-
- | Summary |
- {{ method.summary }} |
-
-
+ |
+ {% if method.html_doc %}
+ {{ method.html_doc | safe | urlize }}
+ {% else %}
+ No documentation available yet.
+ {% endif %}
+ |
{% endfor %}
diff --git a/templates/api/info.html b/templates/api/info.html
index 2052acb..341e915 100644
--- a/templates/api/info.html
+++ b/templates/api/info.html
@@ -16,7 +16,6 @@
fullest, a manager-authorization on at least one organization is recommended.
{% endblock content %}
diff --git a/templates/api/v1/doc.html b/templates/api/v1/doc.html
index d28d921..5670c79 100644
--- a/templates/api/v1/doc.html
+++ b/templates/api/v1/doc.html
@@ -19,7 +19,7 @@ Basics
of positional parameters is highly encouraged.
- URL: https://alex.ia.utwente.nl{{ mountpoint }}
+ URL: https://alex.ia.utwente.nl/api/1/
Authentication