Files
kakebo/app/api/views.py
Andros Fenollosa 19f4e84a30 Remove user FK from all models, add goals and promises
- Remove user ForeignKey from all 7 models (single-user app)
- Update all views, handlers, forms, admin, API, seed, and tests
- Add MonthlyGoal model with goals and promises sections
- Goals/promises: add, toggle (strikethrough), delete via LiveView
2026-03-18 15:18:50 +01:00

118 lines
3.0 KiB
Python

from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from app.expenses.models import Category, Subcategory
from .authentication import HasValidToken, TokenEnvAuthentication
from .serializers import (
CategorySerializer,
ExpenseReadSerializer,
ExpenseWriteSerializer,
SubcategorySerializer,
)
class RootView(APIView):
authentication_classes = [TokenEnvAuthentication]
permission_classes = [HasValidToken]
def get(self, request):
return Response(
{
"type": "Success",
"_links": {
"categories": {"href": "/api/categories/", "method": "GET"},
"expenses": {"href": "/api/expenses/", "method": "POST"},
},
}
)
class BaseAPIView(APIView):
authentication_classes = [TokenEnvAuthentication]
permission_classes = [HasValidToken]
def _success(self, data, meta=None, links=None, status_code=status.HTTP_200_OK):
response = {"type": "Success", "data": data}
if meta:
response["meta"] = meta
if links:
response["_links"] = links
return Response(response, status=status_code)
def _error(
self,
errors,
error_type="ParametersError",
status_code=status.HTTP_400_BAD_REQUEST,
):
formatted = []
if isinstance(errors, dict):
for field, messages in errors.items():
for msg in messages if isinstance(messages, list) else [messages]:
formatted.append({"field": field, "message": str(msg)})
else:
formatted.append({"field": None, "message": str(errors)})
return Response(
{"type": error_type, "errors": formatted},
status=status_code,
)
class CategoriesView(BaseAPIView):
def get(self, request):
categories = Category.objects.prefetch_related("subcategories").all()
serializer = CategorySerializer(categories, many=True)
return self._success(
serializer.data,
links={
"self": {"href": "/api/categories/", "method": "GET"},
},
)
class SubcategoriesView(BaseAPIView):
def get(self, request, category_id):
try:
category = Category.objects.get(id=category_id)
except Category.DoesNotExist:
return self._error(
"Category not found.",
error_type="ResourceError",
status_code=status.HTTP_404_NOT_FOUND,
)
subcategories = Subcategory.objects.filter(category=category)
serializer = SubcategorySerializer(subcategories, many=True)
return self._success(
serializer.data,
links={
"self": {
"href": f"/api/categories/{category_id}/subcategories/",
"method": "GET",
},
"category": {
"href": "/api/categories/",
"method": "GET",
},
},
)
class ExpensesView(BaseAPIView):
def post(self, request):
serializer = ExpenseWriteSerializer(data=request.data)
if not serializer.is_valid():
return self._error(serializer.errors)
expense = serializer.save()
read_serializer = ExpenseReadSerializer(expense)
return self._success(
read_serializer.data,
links={
"self": {"href": "/api/expenses/", "method": "POST"},
"categories": {"href": "/api/categories/", "method": "GET"},
},
status_code=status.HTTP_201_CREATED,
)