From a02bfdb887550f6feb01e849902ce90a6b2dc87b Mon Sep 17 00:00:00 2001 From: Kresten Laust Date: Fri, 29 Nov 2024 22:41:14 +0100 Subject: [PATCH] Improve API (API 1.1) (#421) * Streamline GetMemberID with other Member-API endpoints The other endpoints use 'Invalid ID' to signify either missing or invalid. This endpoint used to use 'Invalid Username' to signify that the member does not exist, while the other's would've used 'Member not found' for that. * Consistent member/user terminology There's an inconsistency in whether to call a member a user. Generally, users are the ones with admin access, while members are the customers. * Consistent view-method naming Even though it's a bool, I believe it's more appropriate to keep the naming consistent with non-bool return types. Also, it still returns an object. * Make get ID consistent as well I won't rename the actual endpoint because of backwards compatibility. * Rename Get Payment QR-endpoint * Refactor Get Payment QR * Rename named_products method * Change return text * Remove use of annotations from future * Object instead of array in Active Products-endpoint More meaningful response in Active Products-endpoint, now returns {"1": {"name": "Beer", "price": 600}}, instead of {"1": ["Beer", 600]} * TODO Check whether room exists * Rename URL name * Strongly typed return-object for category mapping * Make QR Form and helper function generic * Rename 'member' -> 'username'-parameter * Change minimum value for QR-code form to 0 * Fix incorrect 400 response body's * Make api/sale-endpoint responses more specific * Remove unused import * Rename view-functions * Change naming scheme --- stregsystem/forms.py | 2 +- stregsystem/urls.py | 20 +++++----- stregsystem/utils.py | 13 ++++++- stregsystem/views.py | 92 +++++++++++++++++++++++++------------------- 4 files changed, 75 insertions(+), 52 deletions(-) diff --git a/stregsystem/forms.py b/stregsystem/forms.py index 65e16b94..6ba947d7 100644 --- a/stregsystem/forms.py +++ b/stregsystem/forms.py @@ -72,7 +72,7 @@ def __init__(self, *args, **kwargs): class QRPaymentForm(forms.Form): member = forms.CharField(max_length=16) - amount = forms.DecimalField(min_value=50, decimal_places=2, required=False) + amount = forms.DecimalField(min_value=0, decimal_places=2, required=False) class PurchaseForm(forms.Form): diff --git a/stregsystem/urls.py b/stregsystem/urls.py index 135124ed..9fbbe9bb 100644 --- a/stregsystem/urls.py +++ b/stregsystem/urls.py @@ -33,14 +33,14 @@ re_path(r'^(?P\d+)/user/(?P\d+)/pay$', views.menu_userpay, name="userpay"), re_path(r'^(?P\d+)/user/(?P\d+)/rank$', views.menu_userrank, name="userrank"), re_path(r'^(?P\d+)/send_csv_mail/(?P\d+)/$', views.send_userdata, name="send_userdata"), - re_path(r'^api/member/payment/qr$', views.qr_payment, name="payment_qr"), - re_path(r'^api/member/active$', views.check_user_active, name="active_member"), - re_path(r'^api/member/sales$', views.get_user_sales, name="get_user_sales"), - re_path(r'^api/member/get_id$', views.convert_username_to_id, name="get_id"), - re_path(r'^api/member/balance$', views.get_user_balance, name="get_user_balance"), - re_path(r'^api/member$', views.get_user_info, name="get_user_transactions"), - re_path(r'^api/products/named_products$', views.dump_named_items, name="named_products"), - re_path(r'^api/products/active_products$', views.dump_active_items, name="active_products"), - re_path(r'^api/products/category_mappings$', views.dump_product_category_mappings, name="product_mappings"), - re_path(r'^api/sale$', views.api_sale, name="sale"), + re_path(r'^api/member/payment/qr$', views.get_payment_qr, name="api_payment_qr"), + re_path(r'^api/member/active$', views.get_member_active, name="api_member_active"), + re_path(r'^api/member/sales$', views.get_member_sales, name="api_member_sales"), + re_path(r'^api/member/get_id$', views.get_member_id, name="api_member_id"), + re_path(r'^api/member/balance$', views.get_member_balance, name="api_member_balance"), + re_path(r'^api/member$', views.get_member_info, name="api_member_info"), + re_path(r'^api/products/named_products$', views.get_named_products, name="api_named_products"), + re_path(r'^api/products/active_products$', views.get_active_items, name="api_active_products"), + re_path(r'^api/products/category_mappings$', views.get_product_category_mappings, name="api_product_mappings"), + re_path(r'^api/sale$', views.api_sale, name="api_sale"), ] diff --git a/stregsystem/utils.py b/stregsystem/utils.py index 10ad31e0..baafca0e 100644 --- a/stregsystem/utils.py +++ b/stregsystem/utils.py @@ -14,6 +14,8 @@ import qrcode import qrcode.image.svg +import urllib.parse + logger = logging.getLogger(__name__) @@ -161,7 +163,7 @@ def strip_emoji(text): ).strip() -def qr_code(data): +def qr_code(data) -> HttpResponse: response = HttpResponse(content_type="image/svg+xml") qr = qrcode.make(data, image_factory=qrcode.image.svg.SvgPathFillImage) qr.save(response) @@ -169,6 +171,15 @@ def qr_code(data): return response +def mobilepay_launch_uri(comment: str, amount: float) -> str: + query = {'phone': '90601', 'comment': comment} + + if amount is not None: + query['amount'] = amount + + return 'mobilepay://send?{}'.format(urllib.parse.urlencode(query)) + + class stregsystemTestRunner(DiscoverRunner): def __init__(self, *args, **kwargs): settings.TEST_MODE = True diff --git a/stregsystem/views.py b/stregsystem/views.py index c0f94279..90700ee3 100644 --- a/stregsystem/views.py +++ b/stregsystem/views.py @@ -1,7 +1,6 @@ import datetime import io import json -import urllib.parse from typing import List, Type import pytz @@ -20,6 +19,7 @@ from django.shortcuts import get_object_or_404, render, redirect from django.utils import timezone from django.views.decorators.csrf import csrf_exempt +from django_select2 import forms as s2forms from stregreport.views import fjule_party @@ -44,6 +44,7 @@ from stregsystem.utils import ( make_active_productlist_query, qr_code, + mobilepay_launch_uri, make_room_specific_query, make_unprocessed_mobilepayment_query, parse_csv_and_create_mobile_payments, @@ -563,18 +564,18 @@ def signup_tool(request): return render(request, "admin/stregsystem/approval_tools/signup_tool.html", data) -def qr_payment(request): +# API views + + +def get_payment_qr(request): form = QRPaymentForm(request.GET) if not form.is_valid(): return HttpResponseBadRequest("Invalid input for MobilePay QR code generation") - query = {'phone': '90601', 'comment': form.cleaned_data.get('member')} - - if form.cleaned_data.get("amount") is not None: - query['amount'] = form.cleaned_data.get("amount") + username = form.cleaned_data.get('username') + amount = form.cleaned_data.get('amount') - data = 'mobilepay://send?{}'.format(urllib.parse.urlencode(query)) - return qr_code(data) + return qr_code(mobilepay_launch_uri(username, amount)) def signup(request): @@ -620,26 +621,24 @@ def signup_status(request, signup_id): return render(request, "stregsystem/signup_status.html", locals()) -# API views - - -def dump_active_items(request): +def get_active_items(request): room_id = request.GET.get('room_id') or None if room_id is None: - return HttpResponseBadRequest("Missing room_id") + return HttpResponseBadRequest("Parameter missing: room_id") elif not room_id.isdigit(): - return HttpResponseBadRequest("Invalid room_id") + return HttpResponseBadRequest("Parameter invalid: room_id") + # TODO: Check whether room exists items = __get_productlist(room_id) - items_dict = {item.id: (item.name, item.price) for item in items} + items_dict = {item.id: {'name': item.name, 'price': item.price} for item in items} return JsonResponse(items_dict, json_dumps_params={'ensure_ascii': False}) -def check_user_active(request): +def get_member_active(request): member_id = request.GET.get('member_id') or None if member_id is None: - return HttpResponseBadRequest("Missing member_id") + return HttpResponseBadRequest("Parameter missing: member_id") elif not member_id.isdigit(): - return HttpResponseBadRequest("Invalid member_id") + return HttpResponseBadRequest("Parameter invalid: member_id") try: member = Member.objects.get(pk=member_id) except Member.DoesNotExist: @@ -647,27 +646,34 @@ def check_user_active(request): return JsonResponse({'active': member.active}) -def convert_username_to_id(request): +def get_member_id(request): username = request.GET.get('username') or None if username is None: - return HttpResponseBadRequest("Missing username") + return HttpResponseBadRequest("Parameter missing: username") + try: member = Member.objects.get(username=username) except Member.DoesNotExist: - return HttpResponseBadRequest("Invalid username") + return HttpResponseBadRequest("Member not found") + return JsonResponse({'member_id': member.id}) -def dump_product_category_mappings(request): - return JsonResponse({p.id: [(cat.id, cat.name) for cat in p.categories.all()] for p in Product.objects.all()}) +def get_product_category_mappings(request): + return JsonResponse( + { + p.id: [{'category_id': cat.id, 'category_name': cat.name} for cat in p.categories.all()] + for p in Product.objects.all() + } + ) -def get_user_sales(request): +def get_member_sales(request): member_id = request.GET.get('member_id') or None if member_id is None: - return HttpResponseBadRequest("Missing member_id") + return HttpResponseBadRequest("Parameter missing: member_id") elif not member_id.isdigit(): - return HttpResponseBadRequest("Invalid member_id") + return HttpResponseBadRequest("Parameter invalid: member_id") count = 10 if request.GET.get('count') is None else int(request.GET.get('count') or 10) sales = Sale.objects.filter(member=member_id).order_by('-timestamp')[:count] return JsonResponse( @@ -675,12 +681,12 @@ def get_user_sales(request): ) -def get_user_balance(request): +def get_member_balance(request): member_id = request.GET.get('member_id') or None if member_id is None: - return HttpResponseBadRequest("Missing member_id") + return HttpResponseBadRequest("Parameter missing: member_id") elif not member_id.isdigit(): - return HttpResponseBadRequest("Invalid member_id") + return HttpResponseBadRequest("Parameter invalid: member_id") try: member = Member.objects.get(pk=member_id) except Member.DoesNotExist: @@ -688,10 +694,12 @@ def get_user_balance(request): return JsonResponse({'balance': member.balance}) -def get_user_info(request): +def get_member_info(request): member_id = str(request.GET.get('member_id')) or None - if member_id is None or not member_id.isdigit(): - return HttpResponseBadRequest("Missing or invalid member_id") + if member_id is None: + return HttpResponseBadRequest("Parameter missing: member_id") + elif not member_id.isdigit(): + return HttpResponseBadRequest("Parameter invalid: member_id") member = find_user_from_id(int(member_id)) if member is None: @@ -714,7 +722,7 @@ def find_user_from_id(user_id: int): return None -def dump_named_items(request): +def get_named_products(request): items = NamedProduct.objects.all() items_dict = {item.name: item.product.id for item in items} return JsonResponse(items_dict, json_dumps_params={'ensure_ascii': False}) @@ -730,12 +738,16 @@ def api_sale(request): room = str(data['room']) or None member_id = str(data['member_id']) or None - if room is None or not room.isdigit(): - return HttpResponseBadRequest("Missing or invalid room") + if room is None: + return HttpResponseBadRequest("Parameter missing: room") + if not room.isdigit(): + return HttpResponseBadRequest("Parameter invalid: room") if buy_string is None: - return HttpResponseBadRequest("Missing buystring") - if member_id is None or not member_id.isdigit(): - return HttpResponseBadRequest("Missing or invalid member_id") + return HttpResponseBadRequest("Parameter missing: buystring") + if member_id is None: + return HttpResponseBadRequest("Parameter missing: member_id") + if not member_id.isdigit(): + return HttpResponseBadRequest("Parameter invalid: member_id") try: username, bought_ids = parser.parse(_pre_process(buy_string)) @@ -744,7 +756,7 @@ def api_sale(request): member = find_user_from_id(int(member_id)) if member is None: - return HttpResponseBadRequest("Invalid member_id") + return HttpResponseBadRequest("Parameter invalid: member_id") if not member.signup_due_paid: return HttpResponseBadRequest("Signup due not paid") @@ -761,7 +773,7 @@ def api_sale(request): try: room = Room.objects.get(pk=room) except Room.DoesNotExist: - return HttpResponseBadRequest("Invalid room") + return HttpResponseBadRequest("Parameter invalid: room") msg, status, ret_obj = api_quicksale(request, room, member, bought_ids) return JsonResponse( {'status': status, 'msg': msg, 'values': ret_obj}, json_dumps_params={'ensure_ascii': False}