Full Stack Web Tutorial: Django, React JS, JWT Auth, REST, Bootstrap, Pagination
By: Timur Bakibayev
Tags: Full Stack, Programming, React, Django, Tutorial

Create a Python Django Project

I assume you have installed Python3 already.

We will need these libraries to start:

pip install django

pip install djangorestframework

pip install markdown

pip install django-filter

pip install djangorestframework-simplejwt

Create the project and the app:

django-admin startproject sales

cd sales

python manage.py startapp app

This should prepare your project files. Make these changes to settings.py:

Add your app to INSTALLED_APPS:

INSTALLED_APPS = [

'django.contrib.admin',

'django.contrib.auth',

'django.contrib.contenttypes',

'django.contrib.sessions',

'django.contrib.messages',

'django.contrib.staticfiles',

'rest_framework',

'app',

]

Change the allowed hosts. This is bad advice, I know.

ALLOWED_HOSTS = ["*"]

Add “templates” to TEMPLATES:

TEMPLATES = [

{

'BACKEND': 'django.template.backends.django.DjangoTemplates',

'DIRS': ['templates'],

...

Add some settings for Authentication:

REST_FRAMEWORK = {

'DEFAULT_AUTHENTICATION_CLASSES': (

'rest_framework.authentication.BasicAuthentication',

'rest_framework_simplejwt.authentication.JWTAuthentication',

)

}

SIMPLE_JWT = {

'ACCESS_TOKEN_LIFETIME': timedelta(days=7),

}

Some standard stuff for statics:

STATIC_URL = '/static/'

STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

Migrate the database:

python manage.py migrate

And make sure your app is able to run without errors:

python manage.py runserver

Define the Models

We will be working with only one table. So this part is simple.

Edit the app/models.py file and add this model:

class Order(models.Model):

date = models.DateField(blank=False)

item = models.CharField(max_length=100, blank=False)

price = models.DecimalField(decimal_places=2, max_digits=10, default=0)

quantity = models.DecimalField(decimal_places=2, max_digits=10, default=0)

amount = models.DecimalField(decimal_places=2, max_digits=10, default=0)



def __str__(self):

return f"{self.date}: {self.item}"



class Meta:

ordering = ["-id"]

We want to edit the date, item, price, and quantity in our table. The amount will be calculated automatically. And we want the new records to appear at the top, so we have added the corresponding “ordering” here.

Add authentication (with Tests)

We will be using JWT for authentication. This is the best approach if you want to write APIs. You may read more about JWT here.

For Django, there is a library for that. And we have implemented half of it already: we have installed the library with pip, and we have set it up in settings.py.

Now, in order to make it work, just add it to the urls.py:

...

from sales import settings

from django.conf.urls.static import static



urlpatterns = [

path('admin/', admin.site.urls),

path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),

path('login', views.login_view),

] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Now, you may try to get the token with a POST request to “localhost:8000/api/token” with login and password. But before doing that, you need to create at least a superuser:

python manage.py createsuperuser

Optional. You may test it in curl like so (run the server in a separate terminal/cmd):

curl -X POST -H 'Content-Type: Application/JSON' --data '{"username":"yourusername", "password":"yourpassword"}' localhost:8000/api/token/

you should see a response like this:

{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYxODI0MTQ1MiwianRpIjoiZTIzYTQwOTYwMGRiNDdjZjlmYTIxZGZlZDgyMDczNzUiLCJ1c2VyX2lkIjoxfQ.hi2soO9TU0Cbqrl9_lE3cixO9jbuO9LtB1J-7ax_JFQ","access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjE4NzU5ODUyLCJqdGkiOiJkMjk4ZmRmNmNmZDc0MWYyYTZmYzgzYjBhZmNjMzRjNSIsInVzZXJfaWQiOjF9.zXnFqbawUUNTVjOsYUtNiJDtYF-axDHKBZ2phdUAU7Q"}

So we get the “refresh” and the “access” tokens as a response. For simplicity, we will be using only the “access” token.

Now, let’s test this with Django. In the “sales” folder, create a file called “tests_jwt.py”. It is important that the file name starts with “tests”.

from django.test import TestCase

from django.contrib.auth.models import User

import json



test_users = [

{"username": "testuser1", "password": "testpassword1"},

{"username": "testuser2", "password": "testpassword2"},

]



class LoginTest(TestCase):

def setUp(self):

for user in test_users:

new_user = User.objects.create(username=user["username"])

new_user.set_password(user["password"])

new_user.save()



def test_login(self):

USER1 = test_users[0]

res = self.client.post('/api/token/',

data=json.dumps({

'username': USER1["username"],

'password': USER1["password"],

}),

content_type='application/json',

)

result = json.loads(res.content)

self.assertTrue("access" in result)

Here we create a new user and fetch the token from the corresponding URL, and test that “access” is in the response.

To run the test, simply type:

python manage.py test

Add tests for sales orders API

We have not yet built the API, but we first run the tests. Why?

Say, you have started implementing the API and you want to test it. How would you do that? Using curl? Fine. But you probably don’t want to run a lot of curls every time you change something in your code. What if something is broken? You will never know this.

That is why, for API, it is good to follow the TDD (Test Driven Development) approach.

In “app” folder, create a file called “tests_orders.py”. Everything in this chapter should be added to this file.

We start with the setup:

from django.test import TestCase

from django.contrib.auth.models import User

import json



test_user = {"username": "testuser", "password": "testpassword"}





class OrdersTest(TestCase):

def setUp(self):

new_user = User.objects.create(

username=test_user["username"])

new_user.set_password(test_user["password"])

new_user.save()



def get_token(self):

res = self.client.post('/api/token/',

data=json.dumps({

'username': test_user["username"],

'password': test_user["password"],

}),

content_type='application/json',

)

result = json.loads(res.content)

self.assertTrue("access" in result)

return result["access"]

Before running each test in this class, the database is cleaned and this “setUp” method is called. No worries, the tests are running in a separate database, so your data will not be touched. If you are working with something different from SQLite, make sure your user has enough rights to create a database.

We also need to get the token for each test, so we have written the helper method called “get_token”.

Test 1. Make sure that unauthorized users are not allowed to POST new data. Response code should be 401 (Authenticated)

def test_add_orders_forbidden(self):

res = self.client.post('/api/orders/',

data=json.dumps({

'date': "2020-01-01",

'item': "Hard Drive",

'price': 100,

'quantity': 10,

}),

content_type='application/json',

)

self.assertEquals(res.status_code, 401)

res = self.client.post('/api/orders/',

data=json.dumps({

'date': "2020-01-01",

'item': "Hard Drive",

'price': 100,

'quantity': 10,

}),

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer WRONG TOKEN'

)

self.assertEquals(res.status_code, 401)

Test 2. Authorized users are allowed to POST. And we check that the response is correct. The response code should be 201 (Created).

def test_add_orders_ok(self):

token = self.get_token()

res = self.client.post('/api/orders/',

data=json.dumps({

'date': "2020-01-01",

'item': "Hard Drive",

'price': 100,

'quantity': 10,

}),

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)

self.assertEquals(res.status_code, 201)

result = json.loads(res.content)["data"]

self.assertEquals(result["date"], '2020-01-01')

self.assertEquals(result["item"], 'Hard Drive')

self.assertEquals(result["price"], 100)

self.assertEquals(result["quantity"], 10)

Test 3. We should not be able to POST “wrong” data. In this case, the response code should be 400 (Bad Request)

def test_add_orders_wrong_data(self):

token = self.get_token()

res = self.client.post('/api/orders/',

data=json.dumps({

'date': "2020-01-01",

'item': "Hard Drive",

'price': -1,

'quantity': 10,

}),

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)

self.assertEquals(res.status_code, 400)



res = self.client.post('/api/orders/',

data=json.dumps({

'date': "2020-01-01",

'item': "Hard Drive",

'price': 1,

'quantity': -10,

}),

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)

self.assertEquals(res.status_code, 400)



res = self.client.post('/api/orders/',

data=json.dumps({

'date': "2020-01-01",

'item': "",

'price': 1,

'quantity': 10,

}),

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)

self.assertEquals(res.status_code, 400)

Test 4. Make sure the amount is calculated from price and quantity. We force amount = 10000, it should be ignored. Instead, we should get 5*7=35.

def test_add_orders_calculate(self):

token = self.get_token()

res = self.client.post('/api/orders/',

data=json.dumps({

'date': "2020-01-01",

'item': "Hard Drive",

'price': 5,

'quantity': 7,

'amount': 10000, # should be ignored

}),

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)

self.assertEquals(res.status_code, 201)

result = json.loads(res.content)["data"]

self.assertEquals(result["amount"], 35)

Test 5. Test the GET method.

def test_get_records(self):

token = self.get_token()

res = self.client.post('/api/orders/',

data=json.dumps({

'date': "2020-01-01",

'item': "Hard Drive",

'price': 5,

'quantity': 7,

}),

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)

self.assertEquals(res.status_code, 201)

id1 = json.loads(res.content)["data"]["id"]



res = self.client.post('/api/orders/',

data=json.dumps({

'date': "2020-02-02",

'item': "Monitor",

'price': 20,

'quantity': 30,

}),

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)

self.assertEquals(res.status_code, 201)

id2 = json.loads(res.content)["data"]["id"]



res = self.client.get('/api/orders/',

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)



self.assertEquals(res.status_code, 200)

result = json.loads(res.content)["data"]

self.assertEquals(len(result), 2) # 2 records

self.assertTrue(result[0]["id"] == id1 or result[1]["id"] == id1)

self.assertTrue(result[0]["id"] == id2 or result[1]["id"] == id2)



res = self.client.get(f'/api/orders/{id1}/',

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)

self.assertEquals(res.status_code, 200)

result = json.loads(res.content)["data"]

self.assertEquals(result["date"], '2020-01-01')

self.assertEquals(result["item"], 'Hard Drive')

self.assertEquals(result["price"], 5)

self.assertEquals(result["quantity"], 7)

self.assertEquals(result["amount"], 35)

Test 6. A scenario with PUT and DELETE methods.

def test_put_delete_records(self):

token = self.get_token()

res = self.client.post('/api/orders/',

data=json.dumps({

'date': "2020-01-01",

'item': "Hard Drive",

'price': 5,

'quantity': 7,

}),

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)

self.assertEquals(res.status_code, 201)

id = json.loads(res.content)["data"]["id"]



res = self.client.put(f'/api/orders/{id}/',

data=json.dumps({

'date': "2020-02-02",

'item': "Monitor",

'price': 50,

'quantity': 70,

}),

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)



self.assertEquals(res.status_code, 200)

result = json.loads(res.content)["data"]

self.assertEquals(result["date"], '2020-02-02')



res = self.client.get(f'/api/orders/{id}/',

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)

self.assertEquals(res.status_code, 200)

result = json.loads(res.content)["data"]

self.assertEquals(result["date"], '2020-02-02')

self.assertEquals(result["item"], 'Monitor')

self.assertEquals(result["price"], 50)

self.assertEquals(result["quantity"], 70)

self.assertEquals(result["amount"], 3500)



res = self.client.delete(f'/api/orders/{id}/',

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)

self.assertEquals(res.status_code, 410) # Gone



res = self.client.get(f'/api/orders/{id}/',

content_type='application/json',

HTTP_AUTHORIZATION=f'Bearer {token}'

)

self.assertEquals(res.status_code, 404) # Not found

If you now run the test:

python manage.py test

then 7 tests, including the Authentication test should be run. 6 of them should fail, of course.

Now we are ready for the API implementation.

Implement the API: GET and DELETE

Import views_order and add these two lines to the path in urls.py:

...

from app import views_orders

...

path('api/orders/', views_orders.orders),

path('api/orders/<int:order_id>/', views_orders.order),

...

There will be an error that there is no “views_orders” file. This is because we have not created it yet.

Create this file: app/views_orders.py

Our API will be there.

We start with importing the needed libraries and a little helper function:

from rest_framework.decorators import api_view

from django.shortcuts import HttpResponse

from rest_framework import status

from django.forms.models import model_to_dict

from django.core.exceptions import ObjectDoesNotExist

from app.models import Order

import json

import datetime





def serialize_order(order):

serialized = model_to_dict(order)

serialized["date"] = str(order.date)

serialized["amount"] = float(order.amount)

serialized["price"] = float(order.price)

serialized["quantity"] = float(order.quantity)

return serialized

This homemade serializer will be helping us to create a dictionary from our database row (object).

And the API for “GET” and “DELETE” methods:

@api_view(['GET', ])

def orders(request):

if request.user.is_anonymous:

return HttpResponse(json.dumps({"detail": "Not authorized"}), status=status.HTTP_401_UNAUTHORIZED)



if request.method == "GET":

orders_data = Order.objects.all()



orders_count = orders_data.count()



page_size = int(request.GET.get("page_size", "10"))

page_no = int(request.GET.get("page_no", "0"))

orders_data = list(orders_data[page_no * page_size:page_no * page_size + page_size])



orders_data = [serialize_order(order) for order in orders_data]

return HttpResponse(json.dumps({"count": orders_count, "data": orders_data}), status=status.HTTP_200_OK)



return HttpResponse(json.dumps({"detail": "Wrong method"}), status=status.HTTP_501_NOT_IMPLEMENTED)





@api_view(['GET', 'DELETE'])

def order(request, order_id):

if request.user.is_anonymous:

return HttpResponse(json.dumps({"detail": "Not authorized"}), status=status.HTTP_401_UNAUTHORIZED)



try:

order = Order.objects.get(pk=order_id)

except ObjectDoesNotExist:

return HttpResponse(json.dumps({"detail": "Not found"}), status=status.HTTP_404_NOT_FOUND)



if request.method == "GET":

return HttpResponse(json.dumps({"data": serialize_order(order)}), status=status.HTTP_200_OK) if request.method == "DELETE":

order.delete()

return HttpResponse(json.dumps({"detail": "deleted"}), status=status.HTTP_410_GONE) return HttpResponse(json.dumps({"detail": "Wrong method"}), status=status.HTTP_501_NOT_IMPLEMENTED)

We first check if you are authorized for these actions. If not, reply with 401 immediately and quit.

Now, some of the tests should be passing.

Implement the API: POST and PUT

The POST and PUT methods share some common code. Namely, they “copy” the fields from the request to the Order object. They should also check if the data is correct and calculate the amount.

That is why it is a good idea to have a separate function for this:

def save_order(request, order, success_status):

errors = []

item = request.data.get("item", "")

if item == "":

errors.append({"item": "This field is required"})



try:

price = request.data.get("price", "")

if price == "":

errors.append({"price": "This field is required"})

else:

price = int(price)

if price < 0:

errors.append({"price": "Price cannot be negative"})

except ValueError:

errors.append({"price": "Could not parse field"})



try:

quantity = request.data.get("quantity", "")

if quantity == "":

errors.append({"quantity": "This field is required"})

else:

quantity = int(quantity)

if quantity < 0:

errors.append({"quantity": "Quantity cannot be negative"})

except ValueError:

errors.append({"quantity": "Could not parse field"})



date = request.data.get("date", "")

if date == "":

date = datetime.datetime.now()



if len(errors) > 0:

return HttpResponse(json.dumps(

{

"errors": errors

}), status=status.HTTP_400_BAD_REQUEST)



try:

order.date = date

order.item = item

order.price = price

order.quantity = quantity

order.amount = price * quantity

order.save()

except Exception as e:

return HttpResponse(json.dumps(

{

"errors": {"Order": str(e)}

}), status=status.HTTP_400_BAD_REQUEST)



return HttpResponse(json.dumps({"data": serialize_order(order)}), status=success_status)

The function is pretty simple and straightforward. It moves the data from the request to the order.

Now, having this, we may extend our API by adding the POST and PUT methods:

@api_view(['GET', 'POST'])

def orders(request):

if request.user.is_anonymous:

return HttpResponse(json.dumps({"detail": "Not authorized"}), status=status.HTTP_401_UNAUTHORIZED)



if request.method == "GET":

orders_data = Order.objects.all()



orders_count = orders_data.count()



page_size = int(request.GET.get("page_size", "10"))

page_no = int(request.GET.get("page_no", "0"))

orders_data = list(orders_data[page_no * page_size:page_no * page_size + page_size])



orders_data = [serialize_order(order) for order in orders_data]

return HttpResponse(json.dumps({"count": orders_count, "data": orders_data}), status=status.HTTP_200_OK)



if request.method == "POST":

order = Order()

return save_order(request, order, status.HTTP_201_CREATED)



return HttpResponse(json.dumps({"detail": "Wrong method"}), status=status.HTTP_501_NOT_IMPLEMENTED)





@api_view(['GET', 'PUT', 'DELETE'])

def order(request, order_id):

if request.user.is_anonymous:

return HttpResponse(json.dumps({"detail": "Not authorized"}), status=status.HTTP_401_UNAUTHORIZED)



try:

order = Order.objects.get(pk=order_id)

except ObjectDoesNotExist:

return HttpResponse(json.dumps({"detail": "Not found"}), status=status.HTTP_404_NOT_FOUND)



if request.method == "GET":

return HttpResponse(json.dumps({"data": serialize_order(order)}), status=status.HTTP_200_OK)



if request.method == "PUT":

return save_order(request, order, status.HTTP_200_OK)



if request.method == "DELETE":

order.delete()

return HttpResponse(json.dumps({"detail": "deleted"}), status=status.HTTP_410_GONE)



return HttpResponse(json.dumps({"detail": "Wrong method"}), status=status.HTTP_501_NOT_IMPLEMENTED)

Now, all tests should pass. If not, fix the errors before continuing to the next chapter. Because by this point, your back-end should be ready. Congratulations!

If you struggle to pass all tests, compare your code to the one in GitHub.

Initialize the Front-End with React JS

We start by adding a new file called “index.html” in a new folder “app/templates”. This is the files tree that you will have at the end of the tutorial:

Files tree. Make sure you are adding the files to the correct folders.

Edit the “index.html” as follows:

<!DOCTYPE html>

<html lang="en">

<head>

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">

<meta charset="UTF-8">

<title>Sales Orders Management System</title>

</head>

<body>

{% include "react_and_bootstrap.html" %}

<script type="text/jsx" src="/static/js/api.js"></script>

<script type="text/jsx" src="/static/js/sales_page.js"></script>

</body>

</html>

Add the “login.html”, which is very similar:

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">

<title>Login</title>

</head>

<body>

{% include "react_and_bootstrap.html" %}

<script type="text/jsx" src="/static/js/api.js"></script>

<script type="text/jsx" src="/static/js/login_page.js"></script>

</body>

</html>

You can see that we include the “react_and_bootstrap.html”. Add this file as in the tree above, next to the “index.html” and add this content:

<div id="reactAppContainer"></div>

<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

<script src="//cdn.jsdelivr.net/npm/sweetalert2@10"></script>

<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>

<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"></script>

This is a “development version” of React. Before deploying to production, make sure to replace the react scripts (script number 3 and 4) with the following two lines:

<script src="https://unpkg.com/react@17/umd/react.production.min.js" crossorigin></script>

<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js" crossorigin></script>

Now create two files called “sales_page.js” and “login_page.js” with this simple content:

'use strict';

const e = React.createElement;



function App() {

return (

<div>

<h1>Hello, World!</h1>

</div>

);

}



const domContainer = document.querySelector('#reactAppContainer');

ReactDOM.render(

e(App),

domContainer

);

In order to show these HTML files in Django, we need to add two paths in urls.py:

from app import views

...

path('', views.index),

path('login', views.login_view),

and, of course, we need these functions.

Add this content to app/views.py file:

from django.shortcuts import render





def index(request):

context = {}

return render(request, "index.html", context=context)





def login_view(request):

context = {}

return render(request, "login.html", context=context)

You should be able to run the server and see the “Hello, World!” message.

Otherwise, fix the problems before continuing to the next chapter.

Authenticate from Front-End

Next, we prepare this login view.

First, create the API consumer for this.

Again, look at this tree,

and create the api.js file:

const login_api = async (username, password, success, fail) => {

const response = await fetch(

`/api/token/`,

{

method: 'POST',

headers: {

'Accept': 'application/json',

'Content-Type': 'application/json',

},

body: JSON.stringify({

"username": username,

"password": password,

})

}

);

const text = await response.text();

if (response.status === 200) {

console.log("success", JSON.parse(text));

success(JSON.parse(text));

} else {

console.log("failed", text);

Object.entries(JSON.parse(text)).forEach(([key, value])=>{

fail(`${key}: ${value}`);

});

}

};

We will be adding more API consumers here later. It is good to isolate the API calls to a separate file because if something changes in API, we want to only change it in one place.

This function simply makes the POST request to /api/token/. Then it calls either success or fail function, depending on the result.

Go to the login_page.js and replace it with this content:

'use strict';

const e = React.createElement;



function App() {

const [username, setUsername] = React.useState("");

const [password, setPassword] = React.useState("");

const [message, setMessage] = React.useState("");



const success = async (text)=> {

console.log("Yeah! Authenticated!");

await localStorage.setItem("salesToken", text.access);

window.location = "/";

};



const tryLogin = async (e) => {

e.preventDefault();

console.log("Loggin in with", username, password);

await login_api(username, password, success, (text)=>{setMessage(text)});

};



return (

<div style={{width: "400px", margin: "auto", marginTop: "200px",

boxShadow: "5px 5px 20px #cccccccc",

padding: "1em"

}}>

<form>

<div className="mb-3">

<label htmlFor="username" className="form-label">Username</label>

<input autoFocus type="text" className="form-control" id="username" placeholder="username"

onChange={(e)=>{setUsername(e.target.value)}} value={username}/>

</div>

<div className="mb-3">

<label htmlFor="password" className="form-label">Password</label>

<input type="password" className="form-control" id="password" placeholder="password"

onChange={(e)=>{setPassword(e.target.value)}} value={password}/>

</div>

<div style={{margin: "1em", color: "red"}}>{message}</div>

<button type="submit" className="btn btn-primary" onClick={tryLogin}>Login</button>

</form>

</div>

);

}



const domContainer = document.querySelector('#reactAppContainer');

ReactDOM.render(

e(App),

domContainer

);

We are using Bootstrap here. Now, you may navigate to /login in your browser and test first incorrect, and then the correct usernames and passwords.

Congrats! We are halfway there!

Display the orders table (GET)

First, start with the GET API call in api.js:

const get_orders_api = async (pageNo="", success, fail) => {

const token = await localStorage.getItem("salesToken");

if (token === null) {

console.log("No credentials found, redirecting...");

window.location = "/login";

return [];

}

const response = await fetch(

`/api/orders/?page_no=${pageNo}`,

{

method: 'GET',

headers: {

'Content-Type': 'Application/JSON',

'Authorization': `Bearer ${token}`,

}

}

);

const text = await response.text();

if (response.status === 401) {

console.log("Token not valid");

window.location = "/login";

return [];

}

if (response.status === 200) {

console.log("success", JSON.parse(text));

success(JSON.parse(text));

} else {

console.log("failed", text);

Object.entries(JSON.parse(text)).forEach(([key, value])=>{

fail(`${key}: ${value}`);

});

}

};

Here we already care about the page number. By default (until we implement the pagination) it will always be the first page only.

Go to the sales_page.js and start with these constants. We will need them later:

function App() {

const [list, setList] = React.useState([]);

const [pages, setPages] = React.useState([]);

const [page, setPage] = React.useState(0);

const [showModal, setShowModal] = React.useState(false);

const [modalDescription, setModalDescription] = React.useState("");

const [itemId, setItemId] = React.useState(null);

const [error, setError] = React.useState("");

const [item, setItem] = React.useState("");

const [price, setPrice] = React.useState(0);

const [quantity, setQuantity] = React.useState(0);

Here, the useState is the simplified version of the old React “state”. This function (useState) returns a pair: a state and a function that changes this state. For example, list and setList. Here setList is an async function that changes the list. And so on.

Every time we load some data, we need to refresh some of the above variables, namely, list, pages, and page. For this we write this small function:

const success = (data) => {

setList(data.data);

const newPages = [];

if (data.count > 10) {

for (let i=0; i<Math.ceil(data.count / 10); i++) {

newPages.push({

name: (i+1).toString(),

page: i,

});

console.log("page",i);

}

if (page > newPages.length-1) {

setPage(page-1);

}

} else {

setPage(0);

}

setPages(newPages);

};

Here if there are more than 10 items in the database, we show the pagination. Note that our API may return only 10 records in “data”, but also inform us that there are 50 items in “count”. So we rely on the count here.

Getting data is simple if you have already written the API call:

const getData = ()=>{

get_orders_api(page, success, (text)=>{console.log("Error: ", text)});

};

React.useEffect(()=>{

getData();

}, [page]);

Here we use the useEffect function. In our case, we want to start it when the page loads and every time the “page” variable is changed. That’s why we put [page] in the end here. It is a “dependency” for the useEffect method.

To log out, we write this simple function:

const logout = async (e)=>{

await localStorage.setItem("salesToken",null);

window.location = "/login";

};

That’s it. It is only left to render the page:

return (

<div>

<div style={{maxWidth: "800px", margin: "auto", marginTop: "1em", marginBottom: "1em",

padding: "1em"}} className="shadow">

<div style={{display: "flex", flexDirection: "row"}}>

<span>Super Orders App</span>

<a className="btn btn-light" style={{marginLeft: "auto"}} onClick={logout}>Logout</a>

</div>

</div>

<div style={{maxWidth: "800px", margin: "auto", marginTop: "1em", marginBottom: "1em",

padding: "1em"}} className="shadow">

<div style={{display: "flex", flexDirection: "row", marginBottom: "5px"}}>

{pages.length > 0 && <nav className="d-lg-flex justify-content-lg-end dataTables_paginate paging_simple_numbers">

<ul className="pagination">

<li className={"page-item " + (page === 0?"disabled":"")} onClick={(e)=>{

e.preventDefault();

setPage(Math.max(page-1,0));

}}><a className="page-link" href="#" aria-label="Previous"><span

aria-hidden="true">«</span></a></li>

{pages.map((el)=><li key={"page" + el.page} onClick={(e)=>{

setPage(el.page);

}} className={"page-item "+(page===el.page?"active":"")}>

<a className="page-link" href="#">

{el.name}

</a></li>)}

<li className={"page-item " + (page === pages.length-1?"disabled":"")} onClick={(e)=>{

setPage(Math.min(page+1,pages.length-1));

}}><a className="page-link" href="#" aria-label="Next"><span

aria-hidden="true">»</span></a></li>

</ul>

</nav>}

<a className="btn btn-light" style={{marginLeft: "auto"}}

onClick={newOrder}

>New Sales Order</a>

</div>

<table className="table table-hover caption-top">

<thead className="table-light">

<tr>

<th>id</th>

<th>Date</th>

<th>Item</th>

<th>Price</th>

<th>Quantity</th>

<th>Amount</th>

<th>Action</th>

</tr>

</thead>

<tbody>

{ list.map((row)=>

<tr key={row.id}>

<td>{row.id}</td>

<td>{row.date}</td>

<td>{row.item}</td>

<td>{row.price}</td>

<td>{row.quantity}</td>

<td>{row.amount}</td>

<td>

<a className="btn btn-light" style={{marginLeft: "auto"}}

onClick={(e)=>{editOrder(row)}}>Edit</a>{" "}

<a className="btn btn-light" style={{marginLeft: "auto"}}

onClick={(e)=>{deleteOrder(row.id)}}>Delete</a>

</td>

</tr>

)}

</tbody>

</table>

</div>

</div>

);

}

You should now be able to see a list of sales orders. Before testing, make sure to add a couple of orders to the database. You may skip this part until we implement the next chapter “Add new Order”.

Implement “Add new Order”

When we add a new order, we want a modal window to pop up.

First of all, let’s add the POST method to our api.js:

const post_order_api = async (data, success) => {

const token = await localStorage.getItem("salesToken");

if (token === null) {

console.log("No credentials found, redirecting...");

window.location = "/login";

return [];

}

const response = await fetch(

`/api/orders/`,

{

method: 'POST',

headers: {

'Content-Type': 'Application/JSON',

'Authorization': `Bearer ${token}`,

},

body: JSON.stringify(data)

}

);

const text = await response.text();

if (response.status === 401) {

console.log("Token not valid");

window.location = "/login";

return [];

}

if (response.status === 201) {

console.log("success", JSON.parse(text));

success(JSON.parse(text));

} else {

console.log("failed", text);

Object.entries(JSON.parse(text)).forEach(([key, value])=>{

fail(`${key}: ${value}`);

});

}

};

Let’s first render it. Put this code right after the first “<div>” in the return function:

<div style={{background: "#00000060"}}

className={"modal " + (showModal?" show d-block":" d-none")} tabIndex="-1" role="dialog">

<div className="modal-dialog shadow">

<form method="post">

<div className="modal-content">

<div className="modal-header">

<h5 className="modal-title">{modalDescription}</h5>

<button type="button" className="btn-close" onClick={()=>{setShowModal(false)}} aria-label="Close"></button>

</div>

<div className="modal-body">

<label>Item name</label>

<div className="form-group">

<input type="text" className="form-control" name="item" id="itemInput"

value={item} onChange={(e)=>{setItem(e.target.value)}}

placeholder="Item name"/>

</div>

<label style={{marginTop: "1em"}}>Price</label>

<div className="form-group" >

<input type="number" className="form-control" placeholder="Price"

value={price} onChange={(e)=>{setPrice(e.target.value)}}

name="price" />

</div>

<label style={{marginTop: "1em"}}>Quantity</label>

<div className="form-group">

<input type="number" className="form-control"

value={quantity} onChange={(e)=>{setQuantity(e.target.value)}}

placeholder="Quantity" name="quantity" />

</div>

<small className="form-text text-muted">{error}</small>

</div>

<div className="modal-footer">

<button type="button" className="btn btn-secondary" onClick={()=>{setShowModal(false)}} data-bs-dismiss="modal">Close</button>

<button type="submit" className="btn btn-primary" onClick={saveOrder}>Save changes</button>

</div>

</div>

</form>

</div>

</div>

So that the next line would be

<div style={{maxWidth: "800px", margin: "auto", ...

Here you can see that the modal form is only shown when the showModal variable is true. Inside the form, we show the fields of our model, and they use the variables item, price, and quantity.

Next, when you press the submit function, the form should be saved. Add this function:

const saveOrder = (e)=>{

e.preventDefault();

setError("");

console.log("saving new", item, price, quantity);

if (item.length * price * quantity === 0)

setError("Please enter item name, price and quantity");

else {

if (itemId === null)

post_order_api({item, price, quantity}, ()=>{getData();});

else

put_order_api(itemId, {item, price, quantity}, ()=>{getData();});

setShowModal(false);

}

};

In this function, we are already prepared for edit. You may see the if (itemId === null) part here. The only difference is that “edit” sets the itemId.

And this form should open when we press the “New Sales Order” button:

const newOrder = ()=>{

setModalDescription("New order");

setItemId(null);

setItem("");

setPrice(0);

setQuantity(0);

setError("");

setShowModal(true);

const itemInput = document.getElementById("itemInput");

setTimeout(()=>{itemInput && itemInput.focus()}, 1);

};

We reset all variables here, show the window, and set the focus to the first field so that the user does not have to click on it.

Note that we have a small Time Out before focus. That is because we need to give some time to open the modal window. If for some reason, it takes more time to open it, the focus will not be set, but there will be no error either. Because we check it by calculating itemInput && .

You may now add as many Sales Orders as you wish. You should also see the pagination once you add more than 10 records.

Implement “Edit Order”

For editing, we already have a modal window. So we only need to add one function:

const editOrder = (data)=>{

setModalDescription("Edit order");

setItemId(data.id);

setItem(data.item);

setPrice(data.price);

setQuantity(data.quantity);

setError("");

setShowModal(true);

const itemInput = document.getElementById("itemInput");

setTimeout(()=>{itemInput && itemInput.focus()}, 1);

};

We set the variables item, price, and quantity to the ones from our record. All the rest is the same, so it should work immediately.

And add the PUT call to the api.js file:

const put_order_api = async (saleId, data, success) => {

const token = await localStorage.getItem("salesToken");

if (token === null) {

console.log("No credentials found, redirecting...");

window.location = "/login";

return [];

}

const response = await fetch(

`/api/orders/${saleId}/`,

{

method: 'PUT',

headers: {

'Content-Type': 'Application/JSON',

'Authorization': `Bearer ${token}`,

},

body: JSON.stringify(data)

}

);

const text = await response.text();

if (response.status === 401) {

console.log("Token not valid");

window.location = "/login";

return [];

}

if (response.status === 200) {

console.log("success", JSON.parse(text));

success(JSON.parse(text));

} else {

console.log("failed", text);

Object.entries(JSON.parse(text)).forEach(([key, value])=>{

fail(`${key}: ${value}`);

});

}

};

Implement “Delete Order”

Here we want to first ask if the user is sure to delete the order. For this, we use sweet alert. It is not only beautiful but also very easy to use:

const deleteOrder = (orderId)=>{

Swal.fire({

title: 'Are you sure?',

text: "You won't be able to revert this!",

icon: 'warning',

showCancelButton: true,

confirmButtonColor: '#3085d6',

cancelButtonColor: '#d33',

confirmButtonText: 'Yes, delete it!'

}).then((result) => {

if (result.isConfirmed) {

delete_order_api(orderId, ()=>{

Swal.fire({

title: 'Deleted!',

text: "Your order has been deleted!",

icon: 'success',

timer: 1000,

});

getData();

});

}

});

};

And the Delete API call, of course (api.js):

const delete_order_api = async (saleId, success) => {

const token = await localStorage.getItem("salesToken");

if (token === null) {

console.log("No credentials found, redirecting...");

window.location = "/login";

return [];

}

const response = await fetch(

`/api/orders/${saleId}/`,

{

method: 'DELETE',

headers: {

'Content-Type': 'Application/JSON',

'Authorization': `Bearer ${token}`,

}

}

);

const text = await response.text();

if (response.status === 401) {

console.log("Token not valid");

window.location = "/login";

return [];

}

console.log(response.status);

if (response.status === 410) {

console.log("success", JSON.parse(text));

success(JSON.parse(text));

} else {

console.log("failed", text);

Object.entries(JSON.parse(text)).forEach(([key, value])=>{

fail(`${key}: ${value}`);

});

}

};

Note that in each API call, if the response code is 401 (Unauthorized), we redirect the user to “/login” URL.

GitHub Repository

Congratulations! You did it! If something does not work, go to the GitHub repository and check what is wrong by comparing your code to it: https://github.com/timurbakibayev/crud_django_react

Feel free to not only get the project on GitHub but also press the “Use this Template” button to create a project based on that.

Conclusion

Yes, it takes time for such a small project. I planned to write this article in one day, but it took me about a week to implement this and two days to write the article. But we now have a nice template that can be reused for other projects.

If you liked the article, feel free to buy me a coffee:

And have a look at my other articles, mostly about Python and Django: