Blog

Drop Shipping Online Store on Django (Part 7)

7. Data Visualization Using Forms and Views

Attention! If you're having trouble following the previous step, you can visit relevant lesson , download the archive of the previous step, install it, and start this lesson exactly where the previous one ended!

In this course, we will go through all the stages of creating a new project together:

Add pages: shop.html, shop-details.html and cart.html

Our project is nearing completion, from which we are literally a few last steps away.

The most important functional part, which accounts for all the basic operations with the database, was completed in the 5th lesson. Those, in fact, we already have all the business logic and we just have to add a user interface for displaying database data and changing them.

To warm up, let's first do what we already know well - copy the last 3 pages from the template to the templates/shop folder that are still there: shop.html, shop-details.html and cart.html. And let's try to open them in the project.

To do this, first of all, we will remove from them what is already in the base template. Next, add links to these pages to the shop/urls.py configurator.

urlpatterns = [
    path('fill-database/', views.fill_database, name='fill_database'),
    path('', TemplateView.as_view(template_name='shop/shop.html'), name='shop'),
    path('cart_view/',  
         TemplateView.as_view(template_name='shop/cart.html'), name='cart_view'),
    path('detail/<int:pk>/',  
         TemplateView.as_view(template_name='shop/shop-details.html'), 
         name='shop_detail')
]

By the way, pay attention: not only links are shown here, but also the views themselves! Before us is an example of that rare case when the view can not be imported from the views.py module. The point is that template_name (the only required parameter for TemplateView) can be specified in the as_view() method as a kwargs argument. And then the view will “run” directly from the shop/urls.py configurator!

And the final touch - let's add links to the SHOP and CART pages to the main menu in order to immediately check if we made any mistakes in the layout when embedding the base template. There is nowhere to add a link to the detail page yet. Therefore, to check this page for errors, it will have to be called “manually”.

New Generic View Types: ListView and DetailView

We started our acquaintance with the generic view in the 4th lesson, and already then we were convinced how powerful and concise Django's tool is. And from the example above, we also learned that TemplateView can generally be written in one line directly in the shop/urls.py configurator.

Two other representatives of this category, namely: ListView and DetailView, have the same amazing properties. In the simplest version of the view, you can specify only the names of the model and template. And this will be enough for the html template to get all the necessary information and be able to display all the rows of the Products table (ListView), or all the fields of a single product whose pk will be listed at the end of the url (DetailView).

(To be fair, it's not even necessary to specify a template name - by default, this name is already included in the generic view settings. More information can be found here: Class-based views)

Creating ProductsListView

When going to the product page via the link /shop/, the user expects to see the entire list of products. Therefore, we change the already created test view TemplateView, which was created solely to test the template, with a new one - ProductsListView:

from django.views.generic import ListView

from shop.models import Product


class ProductsListView(ListView):
    model = Product
    template_name = 'shop/shop.html'

Do not forget to also make changes to the shop/urls.py configurator:

urlpatterns = [
    path('', views.ProductsListView.as_view(), name='shop'),
    path('cart_view/',
         TemplateView.as_view(template_name='shop/cart.html'), name='cart_view'),
    path('detail/<int:pk>/',
         TemplateView.as_view(template_name='shop/shop-details.html'),
         name='shop_detail'),
    path('fill-database/', views.fill_database, name='fill_database'),
]

By default, all data from the Product model will automatically be passed to the template as an object_list object. Therefore, by creating a cycle through object_list in the html template, we will get access to all product products, which means we can get the values of all fields of interest to us:

# one block
                {% for product in object_list %}
					<div class="col-12 col-lg-4 col-md-6 item">
                        <div class="card" style="width: 18rem;">
                            <form method="post" action="">
                                <img src="{{product.image_url}}" class="card-img-top" alt="...">
                                <div class="card-body">
                                    <h5 class="card-title"><b>{{ product.name}}</b></h5>
                                    <p class="card-text">
                                        {{ product.description }}
                                    </p>
                                </div>
                                <ul class="list-group list-group-flush">
                                    <li class="list-group-item">Price: {{ product.price }}</li>
                                    <li class="list-group-item">
										{% csrf_token %}
										<label class="form-label" for="id_quantity">Quantity:</label>
										<input type="number" name="quantity" value="1" min="1"
											   required id="id_quantity"/>
                                    </li>
                                </ul>
                                <div class="card-body">
                                    <button class="learn-more-btn" type="submit">buy now</button>
                                    <a class="contactus-bar-btn f_right" href="">detail</a>
                                    <br><br>
                                </div>
                            </form>
                        </div>
                    </div>
                {% endfor %}

The shop/shop.html template now contains 4 identical test blocks. Replacing one of them with the proposed option, we will get a complete list of all blocks with all the values of the product table.

The same concise and elegant solution exists in Django for displaying a single selected object. Only now it inherits not ListView, but DetailView:

class ProductsDetailView(DetailView):
    model = Product
    template_name = 'shop/shop-details.html'

And don't forget to change the name of the view in shop/urls.py:

urlpatterns = [
    path('', views.ProductsListView.as_view(), name='shop'),
    path('cart_view/',
         TemplateView.as_view(template_name='shop/cart.html'), name='cart_view'),
    path('detail/<int:pk>/', views.ProductsDetailView.as_view(),
         name='shop_detail'),
    path('fill-database/', views.fill_database, name='fill_database'),
]

Now that the actual product list is displayed, we can add a link to the detail page to the product loop in the shop/shop.html template:

<div class="card-body">
	<button class="learn-more-btn" type="submit">buy now</button>
	<a class="contactus-bar-btn f_right" href="{% url 'shop_detail' product.pk %}">detail</a>
	<br><br>
</div>

Pay attention to how a new compound link {% url 'shop_detail' product.pk %} is created: after a space, from the name url comes the product number in the database - product. pk. Of course, this result also needs to be verified.

Adding the selected item to the cart

So we have come to one of the most crucial moments - to filling the cart with the selected product. Of course, this logic can also be implemented using generic view. Moreover, this method is considered preferable, because, as you know, the more complex the task, the more understandable and concise the generic view code looks, compared to the code of a regular function.

But, this option may not be very clear for beginners. Therefore, to solve this problem, let's return to functions and forms again.

Let's start with the form. We have to fill in the OrderItem table, which is connected by ForeignKey to the Product and Order tables (models). Therefore, in fact, the only unknown field that we have to enter is the Quantity field, and we can take all other data from other tables. Therefore, our AddQuantityForm form will consist of only one field:

from django import forms

from shop.models import OrderItem


class AddQuantityForm(forms.ModelForm):
    class Meta:
        model = OrderItem
        fields = ['quantity']

Now back to the view, which we will call add_item_to_cart. And here our task is extremely simplified - we do not need to create a GET request. This already does the view ProductListView. Therefore, all that is required from the add_item_to_cart view is to receive and process the POST request:

@login_required(login_url=reverse_lazy('login'))
def add_item_to_cart(request, pk):
    if request.method == 'POST':
        quantity_form = AddQuantityForm(request.POST)
        if quantity_form.is_valid():
            quantity = quantity_form.cleaned_data['quantity']
            if quantity:
                cart = Order.get_cart(request.user)
                # product = Product.objects.get(pk=pk)
                product = get_object_or_404(Product, pk=pk)
                cart.orderitem_set.create(product=product,
                                          quantity=quantity,
                                          price=product.price)
                cart.save()
                return redirect('cart_view')
        else:
            pass
    return redirect('shop')

As you can see, if the form has passed validation, then the quantity object is created. We also remember that all OrderItem objects do not exist on their own, but are necessarily tied to some kind of cart or order. The get_cart method that we have already created in the previous lessons is able to provide us with the desired cart - the cart object. The product object, whose quantity we just confirmed, is easily obtained by request for pk=pk. By the way, product can be done using the get() method, but the get_object_or_404() variant is considered more reliable, which can handle a 404 error if the object with the desired pk will not be in the database.

Thus, we have already received all the fields necessary to create a new object. Therefore, now using cart.orderitem_set.create() we create a new model object OrderItem, and using the cart.save() method, we fix the connection of this object with an order basket.

The last thing left for us now is to add a new url to the configurator:

path('add-item-to-cart/<int:pk>', views.add_item_to_cart, name='add_item_to_cart'),

and then add this new url to the action attribute of the form tag on the shop/shop.html page:

<form method="post" action="{% url 'add_item_to_cart' product.pk %}">

Now you're ready to add orders to your shopping cart. True, we can only see the result in the admin panel.

And a very important touch, which almost remained behind the scenes. All users should be able to look at the product catalog. But only registered users can choose a product and add it to the cart. To solve the problem of protecting the add_item_to_cart view from unauthorized access by unauthorized users, the decorator @login_required will help. As you can see, this decorator will automatically redirect an unlogged user to the 'login' login page.

Cart management: display a list of items

Конечно же, добавленные в корзину позиции хотелось бы видеть не только в админке. Тем более, что у нас уже всё для этого готово. Кроме вью. Им и займёмся.

По сути, для отображения элементов корзины необходимо получить данные двух моделей:

  • Модели заказа Order (из которой с помощью метода get_cart(user) получаем объекта cart)
  • И модели OrderItem (для order=cart)

И затем передать эти данные в шаблон с помощью словаря context:

From the cart object in the template, only data related to the cart itself will be retrieved (in our case, only the order amount). We will get the data for each position of the item order as a result of the loop over the items object:

{% for item in items %}
    <div class="row">
        <div class="col-12 col-md-1 item">
            &nbsp;&nbsp;&nbsp;{{ forloop.counter }}
        </div>
        <div class="col-12 col-md-4 item">
            {{ item.product }}
        </div>
        <div class="col-12 col-md-2 item">
            {{ item.quantity }}
        </div>
        <div class="col-12 col-md-2 item">
            {{ item.price }}
        </div>
        <div class="col-12 col-md-2 item">
            {{ item.amount }}
        </div>
        <div class="col-12 col-md-1 item">
        </div>
    </div>
{% endfor %}

Cart Management: Deleting Items

Ошибаться может каждый. Поэтому пользователь должен иметь возможность удалять лишние позиции из корзины.

Как мы уже хорошо усвоили - все изменения базы данных должны проходить только через форму и метод POST. И удаление позиции в том числе.

Здесь стоит отметить, что для удаления элементов модели в Django имеется очень удобное generic view - DeleteView, для которого не нужно ни создавать отдельную форму в модуле shop/forms.py, ни специально описывать метод POST. Всё это уже создано в DeleteView по умолчанию:

@method_decorator(login_required, name='dispatch')
class CartDeleteItem(DeleteView):
    model = OrderItem
    template_name = 'shop/cart.html'
    success_url = reverse_lazy('cart_view')

    # Проверка доступа
    def get_queryset(self):
        qs = super().get_queryset()
        qs.filter(order__user=self.request.user)
        return qs

The only thing we have added here (changed, to be more precise) is the get_queryset method, which filters the OrderItem model data request by user.

We also do not need any additional output using the GET method: as in the case of adding a position to the cart, we will use the ready-made data that cart_view kindly provides us.

All that remains for us is to add a form to EVERY (!!!) position of the cart (fortunately, they are all displayed in a loop anyway) and a new url to call a new view CartDeleteItem.

Changes in the shop/cart.html template:

<div class="col-12 col-md-1 item">
    <form method="post" action="{% url 'cart_delete_item' item.pk %}">
        {% csrf_token %}
        <button type="submit" style="color: blue"><u>delete</u></button>
    </form>
</div>
Добавление в shop/urls.pyl:
path('delete_item/<int:pk>', views.CartDeleteItem.as_view(), name='cart_delete_item'),

Cart management: proceed to create an order

After everything you need has been successfully added to the cart, and everything superfluous has been safely removed from it, all that remains for us is to complete the recruitment process, change the cart status from STATUS_CART to STATUS_WAITING_FOR_PAYMENT and thus proceed to pay for the order.

The make_order method itself has long been created by us. It remains only to add a button to the basket sheet, upon pressing which this method will be launched.

The task for the view will be very simple - find the desired basket and apply the make_order method to it:

@login_required(login_url=reverse_lazy('login'))
def make_order(request):
    cart = Order.get_cart(request.user)
    cart.make_order()
    return redirect('shop')

After changing the status, there will be a redirect to the shop/shop.html page. After connecting online payment, this redirect can be replaced by a transition to the payment aggregator page. And you will also need to remember to add a new link to shop/urls.py:

path('make-order/', views.make_order, name='make_order'),

And add this link to the button on the cart page:

<a class="contactus-bar-btn f_right" href="{% url 'make_order' %}">
    Process to Payment
</a>

Conclusion

Our project has been completed. Of course, a lot was left behind the scenes: confirmation of registration by email, logging, connecting online payment, deployment, etc. etc.

However, the main functionality of the online store has been created. And improvement, as you know, never ends or ends.

In any case, if you have any questions, you know who to ask: it4each.com@gmail.com.

Good luck in creating your own online store and see you on new courses!

You can learn more about all the details of this stage from this video (RU voice):