Skip to content

Commit 58561b9

Browse files
authored
Merge pull request #13 from Kalgoc/expenses
User Expense Type
2 parents e6b88f6 + 096b36a commit 58561b9

File tree

14 files changed

+288
-5
lines changed

14 files changed

+288
-5
lines changed

budget/migrations/0001_initial.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 5.0.6 on 2024-06-10 16:41
1+
# Generated by Django 5.0.6 on 2024-06-10 17:43
22

33
from django.db import migrations, models
44

budget/models.py

-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# budget/models.py
22
from django.db import models
33
from django.conf import settings
4-
import uuid
54

65

76
class Budget(models.Model):
@@ -10,6 +9,3 @@ class Budget(models.Model):
109
amount = models.IntegerField()
1110
created_at = models.DateTimeField(auto_now_add=True)
1211
updated_at = models.DateTimeField(auto_now=True)
13-
14-
def __str__(self):
15-
return f"{self.user}'s budget: {self.amount}"

piggywallet/settings/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"playground",
4747
"authentication",
4848
"budget",
49+
"user_expense_type",
4950
]
5051

5152
REST_FRAMEWORK = {

piggywallet/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@
2323
path("auth/", include("authentication.urls")),
2424
path("playground/", include("playground.urls")),
2525
path("budget/", include("budget.urls")),
26+
path("user_expense_type/", include("user_expense_type.urls")),
2627
]

user_expense_type/__init__.py

Whitespace-only changes.

user_expense_type/admin.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.contrib import admin
2+
from .models import UserExpenseType
3+
4+
5+
@admin.register(UserExpenseType)
6+
class UserExpenseTypeAdmin(admin.ModelAdmin):
7+
list_display = ("username", "name", "created_at", "updated_at")
8+
search_fields = ("username", "name", "category_name")

user_expense_type/apps.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class UserExpenseTypeConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "user_expense_type"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 5.0.6 on 2024-06-10 18:57
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
initial = True
9+
10+
dependencies = []
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name="UserExpenseType",
15+
fields=[
16+
("id", models.AutoField(primary_key=True, serialize=False)),
17+
("username", models.UUIDField()),
18+
("name", models.CharField(default="Personal", max_length=70)),
19+
("description", models.CharField(blank=True, max_length=255, null=True)),
20+
("set_by_user", models.BooleanField(default=False)),
21+
("created_at", models.DateTimeField(auto_now_add=True)),
22+
("updated_at", models.DateTimeField(auto_now=True)),
23+
],
24+
),
25+
]

user_expense_type/migrations/__init__.py

Whitespace-only changes.

user_expense_type/models.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.db import models
2+
from django.conf import settings
3+
4+
5+
class UserExpenseType(models.Model):
6+
id = models.AutoField(primary_key=True)
7+
username = models.UUIDField()
8+
name = models.CharField(max_length=70, default="Personal")
9+
description = models.CharField(max_length=255, blank=True, null=True)
10+
set_by_user = models.BooleanField(default=False)
11+
created_at = models.DateTimeField(auto_now_add=True)
12+
updated_at = models.DateTimeField(auto_now=True)

user_expense_type/serializers.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from rest_framework import serializers
2+
from .models import UserExpenseType
3+
4+
5+
class UserExpenseTypeSerializer(serializers.ModelSerializer):
6+
id = serializers.ReadOnlyField()
7+
8+
class Meta:
9+
model = UserExpenseType
10+
fields = "__all__"
11+
12+
def create(self, validated_data):
13+
return UserExpenseType.objects.create(**validated_data)
14+
15+
def update(self, instance, validated_data):
16+
instance.name = validated_data.get("name", instance.name)
17+
instance.description = validated_data.get("description", instance.description)
18+
instance.set_by_user = validated_data.get("set_by_user", instance.set_by_user)
19+
instance.save()
20+
return instance

