Grocery Bag using Django (Part-4) - CRUD Operations

Introduction

In the previous post, we had completed User Authentication in our web application. If you haven't read it till now, make sure you read it here. In this blog, we are going to add CRUD operation functionality to our web app. It means, now a logged-in or an authenticated user can view all the items, add items in his grocery bag, update a previously added item and delete an item. Let's roll it.

Creating an Item Model

The first thing we need to do is to create a model for our items. Can you guess what all fields should be there for an item? Well, they are - name, quantity, status and date. Let's create the Item model class in the bag/models.py file:

from django.contrib.auth.models import User
from django.db import models

# Create your models here.

STATUS_CHOICES = (
    ('BOUGHT', 'Bought'),
    ('PENDING', 'Pending'),
    ('NOT AVAILABLE', 'Not Available'),
)

class Item(models.Model):
    name = models.CharField(max_length=127)
    quantity = models.CharField(max_length=63)
    status = models.CharField(
        max_length=15, choices=STATUS_CHOICES, default='PENDING')
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    date = models.DateField()
    updated_at = models.DateTimeField(auto_now=True)
    created_at = models.DateTimeField(auto_now_add=True)

In the above script, we have created an Item class that extends the models.Model class. Inside the class, we have mentioned the fields as discussed above. name will be a CharField with a maximum length of up to 127 characters. Similarly, we have set quantity to CharField too, and not IntegerField, just for the fact that quantity can be 3 Kgs as well as 3 pcs. Next, we have a status field that is also a CharField. The status can be one of the three- BOUGHT , PENDING and NOT AVAILABLE. To limit the choices, we have set the choices attribute to a STATUS_CHOICES dictionary and the default value would be PENDING. We have a user field in the model which is a ForeignKey to the User model provided by Django. This field tells that which user has created this item and will help us later in sorting them out. The next field is date which is a DateField. The next two fields are updated_at and created_at which are used to maintain timestamps when the object is created and when it's updated.

After you have created the model, let's run the migrations:

$ python manage.py makemigrations
$ python manage.py migrate

Add a new item

The first thing we need to do is to add a new item into our bag. Note that this operation is only available for authenticated users. Let's create a view function to add a new item in bag/views.py file:

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Item

@login_required
def add_item(request):
    if request.method == "POST":
        name = request.POST.get("name")
        quantity = request.POST.get("quantity")
        status = request.POST.get("status")
        date = request.POST.get("date")
        if name and quantity and status and date:
            new_item = Item(name=name, quantity=quantity,
                            status=status, date=date, user=request.user)
            new_item.save()
            messages.success(request, 'Item added successfully!')
            return redirect('index')
        messages.error(request, "One or more field(s) is missing!")
        return redirect('add-item')
    return render(request, "add.html")

Whenever a GET request is made, we serve the add.html file. If a POST request is made, we get the values of the four fields - name , quantity , status and date. If any of the fields is empty, we flash an error and redirect to the same page. If every field is present, we create a new object of the Item model class and save it, and then redirect to the index page.

Let us create a route for this view function in our bag/urls.py file:

from django.urls import path

from bag.views import add_item, index

urlpatterns = [
    path('', index, name='index'),
    path('add-item', add_item, name='add-item'),
]

Now let us make the required changes in the HTML form in the add.html file:

{% extends "base.html" %}{% load static %} 

{% block title %}Add to bag{% endblock title %} 

{% block content %}
<body>
  <div class="container mt-5">
    <h1>Add to Grocery Bag</h1>
    {% include 'partials/alerts.html' %}
    <form method="post" action="{% url 'add-item' %}">
      {% csrf_token %}
      <div class="form-group mt-2">
        <label>Item name</label>
        <input type="text" name="name" class="form-control" placeholder="Item name" />
      </div>
      <div class="form-group mt-2">
        <label>Item quantity</label>
        <input type="text" name="quantity" class="form-control" placeholder="Item quantity" />
      </div>
      <div class="form-group mt-2">
        <label>Item status</label>
        <select class="form-control" name="status">
          <option value="PENDING">PENDING</option>
          <option value="BOUGHT">BOUGHT</option>
          <option value="NOT AVAILABLE">NOT AVAILABLE</option>
        </select>
      </div>
      <div class="form-group mt-2">
        <label>Date</label>
        <input type="date" name="date" class="form-control" placeholder="Date" />
      </div>
      <div class="form-group mt-2">
        <input type="submit" value="Add" class="btn btn-danger" />
      </div>
    </form>
  </div>
