Skip to content

Commit e89db5d

Browse files
committed
feat(orders): add admin, report order and reject order actions
1 parent 0306826 commit e89db5d

13 files changed

+234
-9
lines changed

apps/deliveries/admin.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Admin for Deliveries App."""
2+
3+
from django.contrib import admin
4+
5+
from apps.utilities.admin import BaseAdmin
6+
from .models import Delivery
7+
8+
9+
@admin.register(Delivery)
10+
class DeliveryAdmin(BaseAdmin):
11+
"""Admin for Delivery model."""
12+
13+
search_fields = ["order_id", "driver_id"]
14+
list_display = ["pk", "order_id", "is_completed", "is_available"]
15+
list_filter = ["status"]
16+
list_editable = ["is_completed", "is_available"]
17+
readonly_fields = ["pk", "created_at", "updated_at"]
18+
autocomplete_fields = ["order_id", "driver_id"]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Generated by Django 5.0.4 on 2024-07-29 17:15
2+
3+
import apps.utilities.paths
4+
import django.db.models.deletion
5+
import simple_history.models
6+
import uuid
7+
from django.conf import settings
8+
from django.db import migrations, models
9+
10+
11+
class Migration(migrations.Migration):
12+
13+
initial = True
14+
15+
dependencies = [
16+
('drivers', '0001_initial'),
17+
('orders', '0003_historicalorder_is_payment_order_is_payment_and_more'),
18+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
19+
]
20+
21+
operations = [
22+
migrations.CreateModel(
23+
name='HistoricalDelivery',
24+
fields=[
25+
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)),
26+
('is_available', models.BooleanField(db_index=True, default=True)),
27+
('created_at', models.DateTimeField(blank=True, editable=False)),
28+
('updated_at', models.DateTimeField(blank=True, editable=False)),
29+
('signature', models.TextField(blank=True, max_length=100)),
30+
('status', models.CharField(choices=[('pending', 'Pending'), ('assigned', 'Assigned'), ('picked_up', 'Picked Up'), ('delivered', 'Delivered'), ('failed', 'Failed')], default='pending', max_length=20)),
31+
('picked_up_at', models.DateTimeField(blank=True)),
32+
('delivered_at', models.DateTimeField(blank=True)),
33+
('is_completed', models.BooleanField(default=False)),
34+
('history_id', models.AutoField(primary_key=True, serialize=False)),
35+
('history_date', models.DateTimeField(db_index=True)),
36+
('history_change_reason', models.CharField(max_length=100, null=True)),
37+
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
38+
('driver_id', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='drivers.driver')),
39+
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
40+
('order_id', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='orders.order')),
41+
],
42+
options={
43+
'verbose_name': 'historical delivery',
44+
'verbose_name_plural': 'historical deliveries',
45+
'ordering': ('-history_date', '-history_id'),
46+
'get_latest_by': ('history_date', 'history_id'),
47+
},
48+
bases=(simple_history.models.HistoricalChanges, models.Model),
49+
),
50+
migrations.CreateModel(
51+
name='Delivery',
52+
fields=[
53+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
54+
('is_available', models.BooleanField(db_index=True, default=True)),
55+
('created_at', models.DateTimeField(auto_now_add=True)),
56+
('updated_at', models.DateTimeField(auto_now=True)),
57+
('signature', models.ImageField(blank=True, upload_to=apps.utilities.paths.signature_path)),
58+
('status', models.CharField(choices=[('pending', 'Pending'), ('assigned', 'Assigned'), ('picked_up', 'Picked Up'), ('delivered', 'Delivered'), ('failed', 'Failed')], default='pending', max_length=20)),
59+
('picked_up_at', models.DateTimeField(blank=True)),
60+
('delivered_at', models.DateTimeField(blank=True)),
61+
('is_completed', models.BooleanField(default=False)),
62+
('driver_id', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deliveries', to='drivers.driver')),
63+
('order_id', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='orders.order')),
64+
],
65+
options={
66+
'verbose_name': 'delivery',
67+
'verbose_name_plural': 'deliveries',
68+
'ordering': ['pk'],
69+
'indexes': [models.Index(fields=['order_id'], name='deliveries__order_i_8072ba_idx'), models.Index(fields=['driver_id'], name='deliveries__driver__493941_idx'), models.Index(fields=['status'], name='deliveries__status_bdfe1b_idx'), models.Index(fields=['order_id', 'status'], name='deliveries__order_i_e7aa5f_idx')],
70+
},
71+
),
72+
migrations.AddConstraint(
73+
model_name='delivery',
74+
constraint=models.UniqueConstraint(fields=('order_id',), name='unique_order_delivery'),
75+
),
76+
]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Generated by Django 5.0.4 on 2024-07-29 18:53
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('deliveries', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='delivery',
15+
name='delivered_at',
16+
field=models.DateTimeField(blank=True, null=True),
17+
),
18+
migrations.AlterField(
19+
model_name='delivery',
20+
name='picked_up_at',
21+
field=models.DateTimeField(blank=True, null=True),
22+
),
23+
migrations.AlterField(
24+
model_name='historicaldelivery',
25+
name='delivered_at',
26+
field=models.DateTimeField(blank=True, null=True),
27+
),
28+
migrations.AlterField(
29+
model_name='historicaldelivery',
30+
name='picked_up_at',
31+
field=models.DateTimeField(blank=True, null=True),
32+
),
33+
]

apps/deliveries/migrations/__init__.py

Whitespace-only changes.

apps/deliveries/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ class Delivery(BaseModel):
3434
choices=StatusChoices.choices,
3535
default=StatusChoices.PENDING,
3636
)
37-
picked_up_at = models.DateTimeField(blank=True)
38-
delivered_at = models.DateTimeField(blank=True)
37+
picked_up_at = models.DateTimeField(blank=True, null=True)
38+
delivered_at = models.DateTimeField(blank=True, null=True)
3939
is_completed = models.BooleanField(default=False)
4040

4141
objects = DeliveryManager()
@@ -57,7 +57,7 @@ class Meta:
5757
]
5858

5959
def __str__(self):
60-
return f"Delivery for {self.order.id} - {self.status}"
60+
return f"Delivery for {self.order_id} - {self.status}"
6161

6262
def save(self, *args, **kwargs):
6363
if self.status == StatusChoices.DELIVERED:

apps/orders/admin.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.contrib import admin
44

55
from apps.utilities.admin import BaseAdmin
6-
from .models import Order, OrderItem
6+
from .models import Order, OrderItem, OrderRating, OrderReport
77

88

99
@admin.register(Order)
@@ -27,3 +27,25 @@ class OrderItemAdmin(BaseAdmin):
2727
list_editable = ["is_available"]
2828
readonly_fields = ["pk", "price", "subtotal", "created_at", "updated_at"]
2929
ordering = ["created_at"]
30+
31+
32+
@admin.register(OrderRating)
33+
class OrderRatingAdmin(BaseAdmin):
34+
"""Admin for OrderRating model."""
35+
36+
search_fields = ["order_id", "user_id"]
37+
list_display = ["pk", "order_id"]
38+
list_filter = ["rating"]
39+
readonly_fields = ["pk", "created_at", "updated_at"]
40+
autocomplete_fields = ["order_id", "user_id"]
41+
42+
43+
@admin.register(OrderReport)
44+
class OrderReportAdmin(BaseAdmin):
45+
"""Admin for OrderReport model."""
46+
47+
search_fields = ["order_id", "user_id"]
48+
list_display = ["pk", "order_id", "user_id"]
49+
list_filter = ["status", "is_resolved"]
50+
readonly_fields = ["pk", "created_at", "updated_at"]
51+
autocomplete_fields = ["order_id", "user_id"]

apps/orders/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ class Meta:
134134
]
135135

136136
def __str__(self):
137-
return f"Rating for Order {self.order.id} by User {self.user.id}: {self.rating}"
137+
return f"Rating {self.pk}"
138138

139139

140140
class OrderReport(BaseModel):
@@ -179,4 +179,4 @@ class Meta:
179179
]
180180

181181
def __str__(self):
182-
return f"Report {self.order_id} by User {self.user_is}: {self.reason}"
182+
return f"Report {self.pk}"

apps/orders/serializers.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
RestaurantMinimalSerializer,
99
FoodMinimalSerializer,
1010
)
11-
from .models import Order, OrderItem
11+
from .models import Order, OrderItem, OrderReport
1212
from .services import OrderItemService
1313

1414

@@ -123,3 +123,14 @@ class Meta:
123123

124124
def validate_quantity(self, value):
125125
return OrderItemService.validate_quantity(value)
126+
127+
128+
class OrderReportWriteSerializer(serializers.ModelSerializer):
129+
"""Serializer for OrderReport model (Create/update)."""
130+
131+
class Meta:
132+
model = OrderReport
133+
fields = [
134+
"reason",
135+
"description",
136+
]

apps/orders/viewsets.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22

33
from django.shortcuts import get_object_or_404
44
from rest_framework.viewsets import ModelViewSet
5+
from rest_framework.decorators import action
6+
from rest_framework.response import Response
7+
from rest_framework import status
58

6-
from apps.users.permissions import IsOwner, IsClient
9+
from apps.users.permissions import IsOwner, IsClient, IsDriver
710
from apps.utilities.mixins import ListCacheMixin, LogicalDeleteMixin
8-
from .models import Order, OrderItem
11+
from apps.deliveries.models import Delivery
12+
from apps.deliveries.choices import StatusChoices
13+
from .models import Order, OrderItem, OrderReport
914
from .serializers import (
1015
OrderReadSerializer,
1116
OrderWriteSerializer,
1217
OrderMinimalSerializer,
1318
OrderItemReadSerializer,
1419
OrderItemWriteSerializer,
20+
OrderReportWriteSerializer,
1521
)
1622

1723

@@ -49,6 +55,65 @@ def get_serializer_class(self):
4955
def perform_create(self, serializer):
5056
serializer.save(user_id=self.request.user)
5157

58+
@action(
59+
detail=True,
60+
methods=["post"],
61+
permission_classes=[IsClient],
62+
url_path="report",
63+
)
64+
def report_order(self, request, *args, **kwargs):
65+
"""
66+
Action report a specific order by ID.
67+
68+
Endpoints:
69+
- GET api/v1/orders/{id}/report/
70+
"""
71+
order = self.get_object()
72+
serializer = OrderReportWriteSerializer(data=request.data)
73+
if serializer.is_valid():
74+
if OrderReport.objects.filter(
75+
order_id=order, user_id=request.user
76+
).exists():
77+
return Response(
78+
{"detail": "You have already reported this order."},
79+
status=status.HTTP_409_CONFLICT,
80+
)
81+
serializer.save(order_id=order, user_id=request.user)
82+
return Response(
83+
{"detail": "Your report has been submitted successfully."},
84+
status=status.HTTP_201_CREATED,
85+
)
86+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
87+
88+
@action(
89+
detail=True,
90+
methods=["patch"],
91+
permission_classes=[IsDriver],
92+
url_path="reject",
93+
)
94+
def reject_order(self, request, pk=None):
95+
"""
96+
Action to mark a specific order report as rejected.
97+
TODO: Concept tests, temporal
98+
99+
Endpoints:
100+
- PATCH api/v1/orders/{id}/reject/
101+
"""
102+
order = self.get_object()
103+
delivery = Delivery.objects.get(order_id=order)
104+
105+
if delivery:
106+
delivery.status = StatusChoices.FAILED
107+
delivery.save()
108+
return Response(
109+
{"detail": f"The order {order} was rejected."},
110+
status=status.HTTP_200_OK,
111+
)
112+
return Response(
113+
{"detail": f"Order {order} not found."},
114+
status=status.HTTP_404_NOT_FOUND,
115+
)
116+
52117

53118
class OrderItemViewSet(ModelViewSet):
54119
"""
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)