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..cb99510 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"), name="jsonrpc_docs"),
]
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..a8d11c0 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,35 +11,48 @@
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.
+ **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 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 = []
authorizations = Authorization.objects.filter(organization=request.organization)
@@ -58,30 +73,44 @@ 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]:
"""
+ **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 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 = []
try:
@@ -102,33 +131,44 @@ 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.
+ **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**:
- Example return value:
- {
- "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"
+ }
+
+ **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:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
authenticationdata__username=radius_username)
@@ -141,24 +181,36 @@ 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.
+ **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:
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..4c9186c 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,25 +10,37 @@
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.
+ **Signature**: `order.unsynchronized(unused)`
+
+ **Arguments**:
+
+ - `unused` : `int` -- Unused parameter
+
+ **Return type**: List of `dict`
- Required user level: Manager
+ **Idempotent**: yes
+
+ **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",
@@ -35,7 +49,7 @@ def order_unsynchronized(request, unused=0):
},
{
"price": "1.00",
- "product": {"name": "Coca Cola"},
+ "product": {"name": "Coca-Cola"},
"amount": 2
}
],
@@ -53,12 +67,12 @@ def order_unsynchronized(request, unused=0):
"start_date": "2014-09-21T14:16:06+00:00",
"user": "s0000000"
}
- },
- {
+ },
+ {
"purchases": [
{
"price": "1.00",
- "product": {"name": "Coca Cola"},
+ "product": {"name": "Coca-Cola"},
"amount": 2
}, {
"price": "0.50",
@@ -80,9 +94,10 @@ def order_unsynchronized(request, unused=0):
"start_date": "2014-09-21T14:16:06+00:00",
"user": "s0000000"
}
- }
- ]
+ }
+ ]
"""
+ request = kwargs.get(REQUEST_KEY)
result = []
orders = Order.objects.filter(authorization__organization=request.organization, synchronized=False)
@@ -94,49 +109,63 @@ 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.
+ **Signature**: `order.get(order_id)`
- Required user level: Manager
+ **Arguments**:
- Returns an order object.
+ - `order_id` : `int` -- ID of the Order object.
- order_id -- ID of the Order object.
+ **Return type**: `dict`
- Raises error 404 if provided order id cannot be found.
+ **Idempotent**: yes
- Example return value:
- {
- "purchases": [
+ **Required user level**: Manager
+
+ **Documentation**:
+
+ Return a specific order.
+
+ Returns an order object.
+
+ **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:
order = Order.objects.get(authorization__organization=request.organization, pk=order_id)
except Order.DoesNotExist:
@@ -145,74 +174,86 @@ 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.
+ **Signature**: `order.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
- Povide a username to select only orders made by the provided user.
+ **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 = []
orders = Order.objects.filter(event__organizer=request.organization)
@@ -232,21 +273,34 @@ 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.
+ **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:
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..32b976d 100644
--- a/alexia/api/v1/methods/event.py
+++ b/alexia/api/v1/methods/event.py
@@ -1,33 +1,46 @@
+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.
+ **Signature**: `event.upcoming_list(include_current)`
+
+ **Arguments**:
+
+ - `include_current` : `bool` -- Whether to include ongoing events, defaults to false
- Required user level: None
+ **Return type**: List of `dict`
- include_current -- Whether to include ongoing events, defaults to false
+ **Idempotent**: yes
+
+ **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 [{
@@ -39,4 +52,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..33226fc 100644
--- a/alexia/api/v1/methods/generic.py
+++ b/alexia/api/v1/methods/generic.py
@@ -1,15 +1,27 @@
-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.
+ **Signature**: `version()`
+
+ **Arguments**:
+
+ - *None*
- Required user level: None
+ **Return type**: `int`
+
+ **Idempotent**: yes
+
+ **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.
@@ -20,38 +32,59 @@ 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.
+ **Signature**: `methods()`
+
+ **Arguments**:
+
+ - *None*
+
+ **Return type**: List of `str`
+
+ **Idempotent**: yes
+
+ **Required user level**: Manager
+
+ **Documentation**:
- Required user level: None
+ Introspect the API and return all callable methods.
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.
+ **Signature**: `login()`
- Required user level: None
+ **Arguments**:
- Returns true when an user has successful 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
- username -- Username of user
- password -- Password of the user
+ **Required user level**: *None*
+
+ **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)
# 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,16 +98,29 @@ 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.
+ **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.
"""
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..1400df5 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,15 @@ 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:
+ """
+ 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)
identifier = rfid_to_identifier(rfid=rfid)
@@ -108,10 +116,18 @@ 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):
- """Saves a new order in the database"""
+def juliana_order_save(event_id: int, user_id: int, purchases: List[Dict], rfid_data: Dict, **kwargs) -> None:
+ """
+ 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)
rfid_identifier = rfid_to_identifier(rfid=rfid_data)
@@ -150,7 +166,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 +185,15 @@ 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:
+ """
+ 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)
try:
@@ -187,12 +210,20 @@ 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):
- """Saves a writeoff order in the Database"""
+def juliana_writeoff_save(event_id: int, writeoff_id: int, purchases: List[Dict], **kwargs) -> bool:
+ """
+ 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)
-
+
try:
writeoff_cat = WriteoffCategory.objects.get(id=writeoff_id)
except WriteoffCategory.DoesNotExist:
@@ -217,7 +248,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 +259,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..0c7393a 100644
--- a/alexia/api/v1/methods/organization.py
+++ b/alexia/api/v1/methods/organization.py
@@ -1,47 +1,77 @@
-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.
+ **Signature**: `organization.current.get()`
+
+ **Arguments**:
+
+ - *None*
+
+ **Return type**: *(optional)* `str`
+
+ **Idempotent**: yes
- Required user level: None
+ **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:
- "inter-actief"
+ **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.
+ **Signature**: `organization.current.set(organization)`
+
+ **Arguments**:
- Required user level: None
+ - `organization` : `str` -- slug of the organization or empty string to deselect organization.
+
+ **Return type**: `bool`
+
+ **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:
if 'organization_pk' in request.session:
del request.session['organization_pk']
@@ -61,23 +91,37 @@ 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]:
"""
+ **Signature**: `organization.list()`
+
+ **Arguments**:
+
+ - *None*
+
+ **Return type**: List of `str`
+
+ **Idempotent**: no
+
+ **Required user level**: *None*
+
+ **Documentation**:
+
List all public organizations.
- Required user level: None
+ Returns an array with zero or more organizations.
- Returns a array with zero or more organizations.
+ **Example return value**:
- Example return value:
- [
- "abacus",
- "inter-actief",
- "proto",
- "scintilla",
- "sirius",
- "stress"
- ]
+ [
+ "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 d47316e..f5d95db 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,47 +10,58 @@
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.
+ **Signature**: `rfid.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
- Provide radius_username to select only RFID cards registered by the provided user.
+ **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 = []
rfidcards = RfidCard.objects.filter(managed_by=request.organization)
@@ -68,29 +81,41 @@ 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]:
"""
+ **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.
"""
+ request = kwargs.get(REQUEST_KEY)
result = []
try:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
@@ -106,32 +131,45 @@ 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.
+ **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:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
authenticationdata__username=radius_username)
@@ -155,21 +193,34 @@ 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.
+ **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:
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..b4c1992 100644
--- a/alexia/api/v1/methods/scheduling.py
+++ b/alexia/api/v1/methods/scheduling.py
@@ -1,50 +1,57 @@
-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.
+ **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:
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..0972d9d 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,34 +12,47 @@
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:
"""
+ **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.
- 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.
- 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 error -32602 (Invalid params) if the username already exists.
+ **Raises errors**:
+
+ - `-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():
@@ -55,37 +70,65 @@ 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.
+ **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()
-@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:
"""
+ **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 a 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,
@@ -96,21 +139,37 @@ 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:
"""
+ **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)
@@ -120,35 +179,43 @@ 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:
"""
+ **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:
user = User.objects.get(authenticationdata__backend=OIDC_BACKEND_NAME,
authenticationdata__username=radius_username)
@@ -174,28 +241,37 @@ 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:
"""
+ **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/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{% block headertitle %}API documentation{% endblock %}
Methods
{% for method in methods %}
{{ method.name }}
+
-
-
{% 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 @@
-
- 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 %}
+
fullest, a manager-authorization on at least one organization is recommended.
- URL: https://alex.ia.utwente.nl{{ mountpoint }} + URL: https://alex.ia.utwente.nl/api/1/