</body>
{% endblock content %}

The form method and action is changed as required, as well as CSRF token is added. Notice that we have added name attribute in each form field, as this is required to fetch the value in our backend.

Update an item

Once an item is added, we can further update it too. Suppose an item was PENDING, but now we have bought that item. So, we can update it and set the status to BOUGHT. Let's add this view function in our bag/views.py file:

@login_required
def update_item(request, item_id):
    item = Item.objects.get(id=item_id)
    date = item.date.strftime("%d-%m-%Y")
    if request.method == 'POST':
        item.name = request.POST.get("name")
        item.quantity = request.POST.get("quantity")
        item.status = request.POST.get("status")
        item.date = request.POST.get("date")
        item.save()
        messages.success(request, 'Item updated successfully!')
        return redirect('index')
    return render(request, "update.html", {'item': item, 'date': date})

This view function is quite similar to the add_item function. But there is a change. With the request, we are getting the item_id in the request URL. Using that item_id, we first fetch the item from the database so that the user can view the previous values in the update form. Whenever a GET request is made, the item and date is passed with the request. If a POST request is made, we get the values from the form and update the appropriate fields and save the item.

A route for this view function will look like this:

from django.urls import path

from bag.views import add_item, index, update_item

urlpatterns = [
    path('', index, name='index'),
    path('add-item', add_item, name='add-item'),
    path('update-item/<int:item_id>', update_item, name='update-item'),
]

Notice that we have added <int:item_id> so that we get the item_id in our view function.

Now let's make the required changes in the update.html file:

{% extends "base.html" %}{% load static %} 

{% block title %}Update bag{% endblock title %} 

{% block content %}
<body>
  <div class="container mt-5">
    <h1>Update Grocery Bag</h1>
    <form method="post" action="{% url 'update-item' item.id %}">
      {% csrf_token %}
      <div class="form-group mt-2">
        <label>Item name</label>
        <input
          type="text"
          class="form-control"
          placeholder="Item name"
          name="name"
          value="{{item.name}}"
        />
      </div>
      <div class="form-group mt-2">
        <label>Item quantity</label>
        <input
          type="text"
          class="form-control"
          placeholder="Item quantity"
          name="quantity"
          value="{{item.quantity}}"
        />
      </div>
      <div class="form-group mt-2">
        <label>Item status</label>
        <select class="form-control" name="status" id="status">
          <option value="PENDING">PENDING</option>
          <option value="BOUGHT" selected>BOUGHT</option>
          <option value="NOT AVAILABLE">NOT AVAILABLE</option>
        </select>
      </div>
      <div class="form-group mt-2">
        <label>Date</label>
        <input
          type="date"
          class="form-control"
          placeholder="Date"
          name="date"
          id="date"
        />
      </div>
      <div class="form-group mt-2">
        <input type="submit" value="Update" class="btn btn-danger" />
      </div>
    </form>
  </div>

  <script>
    // Select appropriate option
    var options = document.getElementById('status').options;
    for (let index = 0; index < options.length; index++) {
      if(options[index].value == '{{item.status}}'){
        options[index].selected = true;
      }
    }

    // Select date
    var fullDate = '{{date}}';
    var dateField = document.getElementById('date');
    dateField.value = `${fullDate.substring(6,12)}-${fullDate.substring(3,5)}-${fullDate.substring(0,2)}`

  </script>
</body>
{% endblock content %}

The form method and action is changed as required, as well as CSRF token is added. The action URL has also item.id passed with it. Notice that we have added name attribute in each form field, as this is required to fetch the value in our backend. The next thing to be considered is the Javascript code at the below of the above script. The Javascript code is used to select the status and date automatically.

