Building a Food Delivery App With Django and Python 3: Part 6 Order Details

Video Tutorial

Code on Github

In this tutorial we will continue to build the food delivery application. This time, we will add an order details page. On this page we will show some details from the order and we will add a button where the order can be marked as shipped. Let’s start by updating our OrderModel in our customer/models.py file:

class OrderModel(models.Model):
created_on = models.DateTimeField(auto_now_add=True)
price = models.DecimalField(max_digits=7, decimal_places=2)
items = models.ManyToManyField('MenuItem', related_name='order', blank=True)
name = models.CharField(max_length=50, blank=True)
email = models.EmailField(max_length=50, blank=True)
street = models.CharField(max_length=50, blank=True)
city = models.CharField(max_length=50, blank=True)
state = models.CharField(max_length=15, blank=True)
zip_code = models.IntegerField(blank=True, null=True)
is_paid = models.BooleanField(default=False)
is_shipped = models.BooleanField(default=False)
def __str__(self):
return f'Order: {self.created_on.strftime("%b %d %Y %I:%M %p")}'

All we are doing is adding a boolean field to check to see if the order has been shipped or not.

Next, let’s create our view:

class OrderDetails(LoginRequiredMixin, UserPassesTestMixin, View):
def get(self, request, pk, *args, **kwargs):
order = OrderModel.objects.get(pk=pk)
context = {'order': order}
return render(request, 'restaurant/order-details.html', context) def test_func(self):
return self.request.user.groups.filter(name='Staff').exists()

We need to add the same mixins that we used before to make sure the page is only accessible by someone logged in with a staff account. Then we are just getting the pk from the url and getting the object that matches that url.

Finally, we are rendering our html template for the order details page. Let’s build that template now:

{% extends 'restaurant/base.html' %}{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 col-sm-12 text-center mt-3">
<h1>Order ID: {{ order.pk }}</h1>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-12 col-sm-12 mt-5">
<h3>Customer Information: </h3>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-sm-12">
<p><span style="font-weight: 400;">Name:</span> {{ order.name }}</p>
<p><span style="font-weight: 400;">Email:</span> {{ order.email }}</p>
<h5 class="pt-3">Address Information: </h5>
<p><span style="font-weight: 400;">Street:</span> {{ order.street }}</p>
<p><span style="font-weight: 400;">City:</span> {{ order.city }}</p>
<p><span style="font-weight: 400;">State:</span> {{ order.state }}</p>
<p><span style="font-weight: 400;">Zip Code:</span> {{ order.zip_code }}</p>
</div>
<div class="col-md-6 col-sm-12">
<h5>Payment and Shipping Information: </h5>
<p class="mt-3">{% if order.is_paid %}
<p><i style="color: green;" class="fas fa-check"></i><span class="pl-2">Order Has Been Paid For!</span></p>
{% else %}
<p><i style="color: red;" class="fas fa-times"></i><span class="pl-2">Order Has Not Been Paid For</span></p>
{% endif %}</p>
{% if order.is_shipped %}
<p><i style="color: green;" class="fas fa-check"></i><span class="pl-2">Order Has Been Shipped!</span></p>
{% else %}
<form method="POST">
{% csrf_token %}
<button type="submit" class="btn btn-outline-success">Mark as Shipped</button>
</form>
{% endif %}
</div>
</div>
</div>
{% endblock content %}

This template is just listing out all of the data from our OrderModel object. You’ll see that we use a couple if statements to show a check icon or a X icon based on if the boolean fields are true or false. Let’s add a url path for this template real quick:

from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from .views import Dashboard, OrderDetails
urlpatterns = [
path('dashboard/', Dashboard.as_view(), name='dashboard'),
path('order/<int:pk>/', OrderDetails.as_view(), name='order-details')
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

All we are doing here is passing in the pk into the url, which we are using in our views.py file to get the OrderModel object.

We also put a form with a button at the bottom of the HTML template file. When we click the button we will send a POST request to this same url. We can add a post method in our view class to handle this. Once we receive a post request, we want to mark the order as paid. Let’s do this now in our restaurant/views.py OrderDetails class:

class OrderDetails(LoginRequiredMixin, UserPassesTestMixin, View):
def get(self, request, pk, *args, **kwargs):
order = OrderModel.objects.get(pk=pk)
context = {'order': order}
return render(request, 'restaurant/order-details.html', context)

def post(self, request, pk, *args, **kwargs):
order = OrderModel.objects.get(pk=pk)
order.is_shipped = True
order.save()

context = {'order': order}
return render(request, 'restaurant/order-details.html', context)

def test_func(self):
return self.request.user.groups.filter(name='Staff').exists()

In the post method, we are getting the pk from the url and setting the is_shipped to True. We then save the object and render the exact same template as before. Now we should see a checkmark that tells us the order is shipped after clicking the button.

Let’s add an edit icon to our dashboard.html to get to this view. Here is the updated table in teh restaurant/templates/restaurant/dashboard.html file:

<table class="table table-hover table-striped">
<thead>
<tr>
<th scope="col">Order ID</th>
<th scope="col">Price</th>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Street</th>
<th scope="col">City</th>
<th scope="col">State</th>
<th scope="col">Zip Code</th>
<th scope="col">Is Paid?</th>
<th scope="col">Details</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<th scope="row">{{ order.pk }}</th>
<td>{{ order.price }}</td>
<td>{{ order.name }}</td>
<td>{{ order.email }}</td>
<td>{{ order.street }}</td>
<td>{{ order.city }}</td>
<td>{{ order.state }}</td>
<td>{{ order.zip_code }}</td>
<td>
{% if order.is_paid %}
<i style="color: green;" class="fas fa-check"></i>
{% else %}
<i style="color: red;" class="fas fa-times"></i>
{% endif %}
</td>
<td><a href="{% url 'order-details' order.pk %}"><i class="far fa-edit"></i></a></a></td>
</tr>
{% endfor %}
</tbody>
</table>

Now that we have everything set up and working, there is one small change that I want to make to the dashboard view class. Right now, all of today’s orders shows up in the list, but it would probably be better if only the orders from today that aren’t marked as shipped will show up in the list. To do this, I don’t want to change the filter parameters because that would change the totals being calculated at the top of the dashboard. I want to create a new list called unshipped_orders and when we loop through the orders to calculate the price, I also want to check to see if the order is marked as shipped. If it is not, then I want to append it to the list and pass that unshipped_orders list in as the orders variable in our context. Here is an example of that in our restaurant/views.py Dashboard class:

class Dashboard(LoginRequiredMixin, UserPassesTestMixin, View):
def get(self, request, *args, **kwargs):
today = datetime.today()
orders = OrderModel.objects.filter(created_on__year=today.year, created_on__month=today.month, created_on__day=today.day)
total_revenue = 0
unshipped_orders = [] for order in orders:
total_revenue += order.price
if not order.is_shipped:
unshipped_orders.append(order)

context = {
'orders': unshipped_orders,
'total_revenue': total_revenue,
'total_orders': len(orders)
}
return render(request, 'restaurant/dashboard.html', context)
def test_func(self):
return self.request.user.groups.filter(name='Staff').exists()

You can see that those changes that were explained above are made in the for order in orders loop. Now only the orders from today that are not currently shipped will show up in our table.

That is where we will stop today, with that change we have most of the basic functionality added for our restaurant side of the application. We will come back later and add some extra more advanced pieces to the app.