Skip to content

Commit 65ba66e

Browse files
feat(reviews): implement Review endpoints with serializers and enhanced testing
- Added ReviewViewSet for handling review operations on products - Implemented ReviewSerializer for review data validation and transformation - Established complete CRUD endpoints for product reviews - Improved documentation for better clarity on review functionality - Rebuilt and enhanced test suites across all apps to reflect recent improvements - Ensured test coverage accounts for latest changes from previous pushes The review system is now fully integrated with products, providing users the ability to create, read, update, and delete their product reviews.
1 parent 1817ed4 commit 65ba66e

File tree

14 files changed

+372
-87
lines changed

14 files changed

+372
-87
lines changed

.gitignore

-15 Bytes
Binary file not shown.

cart/tests.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
class CartTests(TestCase):
1717
def setUp(self):
18-
self.user = User.objects.create_user(username='testuser', password='testpass')
18+
self.user = User.objects.create_user(username='testuser', password='testpass', email='[email protected]')
1919
self.category = Category.objects.create(name='Test Category')
2020
self.product = Product.objects.create(
2121
user=self.user,
@@ -53,7 +53,7 @@ def test_get_total_price(self):
5353

5454
class CartAPITests(APITestCase):
5555
def setUp(self):
56-
self.user = User.objects.create_user(username='testuser', password='testpass')
56+
self.user = User.objects.create_user(username='testuser', password='testpass', email='[email protected]')
5757
self.category = Category.objects.create(name='Test Category')
5858
self.product = Product.objects.create(
5959
user=self.user,
@@ -62,7 +62,7 @@ def setUp(self):
6262
stock=100,
6363
category=self.category
6464
)
65-
self.cart_url = reverse('api-v1:cart')
65+
self.cart_url = reverse('api-v1:cart-list')
6666
self.client.login(username='testuser', password='testpass')
6767

6868
def test_get_cart(self):
@@ -73,17 +73,17 @@ def test_get_cart(self):
7373

7474
def test_add_product_to_cart(self):
7575
response = self.client.post(
76-
reverse('api-v1:cart-product', kwargs={'product_id': self.product.product_id}),
76+
reverse('api-v1:cart-add-to-cart', kwargs={'product_id': self.product.product_id}),
7777
data={'quantity': 2}
7878
)
7979
self.assertEqual(response.status_code, status.HTTP_200_OK)
8080
self.assertEqual(response.data['message'], 'Product added/updated in cart')
8181

8282
def test_remove_product_from_cart(self):
8383
self.client.post(
84-
reverse('api-v1:cart-product', kwargs={'product_id': self.product.product_id}),
84+
reverse('api-v1:cart-add-to-cart', kwargs={'product_id': self.product.product_id}),
8585
data={'quantity': 2}
8686
)
87-
response = self.client.delete(reverse('api-v1:cart-product', kwargs={'product_id': self.product.product_id}))
87+
response = self.client.delete(reverse('api-v1:cart-remove-from-cart', kwargs={'product_id': self.product.product_id}))
8888
self.assertEqual(response.status_code, status.HTTP_200_OK)
8989
self.assertEqual(response.data['message'], 'Product removed from cart')

config/uwsgi/uwsgi.ini

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
[uwsgi]
2-
socket=/code/ecommerce_api/uwsgi_app.sock
3-
chdir = /code/ecommerce_api/
4-
module=ecommerce_api.wsgi:application
5-
master=true
6-
chmod-socket=666
7-
uid=www-data
8-
gid=www-data
9-
vacuum=true
2+
socket = /code/ecommerce_api/uwsgi_app.sock
3+
http = :8000
4+
chdir = /code
5+
module = ecommerce_api.wsgi:application
6+
master = true
7+
chmod-socket = 666
8+
uid = www-data
9+
gid = www-data
10+
vacuum = true

ecommerce_api/settings/base.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@
280280
"name": "Cart",
281281
"description": "Endpoints for managing the shopping cart."
282282
},
283+
{
284+
"name": "Reviews",
285+
"description": "Endpoints for managing product reviews."
286+
}
283287
],
284288
# Authentication configuration
285289
"SECURITY": [
@@ -298,7 +302,7 @@
298302
CACHES = {
299303
"default": {
300304
"BACKEND": "django_redis.cache.RedisCache",
301-
"LOCATION": "redis://cache:6379/1", # use 'cache' service name
305+
"LOCATION": f"redis://{config('REDIS_HOST')}:6379/1", # use 'cache' service name
302306
"OPTIONS": {
303307
"CLIENT_CLASS": "django_redis.client.DefaultClient",
304308
}
@@ -309,7 +313,7 @@
309313
'default': {
310314
'BACKEND': 'channels_redis.core.RedisChannelLayer',
311315
'CONFIG': {
312-
'hosts': [('cache', 6379)], # use 'cache' service name
316+
'hosts': [(config('REDIS_HOST'), 6379)], # use 'cache' service name
313317
},
314318
},
315319
}
@@ -413,4 +417,4 @@
413417
DOMAIN = config('DOMAIN')
414418
SITE_NAME = config('SITE_NAME')
415419

416-
TAGGIT_CASE_INSENSITIVE = True
420+
TAGGIT_CASE_INSENSITIVE = True

orders/permissions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from rest_framework import permissions
2+
3+
4+
class IsAdminOrOwner(permissions.BasePermission):
5+
def has_object_permission(self, request, view, obj):
6+
if request.user and request.user.is_staff:
7+
return True
8+
return obj.user == request.user
9+
10+
def has_permission(self, request, view):
11+
return request.user and request.user.is_authenticated

orders/tests.py

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.utils import timezone
99
from rest_framework import status
1010
from rest_framework.test import APITestCase
11+
from rest_framework_simplejwt.tokens import RefreshToken
1112

1213
from coupons.models import Coupon
1314
from orders.models import Order, OrderItem
@@ -17,11 +18,12 @@
1718

1819

1920
class OrderAPITests(APITestCase):
21+
2022
def setUp(self):
2123
self.admin = User.objects.create_superuser(
2224
username='admin', password='pass', email='[email protected]'
2325
)
24-
self.user = User.objects.create_user(username='user', password='pass')
26+
self.user = User.objects.create_user(username='user', password='pass', email='[email protected]')
2527
self.category = Category.objects.create(name='Test Category')
2628
self.product1 = Product.objects.create(
2729
user=self.admin,
@@ -41,8 +43,11 @@ def setUp(self):
4143
self.url = reverse('api-v1:order-list')
4244
self.cart_session_id = getattr(settings, 'CART_SESSION_ID', 'cart')
4345

46+
# Set JWT token for admin by default
47+
refresh = RefreshToken.for_user(self.admin)
48+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
49+
4450
def prepare_cart_session(self, products):
45-
"""Helper method to prepare cart session data"""
4651
session = self.client.session
4752
session[self.cart_session_id] = {}
4853
for product, quantity in products:
@@ -54,7 +59,8 @@ def prepare_cart_session(self, products):
5459
session.save()
5560

5661
def test_create_order_empty_cart(self):
57-
self.client.login(username='admin', password='pass')
62+
refresh = RefreshToken.for_user(self.admin)
63+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
5864
session = self.client.session
5965
if self.cart_session_id in session:
6066
del session[self.cart_session_id]
@@ -65,7 +71,8 @@ def test_create_order_empty_cart(self):
6571
self.assertEqual(response.data['error'], "You cannot place an order with an empty cart.")
6672

6773
def test_create_order_with_cart(self):
68-
self.client.login(username='admin', password='pass')
74+
refresh = RefreshToken.for_user(self.admin)
75+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
6976
self.prepare_cart_session([(self.product1, 2)])
7077

7178
with patch('orders.views.send_order_confirmation_email.delay') as mock_task:
@@ -82,34 +89,35 @@ def test_create_order_with_cart(self):
8289
mock_task.assert_called_once_with(order.order_id)
8390

8491
def test_create_order_with_multiple_products(self):
85-
self.client.login(username='admin', password='pass')
92+
refresh = RefreshToken.for_user(self.admin)
93+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
8694
self.prepare_cart_session([(self.product1, 1), (self.product2, 3)])
8795

8896
response = self.client.post(self.url)
8997
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
9098

9199
order = Order.objects.get(order_id=response.data['order_id'])
92100
self.assertEqual(order.items.count(), 2)
93-
self.assertEqual(order.quantity, 4) # Number of distinct products
94-
self.assertEqual(order.get_total_cost_before_discount(), decimal.Decimal('140.00')) # 50 + (30*3)
95-
101+
self.assertEqual(order.quantity, 4)
102+
self.assertEqual(order.get_total_cost_before_discount(), decimal.Decimal('140.00'))
96103

97104
def test_order_list_filtered_by_user(self):
98-
# Create orders for both users
99105
order1 = Order.objects.create(user=self.admin, quantity=1)
100106
OrderItem.objects.create(order=order1, product=self.product1, quantity=1)
101107

102108
order2 = Order.objects.create(user=self.user, quantity=1)
103109
OrderItem.objects.create(order=order2, product=self.product2, quantity=1)
104110

105-
# Test admin can see all orders
106-
self.client.login(username='admin', password='pass')
111+
# Admin can see all orders
112+
refresh = RefreshToken.for_user(self.admin)
113+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
107114
response = self.client.get(self.url)
108115
self.assertEqual(response.status_code, status.HTTP_200_OK)
109116
self.assertEqual(len(response.data['data']), 2)
110117

111-
# Test regular user can only see their own orders
112-
self.client.login(username='user', password='pass')
118+
# Regular user can only see their own orders
119+
refresh = RefreshToken.for_user(self.user)
120+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
113121
response = self.client.get(self.url)
114122
self.assertEqual(response.status_code, status.HTTP_200_OK)
115123
self.assertEqual(len(response.data['data']), 1)
@@ -120,30 +128,34 @@ def test_order_retrieve(self):
120128
OrderItem.objects.create(order=order, product=self.product1, quantity=2)
121129
detail_url = reverse('api-v1:order-detail', kwargs={'pk': order.order_id})
122130

123-
# Test admin can retrieve any order
124-
self.client.login(username='admin', password='pass')
131+
# Admin can retrieve any order
132+
refresh = RefreshToken.for_user(self.admin)
133+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
125134
response = self.client.get(detail_url)
126135
self.assertEqual(response.status_code, status.HTTP_200_OK)
127136
self.assertEqual(response.data['order_id'], str(order.order_id))
128137

129-
# Test regular user cannot retrieve other user's order
130-
self.client.login(username='user', password='pass')
138+
# Regular user cannot retrieve other user's order
139+
refresh = RefreshToken.for_user(self.user)
140+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
131141
response = self.client.get(detail_url)
132142
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
133143

134144
def test_order_update_permissions(self):
135145
order = Order.objects.create(user=self.admin, quantity=1)
136146
OrderItem.objects.create(order=order, product=self.product1, quantity=1)
137147
detail_url = reverse('api-v1:order-detail', kwargs={'pk': order.order_id})
138-
data = {'status': 'CO'} # Completed
148+
data = {'status': 'CO'}
139149

140-
# Test regular user cannot update
141-
self.client.login(username='user', password='pass')
150+
# Regular user cannot update
151+
refresh = RefreshToken.for_user(self.user)
152+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
142153
response = self.client.patch(detail_url, data)
143154
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
144155

145-
# Test admin can update
146-
self.client.login(username='admin', password='pass')
156+
# Admin can update
157+
refresh = RefreshToken.for_user(self.admin)
158+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
147159
response = self.client.patch(detail_url, data)
148160
self.assertEqual(response.status_code, status.HTTP_200_OK)
149161
order.refresh_from_db()
@@ -154,13 +166,15 @@ def test_order_delete_permissions(self):
154166
OrderItem.objects.create(order=order, product=self.product1, quantity=1)
155167
detail_url = reverse('api-v1:order-detail', kwargs={'pk': order.order_id})
156168

157-
# Test regular user cannot delete
158-
self.client.login(username='user', password='pass')
169+
# Regular user cannot delete
170+
refresh = RefreshToken.for_user(self.user)
171+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
159172
response = self.client.delete(detail_url)
160173
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
161174

162-
# Test admin can delete
163-
self.client.login(username='admin', password='pass')
175+
# Admin can delete
176+
refresh = RefreshToken.for_user(self.admin)
177+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
164178
response = self.client.delete(detail_url)
165179
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
166180
self.assertFalse(Order.objects.filter(pk=order.order_id).exists())
@@ -170,7 +184,8 @@ def test_order_serializer_fields(self):
170184
OrderItem.objects.create(order=order, product=self.product1, quantity=2)
171185
detail_url = reverse('api-v1:order-detail', kwargs={'pk': order.order_id})
172186

173-
self.client.login(username='admin', password='pass')
187+
refresh = RefreshToken.for_user(self.admin)
188+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
174189
response = self.client.get(detail_url)
175190

176191
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -183,7 +198,7 @@ def test_order_serializer_fields(self):
183198
self.assertIn('original_price', data)
184199
self.assertEqual(data['original_price'], '100.00')
185200
self.assertIn('total_price', data)
186-
self.assertEqual(data['total_price'], '90.00') # With 10% coupon
201+
self.assertEqual(data['total_price'], '90.00')
187202
self.assertIn('coupon', data)
188203
self.assertEqual(data['coupon'], 'TEST10')
189204
self.assertIn('discount', data)
@@ -193,22 +208,25 @@ def test_order_status_choices(self):
193208
order = Order.objects.create(user=self.admin, quantity=1)
194209
detail_url = reverse('api-v1:order-detail', kwargs={'pk': order.order_id})
195210

196-
self.client.login(username='admin', password='pass')
211+
refresh = RefreshToken.for_user(self.admin)
212+
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')
197213
response = self.client.get(detail_url)
198-
self.assertEqual(response.data['status'], 'PE') # Pending by default
214+
self.assertEqual(response.data['status'], 'PE')
199215

200-
# Test valid status update
201-
data = {'status': 'CO'} # Completed
216+
# Valid status update
217+
data = {'status': 'CO'}
202218
response = self.client.patch(detail_url, data)
203219
self.assertEqual(response.status_code, status.HTTP_200_OK)
204220
self.assertEqual(response.data['status'], 'CO')
205221

206-
# Test invalid status
222+
# Invalid status
207223
data = {'status': 'INVALID'}
208224
response = self.client.patch(detail_url, data)
209225
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
210226

211227
def test_unauthenticated_access(self):
228+
self.client.credentials() # Remove authentication
229+
212230
# List
213231
response = self.client.get(self.url)
214232
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

orders/views.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
OpenApiResponse,
66
extend_schema_view,
77
)
8-
from rest_framework import permissions, viewsets, status
8+
from rest_framework import viewsets, status
99
from rest_framework.permissions import IsAdminUser, IsAuthenticated
1010
from rest_framework.response import Response
1111

1212
from cart.cart import Cart
1313
from coupons.models import Coupon
1414
from .models import Order, OrderItem
15+
from .permissions import IsAdminOrOwner
1516
from .serializers import OrderSerializer
1617
from .tasks import send_order_confirmation_email
1718

@@ -74,7 +75,7 @@
7475
class OrderViewSet(viewsets.ModelViewSet):
7576
queryset = Order.objects.prefetch_related('items__product')
7677
serializer_class = OrderSerializer
77-
permission_classes = [IsAdminUser]
78+
permission_classes = [IsAuthenticated, IsAdminOrOwner]
7879

7980
def get_queryset(self):
8081
qs = super().get_queryset()
@@ -84,10 +85,10 @@ def get_queryset(self):
8485

8586
def get_permissions(self):
8687
if self.action in ['update', 'partial_update', 'destroy']:
87-
self.permission_classes = [permissions.IsAdminUser]
88+
permission_classes = [IsAdminUser]
8889
else:
89-
self.permission_classes = [IsAuthenticated]
90-
return super().get_permissions()
90+
permission_classes = [IsAdminOrOwner]
91+
return [permission() for permission in permission_classes]
9192

9293
def create(self, request, *args, **kwargs):
9394
logger.info("Creating order for user id: %s", request.user.id)
@@ -99,7 +100,8 @@ def create(self, request, *args, **kwargs):
99100
status=status.HTTP_400_BAD_REQUEST)
100101
coupon = None
101102
try:
102-
if request.session['coupon_id']:
103+
coupon_id = request.session.get('coupon_id', None)
104+
if coupon_id:
103105
coupon = Coupon.objects.get(id=request.session['coupon_id'])
104106
except Coupon.DoesNotExist:
105107
logger.warning("Invalid coupon ID in session for user id: %s", request.user.id)

requirements.txt

60 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)