Delete an item

This is the easiest task in the CRUD operation. Let's see the view function:

@login_required
def delete_item(request, item_id):
    item = Item.objects.get(id=item_id)
    item.delete()
    messages.error(request, 'Item deleted successfully!')
    return redirect('index')

As in the update view function, we again get the item_id in the delete_item view function. We fetch that item and delete it and then redirect the user to the index page with a flash.

Let's add this view function in our urls.py:

from django.urls import path

from bag.views import add_item, delete_item, index, update_item

urlpatterns = [
    path('', index, name='index'),
    path('add-item', add_item, name='add-item'),
    path('update-item/<int:item_id>', update_item, name='update-item'),
    path('delete-item/<int:item_id>', delete_item, name='delete-item'),
]

Updating Index Page

Till now we're just rendering the index.html file. But now we need to fetch all the items added by the user and send it to the frontend. So, let's update the index view function:

@login_required
def index(request):
    items = Item.objects.filter(user=request.user).order_by('-id')
    context = {
        'items': items
    }
    return render(request, "index.html", context)

We are filtering the items according to the logged-in user, and then ordered it by the id in descending order.

Let's update the index.html file:

{% extends "base.html" %}{% load static %} 

{% block title %}View Bag{% endblock title %} 

{% block content %}
<body>
  <div class="container mt-5">
    <!-- top -->
    <div class="row">
      <div class="col-lg-6">
        <h1>View Grocery List</h1>
        <a href="{% url 'add-item' %}">
          <button type="button" class="btn btn-success">Add Item</button>
        </a>
      </div>
      <div class="col-lg-6 float-right">
        <div class="row">
          <div class="col-lg-6">
            <!-- Date Filtering-->
            <input type="date" class="form-control" />
          </div>
          <div class="col-lg-4">
            <input type="submit" class="btn btn-danger" value="filter" />
          </div>
          <div class="col-lg-2">
            <p class="mt-1"><a href="{% url 'signout' %}">Log Out</a></p>
          </div>
        </div>
      </div>
    </div>
    <br>
    {% include 'partials/alerts.html' %}
    <!-- Grocery Cards -->
    <div class="row mt-4">
      <!-- Loop This -->
      {% for item in items %}
      <div class="col-lg-4 mb-3">
        <div class="card">
          <div class="card-body">
            <h5 class="card-title">{{item.name}}</h5>
            <h6 class="card-subtitle mb-2 text-muted">{{item.quantity}}</h6>
            {% if item.status == 'PENDING' %}
            <p class="text-info">{{item.status}}</p>
            {% elif item.status == 'BOUGHT' %}
            <p class="text-success">{{item.status}}</p>
            {% else %}
            <p class="text-danger">{{item.status}}</p>
            {% endif %}
            <div class="row">
              <div class="col-md-6">
                <a href="{% url 'update-item' item.id %}">
                  <button type="button" class="btn btn-primary">UPDATE</button>
                </a>
              </div>
              <div class="col-md-6">
                <a href="{% url 'delete-item' item.id %}">
                  <button type="button" class="btn btn-danger">DELETE</button>
                </a>
              </div>
            </div>
          </div>
        </div>
      </div>
      {% endfor %}
    </div>
  </div>
</body>
{% endblock content %}

In Django templates, we can iterate over the data passed from the backend using a for loop. Thats what we are doing here. We are iterating over the items using an iterator item. Also we have used if condition to add colors to the status. We have two buttons - update and delete with the URLs set to the update and delete route respectively.

Admin Panel

Django comes with a pre-built admin panel. We just need to create a superuser to access the admin panel. Let's create that superuser using the below command:

$ python manage.py createsuperuser

This will ask you username, email(optional) and the password. Just give those details and a superuser will be created. Then you can go to the http://127.0.0.1:8000/admin route, and login with your credentials.

Demo Video

You can watch the demo video:

Conclusion

In this part, we have completed the most important step, i.e. CRUD operations on the grocery items. In the next part, we'll see how we can use the filter option available on the index page. Stay tuned!

24