Skip to content

Commit

Permalink
Improve API (API 1.1) (#421)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
krestenlaust authored Nov 29, 2024
1 parent 4d76a80 commit a02bfdb
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 52 deletions.
2 changes: 1 addition & 1 deletion stregsystem/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
20 changes: 10 additions & 10 deletions stregsystem/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@
re_path(r'^(?P<room_id>\d+)/user/(?P<member_id>\d+)/pay$', views.menu_userpay, name="userpay"),
re_path(r'^(?P<room_id>\d+)/user/(?P<member_id>\d+)/rank$', views.menu_userrank, name="userrank"),
re_path(r'^(?P<room_id>\d+)/send_csv_mail/(?P<member_id>\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"),
]
13 changes: 12 additions & 1 deletion stregsystem/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import qrcode
import qrcode.image.svg

import urllib.parse

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -161,14 +163,23 @@ 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)

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
Expand Down
92 changes: 52 additions & 40 deletions stregsystem/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import datetime
import io
import json
import urllib.parse
from typing import List, Type

import pytz
Expand All @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -620,78 +621,85 @@ 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:
return HttpResponseBadRequest("Member not found")
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(
{'sales': [{'timestamp': s.timestamp, 'product': s.product.name, 'price': s.product.price} for s in sales]}
)


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:
return HttpResponseBadRequest("Member not found")
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:
Expand All @@ -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})
Expand All @@ -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))
Expand All @@ -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")
Expand All @@ -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}
Expand Down

0 comments on commit a02bfdb

Please sign in to comment.