user_expense_type/tests.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from django.test import TestCase
2+
from unittest.mock import patch
3+
from rest_framework.test import APIClient
4+
from rest_framework import status
5+
from authentication.services.cognito_service import CognitoService
6+
from user_expense_type.views import UserExpenseTypeViewSet
7+
from rest_framework.response import Response
8+
9+
10+
class UserExpenseTypeViewSetTestCase(TestCase):
11+
def setUp(self):
12+
self.client = APIClient()
13+
self.view = UserExpenseTypeViewSet()
14+
self.user_expense_type_data = {
15+
"name": "Viajes",
16+
"description": "Gastos incurridos en viajes",
17+
"set_by_user": True,
18+
}
19+
20+
@patch.object(UserExpenseTypeViewSet, "create")
21+
@patch.object(CognitoService, "login_user")
22+
def test_create(self, mock_login_user, mock_create):
23+
mock_login_user.return_value = {
24+
"AuthenticationResult": {"AccessToken": "mock_access_token", "IdToken": "mock_id_token"}
25+
}
26+
mock_create.return_value = Response(status=status.HTTP_201_CREATED, data=self.user_expense_type_data)
27+
28+
request = self.client.post("/user_expense_type/", self.user_expense_type_data)
29+
request.headers["Authorization"] = "Bearer mock_access_token"
30+
31+
response = self.client.post("/user_expense_type/", self.user_expense_type_data)
32+
33+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
34+
self.assertEqual(response.data["name"], "Viajes")
35+
36+
@patch.object(UserExpenseTypeViewSet, "list")
37+
@patch.object(CognitoService, "login_user")
38+
def test_list(self, mock_login_user, mock_list):
39+
mock_login_user.return_value = {
40+
"AuthenticationResult": {"AccessToken": "mock_access_token", "IdToken": "mock_id_token"}
41+
}
42+
mock_list.return_value = Response(status=status.HTTP_200_OK, data=self.user_expense_type_data)
43+
44+
request = self.client.get("/user_expense_type/")
45+
request.headers["Authorization"] = "Bearer mock_access_token"
46+
47+
response = self.client.get("/user_expense_type/")
48+
49+
self.assertEqual(response.status_code, status.HTTP_200_OK)
50+
self.assertEqual(response.data["name"], "Viajes")
51+
52+
@patch.object(UserExpenseTypeViewSet, "destroy")
53+
@patch.object(CognitoService, "login_user")
54+
def test_destroy(self, mock_login_user, mock_destroy):
55+
mock_login_user.return_value = {
56+
"AuthenticationResult": {"AccessToken": "mock_access_token", "IdToken": "mock_id_token"}
57+
}
58+
mock_destroy.return_value = Response(status=status.HTTP_204_NO_CONTENT)
59+
60+
request = self.client.delete("/user_expense_type/1/")
61+
request.headers["Authorization"] = "Bearer mock_access_token"
62+
63+
response = self.client.delete("/user_expense_type/1/")
64+
65+
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
66+
67+
@patch.object(UserExpenseTypeViewSet, "partial_update")
68+
@patch.object(CognitoService, "login_user")
69+
def test_partial_update(self, mock_login_user, mock_partial_update):
70+
mock_login_user.return_value = {
71+
"AuthenticationResult": {"AccessToken": "mock_access_token", "IdToken": "mock_id_token"}
72+
}
73+
new_user_expense_type_data = {
74+
"name": "Viajes",
75+
"description": "Gastos incurridos en viajes por placer",
76+
"set_by_user": True,
77+
}
78+
mock_partial_update.return_value = Response(status=status.HTTP_200_OK, data=new_user_expense_type_data)
79+
80+
request = self.client.put("/user_expense_type/1/", {"description": "Gastos incurridos en viajes por placer"})
81+
request.headers["Authorization"] = "Bearer mock_access_token"
82+
83+
response = self.client.put("/user_expense_type/1/", {"description": "Gastos incurridos en viajes por placer"})
84+
85+
self.assertEqual(response.status_code, status.HTTP_200_OK)
86+
self.assertEqual(response.data["description"], "Gastos incurridos en viajes por placer")

user_expense_type/urls.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# user_expense_type/urls.py
2+
from django.urls import path
3+
from .views import UserExpenseTypeViewSet
4+
5+
urlpatterns = [
6+
path(
7+
"",
8+
UserExpenseTypeViewSet.as_view({"get": "list", "post": "create"}),
9+
name="user_expense_type",
10+
),
11+
path(
12+
"<int:pk>/",
13+
UserExpenseTypeViewSet.as_view({"get": "retrieve", "delete": "destroy", "put": "partial_update"}),
14+
name="user_expense_type_detail",
15+
),
16+
]

user_expense_type/views.py

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from rest_framework import viewsets, status
2+
from rest_framework.response import Response
3+
from .models import UserExpenseType
4+
from .serializers import UserExpenseTypeSerializer
5+
from authentication.decorators import cognito_authenticated
6+
import jwt
7+
8+
9+
class UserExpenseTypeViewSet(viewsets.ViewSet):
10+
def get_user_id_from_token(self, request):
11+
try:
12+
authorization_header = request.headers.get("Authorization")
13+
if not authorization_header:
14+
raise Exception("Authorization header not found")
15+
16+
token = authorization_header.split()[1]
17+
decoded_token = jwt.decode(token, options={"verify_signature": False})
18+
username = decoded_token.get("username")
19+
if not username:
20+
raise Exception("User ID not found in token")
21+
return username
22+
except jwt.DecodeError:
23+
raise Exception("Invalid token")
24+
except jwt.ExpiredSignatureError:
25+
raise Exception("Expired token")
26+
except Exception as e:
27+
raise Exception(f"Error decoding token: {e}")
28+
29+
@cognito_authenticated
30+
def list(self, request):
31+
try:
32+
username = self.get_user_id_from_token(request)
33+
user_expense_types = UserExpenseType.objects.filter(username=username)
34+
serializer = UserExpenseTypeSerializer(user_expense_types, many=True)
35+
for ser in serializer.data:
36+
ser.pop("username")
37+
ser.pop("created_at")
38+
ser.pop("updated_at")
39+
return Response(data=serializer.data)
40+
except UserExpenseType.DoesNotExist:
41+
return Response({"error": "UserExpenseType not found"}, status=status.HTTP_404_NOT_FOUND)
42+
except Exception as e:
43+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
44+
45+
@cognito_authenticated
46+
def retrieve(self, request, pk=None):
47+
try:
48+
username = self.get_user_id_from_token(request)
49+
user_expense_type = UserExpenseType.objects.get(id=pk, username=username)
50+
serializer = UserExpenseTypeSerializer(user_expense_type)
51+
response = {
52+
"name": serializer.data["name"],
53+
"description": serializer.data["description"],
54+
"set_by_user": serializer.data["set_by_user"],
55+
}
56+
return Response(data=response)
57+
except UserExpenseType.DoesNotExist:
58+
return Response({"error": "UserExpenseType not found"}, status=status.HTTP_404_NOT_FOUND)
59+
except Exception as e:
60+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
61+
62+
@cognito_authenticated
63+
def create(self, request):
64+
try:
65+
username = self.get_user_id_from_token(request)
66+
data = request.data.copy()
67+
data["username"] = username
68+
69+
serializer = UserExpenseTypeSerializer(data=data)
70+
if serializer.is_valid():
71+
serializer.save()
72+
response = {
73+
"name": serializer.data["name"],
74+
"description": serializer.data["description"],
75+
"set_by_user": serializer.data["set_by_user"],
76+
}
77+
return Response(data=response, status=status.HTTP_201_CREATED)
78+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
79+
except Exception as e:
80+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
81+
82+
@cognito_authenticated
83+
def destroy(self, request, pk=None):
84+
try:
85+
username = self.get_user_id_from_token(request)
86+
user_expense_type = UserExpenseType.objects.get(id=pk, username=username)
87+
user_expense_type.delete()
88+
return Response(status=status.HTTP_204_NO_CONTENT)
89+
except UserExpenseType.DoesNotExist:
90+
return Response({"error": "UserExpenseType not found"}, status=status.HTTP_404_NOT_FOUND)
91+
except Exception as e:
92+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
93+
94+
@cognito_authenticated
95+
def partial_update(self, request, pk=None):
96+
try:
97+
username = self.get_user_id_from_token(request)
98+
user_expense_type = UserExpenseType.objects.get(id=pk, username=username)
99+
serializer = UserExpenseTypeSerializer(user_expense_type, data=request.data, partial=True)
100+
if serializer.is_valid():
101+
serializer.save()
102+
response = {
103+
"name": serializer.data["name"],
104+
"description": serializer.data["description"],
105+
"set_by_user": serializer.data["set_by_user"],
106+
}
107+
return Response(data=response)
108+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
109+
except UserExpenseType.DoesNotExist:
110+
return Response({"error": "UserExpenseType not found"}, status=status.HTTP_404_NOT_FOUND)
111+
except Exception as e:
112+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

0 commit comments

Comments
 (0)