Django Celery with Real-time Monitoring Tasks Using Flower

In today’s discussions with {{ PLP }}, we are about to learn the asynchronous method with Django Celery plus the real-time monitoring tasks using the Flower.

Do you know why we can’t avoid in today’s modern web applications to use the distributed task queue asynchronously? Definitely, we can’t avoid it, I will demonstrate to you what exactly I mean the strength of the async distributed task and the benefit it brings to our web applications.

Celery is a simple, flexible, and reliable distributed system to process vast amounts of messages, while providing operations with the tools required to maintain such a system. The Celery is designed for task queue that focuses on the real-time processing of requests both users and within our internal web-applications.

Moreover, Celery supports the tasks scheduling services as well, meaning, we can do automated scripts we have to run on a scheduled specified timing to execute the scripts, for example, we have a Django scripts to delete physical rows from the table that are marked as in-active status for our database maintenance instead of using the traditional Cron Jobs provided by any UNIX-systems which are heavily relying the assistance mostly with the system’s administrators within your organization or things alike. This is just one of the many things we can do to schedule tasks using the Celery that we have full control over it as a Django developer.

The Celery is coupled with Flower monitoring tool which receives triggered events from the Celery itself in real-time visualization provided for us using their web-based application. It can track the tasks progress history, ability to show task details (arguments, start time, runtime, and more), graphs and statistics.

Just a quick glance at the rear, we have previous discussions about the Supervisor Installation and Configuration on Ubuntu 18.04 LTS, which really helps us a lot in securing our web servers important web services to always attempt to auto-run it during boot up.

Introduction

With the arrival of Celery and Flower technology, the Django that serves the scripts in a traditional blocking sequence, meaning, it will execute from A to Z as we’re like reading the alphabets, it will be an ordered bases, you can’t skip in-between unless the other scripts will be executed first and then wait until it will complete, then finally execute the next sequence of your scripts.

Furthermore, when one of your sequence code stuck with the heavy payloads of operation, your user will wait until it will be completed and feels the slowness of your web-applications and the responsiveness will be disrupted and causes bad user’s experience.

In spite of this default blocking sequence that most of the programming languages built in the same paradigm, the Django Celery comes into play and provide the Asynchronously distributed task queue to eliminate the blocking sequence that causes much delay in between the scripts to be executed.

After we separate the time-consuming process and sent it to the Celery to process it separately so that the rest of the sequence will keep continuing processing the rest of the codes without delay. When the Celery did processing what intends to be processed, it will come back afterward bringing the results that we’re expecting.

So, prepare your self once again and stay focus because this would be exciting and fun learning with {{ PLP }}.

Getting Started

I will be giving you a step-by-step guide and provide live demonstrations about what we’re about to learn for today, to illustrate and see the difference between the blocking and non-blocking sequence and how we can separate the codes that contribute the delay between your codes.

In addition, I will show you how we can schedule a tasks using Celery and execute this scripts accordingly within the specified timings, all of this events that triggered by the Celery will be monitored in real-time using the Flower web-interface.

Step 1: Install the Celery

First of all, we need to install the Celery, make it sure, that you activate the VirtualEnv so we can contain this installation within the specified Django project and the corresponding virtual environment. So, let’s start installing it.

1
2
root@localhost:~# cd env_dev
root@localhost:~/env_dev# source bin/activate

After we have activated the specified VirtualEnv, to make it sure that all of the installation packages will be stored within this virtual environment, now, you see it’s been activated when (env name) will be seen before the root. So, let us proceed with the Celery installation, notice that the “celery[redis]” package we have to install instead of pip3 install celery, because we’re using the Redis as the message broker for Celery.

1
pip3 install -U "celery[redis]"

Next, create a new Python file and name it as celery.py, then save it inside the dev/dev directory path, I use the existing project we had previously which is the dev project folder name. But, for your case, I’m sure it’s a different folder name. COPY exactly the code snippets below to the celery.py file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery

# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dev.settings')

app = Celery('dev')

# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
#   should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')

# Load task modules from all registered Django app configs.
app.autodiscover_tasks()


@app.task(bind=True)
def debug_task(self):
    print('Request: {0!r}'.format(self.request))

Look for the word “dev” and replace it with your own Django project folder name and save it again.

Still, at dev/dev directory path, look for the file __init__.py and COPY exactly the code snippets below and save it.

1
2
3
4
5
6
7
from __future__ import absolute_import, unicode_literals

# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app

__all__ = ('celery_app',)

This will ensure that the Celery will always be imported and executed at all the time.

Furthermore, open up your dev/dev/settings.py and insert the code snippets below to initialize the Redis server and it’s port to be used, we use the Redis as the message broker instead of Rabbitmq server for Celery, but, of-course you can use the Rabbitmq if you want, but, personally, I use Redis as it’s more widely used and stable.

1
2
3
4
5
6
7
8
9
# CELERY STUFF
BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Asia/Manila'
CELERY_ENABLE_UTC = False
CELERYBEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'

The BROKER_URL = ‘redis://localhost:6379/0’ is the Redis database itself, if you have multiple Django projects and using the Redis Celery combination, you must remember to assign the new Redis database instead of re-using the same BROKER_URL previously, for example, BROKER_URL = ‘redis://localhost:6379/1’, just increment it with unique numerical identifier for your Redis database, otherwise, your new Django project which uses the previously taken BROKER_URL, will not work.

Step 2: Install Redis Server

Before we install the Redis server, deactivate your VirtualEnv first, as we need to install this as a system-wide, meaning, only one time we install the Redis server throughout the entire single web server machine.

To deactivate the specific virtual environment, execute the command below.

1
(env_dev) root@localhost:~/env_dev# deactivate

Type the word, deactivate only when your in the same directory of where the VirtualEnv is located. It’s a good practice before we install any software when VirtualEnv is not activated to always go back to the root directory, just type cd ~ and hit the enter key. Now, install the Redis server distribution package.

1
sudo apt install redis-server

If prompt [“Y/n”], type “y” to proceed, it should be a smooth installation. Next, after you install it, we need to check if it’s running or not, to do this, execute the command below.

1
sudo systemctl status redis

Then, it will return something like this below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
● redis-server.service - Advanced key-value store
   Loaded: loaded (/lib/systemd/system/redis-server.service; enabled; vendor pre
   Active: active (running) since Thu 2019-04-04 05:15:58 UTC; 5min ago
     Docs: http://redis.io/documentation,
           man:redis-server(1)
 Main PID: 13402 (redis-server)
    Tasks: 4 (limit: 1111)
   CGroup: /system.slice/redis-server.service
           └─13402 /usr/bin/redis-server 127.0.0.1:6379

Apr 04 05:15:58 localhost systemd[1]: Starting Advanced key-value store...
Apr 04 05:15:58 localhost systemd[1]: redis-server.service: Can't open PID file
Apr 04 05:15:58 localhost systemd[1]: Started Advanced key-value store.
lines 1-13/13 (END)

Once it is running, definitely it works and no need to test it further. But, just a quick check, press Ctrl + C to exit from the Redis status console and then execute the command below.

1
2
3
4
root@localhost:~# redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> exit

So, the Redis server is up and running from our Ubuntu 18.04 LTS web server.

Step 3: Install RabbitMQ Server

However, the default message broker for the Flower is the RabbitMQ server which means, we need to install first the RabbitMQ server to our Ubuntu 18.04 LTS in order for this software to work properly. This installation is a system-wide, as we don’t need to install this into the individual VirtualEnv.

1
sudo apt-get install rabbitmq-server

If prompt with “[Y/n]“, then type in “y” and press enter to continue the installation. Next, we need to check if the RabbitMQ server is up and running.

1
sudo systemctl status rabbitmq-server.service

After executing the command above, the RabbitMQ server status would look like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
● rabbitmq-server.service - RabbitMQ Messaging Server
   Loaded: loaded (/lib/systemd/system/rabbitmq-server.service; enabled; vendor
   Active: active (running) since Thu 2019-04-04 07:12:23 UTC; 11min ago
 Main PID: 15113 (rabbitmq-server)
    Tasks: 86 (limit: 1111)
   CGroup: /system.slice/rabbitmq-server.service
           ├─15113 /bin/sh /usr/sbin/rabbitmq-server
           ├─15129 /bin/sh /usr/lib/rabbitmq/bin/rabbitmq-server
           ├─15288 /usr/lib/erlang/erts-9.2/bin/epmd -daemon
           ├─15399 /usr/lib/erlang/erts-9.2/bin/beam.smp -W w -A 64 -P 1048576 -
           ├─15509 erl_child_setup 65536
           ├─15570 inet_gethost 4
           └─15571 inet_gethost 4

Apr 04 07:12:18 localhost systemd[1]: Starting RabbitMQ Messaging Server...
Apr 04 07:12:20 localhost rabbitmq[15114]: Waiting for rabbit@localhost
Apr 04 07:12:20 localhost rabbitmq[15114]: pid is 15129
Apr 04 07:12:23 localhost systemd[1]: Started RabbitMQ Messaging Server.
lines 1-18/18 (END)

So, now, the RabbitMQ server based on the status is now active and running, then, press CTRL + C to exit from the RabbitMQ console.

Step 4: Install Flower

For us to monitor the Celery triggered tasks, the only reliable tools that are dedicated to Django Celery is the Flower, which is a real-time monitoring task for Celery. Before we install the flower, make sure that you activate again the VirtualEnv to contain the installation packages.

1
pip3 install flower

The Flower installation is quite straightforward, and just an experienced tip, one of the Flower dependency library is the tornado>=4.2.0,<6.0.0 versions as of this writing, tornado > 6 is not yet fully supported by the Flower, so it won’t work if beyond tornado > 6.

Step 5: Configure the Supervisor

The Celery and Flower must include in the Supervisor application to monitor it and must constantly run automatically when the system boot up, as these programs will not run automatically during the boot time.

First of all, download the /etc/supervisor using the FileZilla to your local drive for us to add 3 new configuration files.

Furthermore, I would like you to create 2 workers at least to accept any Celery triggered events to process that queue sent by the Redis message broker.

Although, it’s a bit overkill because, 2 workers to process simple but no much requests site traffics, but, it’s a good practice and I recommend it for your Django powered application. You can always increase these workers, just replicate the config file, but, don’t forget to give a unique worker name to avoid collations with each workers configurations.

Afterward, create a the first worker 1 config file and named it as the dev_celery_worker_1.conf, this file should be stored at /etc/supervisor/conf.d/ directory path. COPY exactly the code snippets below and save it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[program:dev_celery_worker_1]
command=/root/env_dev/bin/celery worker -A dev -B --loglevel=INFO --concurrency=2 -n worker1@%%h

directory=/root/dev/
user=root

; Supervisor will start as many instances of this program as named by numprocs
numprocs=1

; Put process stdout output in this file
stdout_logfile=/var/log/celery/dev_celery_worker_1.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10

stderr_logfile=/var/log/celery/dev_celery_worker_1.log
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=10

autostart=true
autorestart=true
startsecs=10
stopwaitsecs = 600
killasgroup=true
priority=998

And then, for the worker 2, named this file as the dev_celery_worker_2.conf and save it still inside the /etc/supervisor/conf.d/ directory path. COPY exactly the code snippets below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[program:dev_celery_worker_2]
command=/root/env_dev/bin/celery worker -A dev -B --loglevel=INFO --concurrency=2 -n worker2@%%h

directory=/root/dev/
user=root

; Supervisor will start as many instances of this program as named by numprocs
numprocs=1

; Put process stdout output in this file
stdout_logfile=/var/log/celery/dev_celery_worker_2.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10

stderr_logfile=/var/log/celery/dev_celery_worker_2.log
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=10

autostart=true
autorestart=true
startsecs=10
stopwaitsecs = 600
killasgroup=true
priority=998

Then, change all the keyword “dev” based on your Django project folder name. This configuration file is for the Celery settings for “dev” Django project we have.

The 2nd config file called the flower_dev.conf is the Flower settings for the “dev” Django project, COPY exactly the code snippets below and save it inside the /etc/supervisor/conf.d/ directory path.

1
2
3
4
5
6
7
8
9
10
11
[program:flower_dev]
command=/root/env_dev/bin/celery -A dev flower --port=5555 --basic_auth=your_user_name:your_password
directory=/root/dev/
user=root
numprocs=1
autostart=true
autorestart=true
startsecs=10
stopwaitsecs = 600
killasgroup=true
priority=998

Again, change all containing “dev” keyword from the configurations, change it with your own Django folder name accordingly. By the way, all the command, basic_auth, and the directory settings must change it with your own directory path as well. Don’t forget to enter your username and password from the basic_auth setting, then the port, you can choose with your own, but the 5555 is the default port for Flower.

If you have multiple Django projects, again, you need to repeat these steps to create the 3 files under the Supervisor which are the Daphne, Celery, and the Flower config files. Don’t forget to give a unique port number in each configuration.

Next, upload these 2 new Supervisor configurations files to your web server under the /etc/supervisor/conf.d/ directory path as well as the 3 files from the dev/dev/ directory path, these are the __init__.py, celery.py, and settings.py respectively.

Now, we need to create this folder first to store the Celery logs to our web server, execute the command below to create a new folder named “celery” inside the /var/log/ directory path.

1
sudo mkdir /var/log/celery

Afterward, we need to reload the Supervisor to take the changes, execute the following commands below.

1
sudo supervisorctl reread

The dev_celery and flower_dev have been detected as new available changes to be implemented by the Supervisor program. Next, we need to execute the command below to update the Supervisor program for these changes.

1
sudo supervisorctl update

Again, the dev_celery_worker_1, dev_celery_worker_2 and the flower_dev added process to group status and have been included in the Supervisor’s monitoring and management program.

Don’t forget to include these 3 new libraries to your requirements.txt file.

1
2
3
celery
flower
tornado>=4.2.0,<6.0.0

Next, it’s time to visit the Supervisor web interface to check the programs that we have added must be there.

Great!, it’s working, now, we can see we added 3 new programs to be monitored by the Supervisor, the 2 Celery workers, and the Flower programs. Awesome!

Also, the check out the Flower real-time monitoring web interface as well for Celery triggered events.

The Flower works as well, great!, as you can see the 2 workers status are now online and ready to accept triggered events from the Celery and these workers delivered results that we need for our Django powered applications.

Step 6: Test the Celery Event using the shared_task() decorator

Now, for the coding part, As I’ve promised earlier to you, we need to test and see it for your self the strength of the Asynchronous task provided by the Celery, first of all, we need to know re-use the previous Models we have which is the ContactUs model.

One of the real-scenario I would like to demonstrate to you when we have a Django form that would automatically send an email notification to the user and BCC the site administrator’s email address.

Hence, the email invoked by our application normally will take time to send it out the corresponding email, this time, I will show you first how to make it faster and let the Celery handle it and return to us later when it finishes processing it for us.

First, create the tasks.py and saved it inside the dev/myroot/views directory and COPY exactly the code snippets below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from __future__ import absolute_import, unicode_literals
from django.conf import settings
from celery import shared_task
import arrow

# Call myroot properties
from myroot.views.vemail import do_send_email


@shared_task()
def contactus_send_mail(subject, email, full_name, message, GET_USER_TZN):
    # Execute the celery task to send email
    do_send_email('email_contactus_with_celery.html',
                  subject,
                  settings.APP_EMAIL_FROM,
                  (email,),
                  'Thank you for getting in touch!',
                  arrow.utcnow().to(GET_USER_TZN).format('MMM DD, YYYY hh:mm a'),
                  'Awesome!',
                  full_name,
                  None,
                  message)

Next, install first the Arrow Python library as we are using this for converting local time based on the current user’s timezone and don’t forget to include this Arrow library to your requirements.txt file.

Afterward, create a new Python file called vemail.py and again saved it inside the dev/myroot/views directory and COPY exactly the code snippets below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from django.conf import settings
from django.core.mail import EmailMessage
from django.template.loader import render_to_string


def do_send_email(pEmailTemplate, pSubject, pFrom, pTo, emHdrTitle, pDate,
                  emSubHdrTitle, emName, pCC=None, pMessage=None):
    """The common function to auto send email to the user when triggered."""
    msg_html = render_to_string('myroot/email_format/'+pEmailTemplate,
                                {'EMAIL_HEADER_TITLE': emHdrTitle,
                                    'EMAIL_SUB_TITLE': emSubHdrTitle,
                                    # Custom Variables used for specified email template
                                    'FULL_NAME': emName,
                                    'SUBJECT': pSubject,
                                    'SENT_DT': pDate,
                                    'SENT_MESSAGE': pMessage,
                                    # Site Common Settings
                                    'APP_SITE_TEMPLATE_COLOR': settings.APP_SITE_TEMPLATE_COLOR,
                                    'APP_URL_TOP_LOGO': settings.APP_URL_TOP_LOGO,
                                    'SITE_SHORT_NAME': settings.SITE_SHORT_NAME,
                                    'SITE_FULL_NAME': settings.SITE_FULL_NAME,
                                    'BASE_URL': settings.BASE_URL,
                                 })
    msg = EmailMessage(subject=pSubject, body=msg_html,
                       from_email=pFrom, to=pTo, cc=pCC,
                       bcc=["'"+settings.APP_EMAIL_BCC+"'"])
    msg.content_subtype = "html"  # Main content is now text/html
    msg.send()

Next, open up your dev/myroot/views/vmain.py and import the contactus_send_mail from the tasks.py.

1
2
# For Celery related custom functions
from myroot.views.tasks import contactus_send_mail

Then, scroll at the bottom of vmain.py and insert the code snippets below to create a new view for this shared_task() testing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def contactus_with_celery_view(request):
    """Renders the contact us page with Celery task when sending an email."""
    if request.method == 'GET':

        # Get contact us form to display
        form = Basic_CRUD_Create_Form()
        return render(request, 'myroot/contactus_with_celery.html',
                      {'form': form, 'title': "Learn Django Celery with Real-time Monitoring Tasks Using Flower",
                       'meta_desc': """Learn how to separate the time-consuming process and sent it to the Celery
                       to process it separately so that the rest of the sequence will keep continuing processing the
                       rest of the codes without delay. - """
+ settings.SITE_FULL_NAME})

    data = dict()
    if request.method == 'POST':
        form = Basic_CRUD_Create_Form(request.POST)
        GET_USER_TZN = request.POST.get('GET_USER_TZN')

        if form.is_valid():

            ''' Begin reCAPTCHA validation '''
            recaptcha_response = request.POST.get('g-recaptcha-response')
            data = {
                'secret': settings.GRECAP_SECRET_KEY,
                'response': recaptcha_response
            }
            r = requests.post(settings.GRECAP_VERIFY_URL, data=data)
            result = r.json()
            ''' End reCAPTCHA validation '''

            if result['success']:
                # Save form data successfully
                form.save()

                # Return some json response back to user
                msg = """ Your inquiry was sent successfully, thank you! """
                data = dict_alert_msg('True', 'Awesome!', msg, 'success')

                subject = form.cleaned_data.get("subject")
                email = form.cleaned_data.get("email")
                full_name = form.cleaned_data.get("full_name")
                message = form.cleaned_data.get("message")

                # Call the celery task async
                contactus_send_mail.delay(subject, email, full_name, message, GET_USER_TZN)

            else:

                # Return some json response back to user
                msg = """Invalid reCAPTCHA, please try again."""
                data = dict_alert_msg('False', 'Oops, Error', msg, 'error')

        else:

            # Extract form.errors
            msg = None
            msg = [(k, v[0]) for k, v in form.errors.items()]
            data = dict_alert_msg('False', 'Oops, Error', msg, 'error')

        return JsonResponse(data)

Again, we need to use the reCAPTCHA v3 to protect our Django form from the auto-bots provided by Google itself.

Next, create a new Django Template and saved it inside the dev/myroot/templates/myroot/ directory, and COPY exactly the code snippets below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
{% extends "myroot/layout_home.html" %}
{% load static %}

{% block extra_styles_head %}
<link rel="stylesheet" href="{% static 'summernote/summernote-bs4.css' %}">

<!-- START OF reCaptcha settings -->
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&amp;render=explicit" async="" defer=""></script>
<script type="text/javascript" defer="">
    var onloadCallback = function(){
        grecaptcha.render("gReCaptcha",{
            "sitekey": "{{ GRECAP_SITE_KEY }}",
            "badge": "inline",
            "type": "image",
            "size": "invisible",
            "callback": onSubmit
        });
    };

    var onSubmit = function(token){
        saveForm();
    }

    function validate(event){
        if (isRequiredFieldsPass()){
            event.preventDefault();
            grecaptcha.execute();
        }
    }
</script>
<!-- END OF reCaptcha settings -->
{% endblock %}

{% block page_sub_title %}
<div class="jumbotron">
    <div class="container">
        <h1 class="display-3">{{ title }}</h1>
        <p>
            This demonstration is to send an automatic email notification when you submit the form, you can see, that it will be faster
            the pop-up box will appear to let you know that your data has been submitted successfully.  Try to give your correct email address
            so that you can receive the email notification based on your entries.
        </p>
        <p>
            <a class="btn btn-primary btn-lg" href="https://pinoylearnpython.com/django-celery-with-real-time-monitoring-tasks-using-flower" role="button">Learn more »</a>
            <a class="btn btn-secondary btn-lg" href="https://dev.pinoylearnpython.com/django-basic-crud-list/" role="button">Back to Django Manage Table Rows »</a>
        </p>
    </div>
</div>
{% endblock %}

{% block content %}
    <div class="container">
        <div class="row">
            <div class="col-md-12 order-md-1">

                <form id="formContactUs" class="" method="POST">
                    {% csrf_token %}

                    <div class="form-label-group">
                        {{ form.full_name }}
                        <label for="id_full_name">Full Name</label>
                    </div>

                    <div class="form-label-group">
                        {{ form.email }}
                        <label for="id_email">Email</label>
                    </div>

                    <div class="form-label-group">
                        {{ form.subject }}
                        <label for="id_subject">Subject</label>
                    </div>

                    <div class="form-label-group">
                        {{ form.message }}
                    </div>

                    <!-- reCaptcha place holder-->
                    <div id="gReCaptcha"></div>

                </form>

                <div class="">
                    <button class="btn btn-lg btn-primary btn-block" onclick="saveForm();" id="btnContactUs">Submit your Inquiry</button>
                </div>

            </div>
        </div>
    </div>
{% endblock %}

{% block scripts %}
<script src="{% static 'assets/js/common/common.js' %}" defer=""></script>
<script src="{% static 'summernote/summernote-bs4.min.js' %}" defer=""></script>
<script src="{% static 'sweetalert/sweetalert.js' %}" defer=""></script>

<script defer="">
    var BASE_URL = "{{ BASE_URL }}";
    var GET_USER_TZN = Intl.DateTimeFormat().resolvedOptions().timeZone;

    function isRequiredFieldsPass()
    {
        id_full_name = $("#id_full_name").val();
        id_email = $("#id_email").val();
        id_subject = $("#id_subject").val();
        id_message = $("#id_message").val();

        if (id_full_name === undefined || id_full_name === null || id_full_name ===""){
            swal("Full Name is Required!", "Please enter your full name", "error");
            $("#id_full_name").focus();
            return false;
        }

        if (id_email === undefined || id_email === null || id_email ===""){
            swal("Email is Required!", "Please enter your email", "error");
            $("#id_email").focus();
            return false;
        }

        if (id_subject === undefined || id_subject === null || id_subject ===""){
            swal("Subject is Required!", "Please enter your subject", "error");
            $("#id_subject").focus();
            return false;
        }

        if (id_message === undefined || id_message === null || id_message ===""){
            swal("Message is Required!", "Please enter your message", "error");
            $("#id_message").focus();
            return false;
        }

        return true;
    }

    function saveForm()
    {
        //Get the form instance
        var $form = $("#formContactUs");
        id_full_name = $("#id_full_name").val();
        id_email = $("#id_email").val();
        id_subject = $("#id_subject").val();
        id_message = $("#id_message").val();

        if (id_full_name === undefined || id_full_name === null || id_full_name ===""){
            swal("Full Name is Required!", "Please enter your full name", "error");
            $("#id_full_name").focus();
            return false;
        }

        if (id_email === undefined || id_email === null || id_email ===""){
            swal("Email is Required!", "Please enter your email", "error");
            $("#id_email").focus();
            return false;
        }

        if (id_subject === undefined || id_subject === null || id_subject ===""){
            swal("Subject is Required!", "Please enter your subject", "error");
            $("#id_subject").focus();
            return false;
        }

        if (id_message === undefined || id_message === null || id_message ===""){
            swal("Message is Required!", "Please enter your message", "error");
            $("#id_message").focus();
            return false;
        }

        $.ajax({
            method: "POST",
            url: BASE_URL+'contactus_with_celery/',
            data: $form.serialize() + '&GET_USER_TZN='+GET_USER_TZN,
            cache: false,
            dataType: "json",
            beforeSend: function(){
                //Start displaying button's working animation
                var loadingText = '
<i class="fa fa-circle-o-notch fa-spin"></i> working...';
                if ($("#btnContactUs").html() !== loadingText) {
                    $("#btnContactUs").data('
original-text', $("#btnContactUs").html());
                    $("#btnContactUs").html(loadingText);
                }
            },
            success: function(jResults)
            {
                // Reload reCaptcha
                grecaptcha.reset();
                myutils.btnReCaptcha("btnContactUs");

                $("#btnContactUs").html($("#btnContactUs").data('
original-text')); //stop animation and switch back to original text

                if(jResults.alert_type =='
success'){
                    swal(jResults.alert_title, jResults.alert_msg, jResults.alert_type);
                    $form[0].reset(); // reset form data
                    $("#id_message").summernote("reset");
                    $("#id_full_name").focus();
                }
                else {
                    var strErr = jResults.alert_msg + '
';
                    strErr = strErr.split(",").pop();
                    swal(jResults.alert_title, strErr, jResults.alert_type);
                }
            }
        });
    }

    $(document).ready(function()
    {
        // Reload reCaptcha
        myutils.btnReCaptcha("btnContactUs");

        $('
#id_message').summernote({
            placeholder: 'Type your message here.',
            tabsize: 2,
            height: 300
        });
    });
</script>
{% endblock %}

Next, create a new subfolder named it as email_format to your web server under the /root/dev/myroot/templates/myroot/ directory and create a new HTML file and named it as email_contactus_with_celery.html. COPY exactly the code snippets below, this serves as the standard email HTML format as the email content that the user will receive via email.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">

  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="width=device-width">
    <title>{{ SITE_SHORT_NAME }}</title>
    {% load static %}
    <style>
      .wrapper {
  width: 100%; }

#outlook a {
  padding: 0; }

body {
  width: 100% !important;
  min-width: 100%;
  -webkit-text-size-adjust: 100%;
  -ms-text-size-adjust: 100%;
  margin: 0;
  Margin: 0;
  padding: 10px;
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
  box-sizing: border-box; }

.ExternalClass {
  width: 100%; }
  .ExternalClass,
  .ExternalClass p,
  .ExternalClass span,
  .ExternalClass font,
  .ExternalClass td,
  .ExternalClass div {
    line-height: 100%; }

#backgroundTable {
  margin: 0;
  Margin: 0;
  padding: 0;
  width: 100% !important;
  line-height: 100% !important; }

img {
  outline: none;
  text-decoration: none;
  -ms-interpolation-mode: bicubic;
  width: auto;
  max-width: 100%;
  clear: both;
  display: block; }

center {
  width: 100%;
  min-width: 580px; }

a img {
  border: none; }

p {
  margin: 0 0 0 10px;
  Margin: 0 0 0 10px; }

table {
  border-spacing: 0;
  border-collapse: collapse; }

td {
  word-wrap: break-word;
  -webkit-hyphens: auto;
  -moz-hyphens: auto;
  hyphens: auto;
  border-collapse: collapse !important; }

table, tr, td {
  padding: 0;
  vertical-align: top;
  text-align: left; }

@media only screen {
  html {
    min-height: 100%;
    background: #f3f3f3; } }

table.body {
  background: #f3f3f3;
  height: 100%;
  width: 100%; }

table.container {
  background: #fefefe;
  width: 580px;
  margin: 0 auto;
  Margin: 0 auto;
  text-align: inherit; }

table.row {
  padding: 0;
  width: 100%;
  position: relative; }

table.spacer {
  width: 100%; }
  table.spacer td {
    mso-line-height-rule: exactly; }

table.container table.row {
  display: table; }

td.columns,
td.column,
th.columns,
th.column {
  margin: 0 auto;
  Margin: 0 auto;
  padding-left: 16px;
  padding-bottom: 16px; }
  td.columns .column,
  td.columns .columns,
  td.column .column,
  td.column .columns,
  th.columns .column,
  th.columns .columns,
  th.column .column,
  th.column .columns {
    padding-left: 0 !important;
    padding-right: 0 !important; }
    td.columns .column center,
    td.columns .columns center,
    td.column .column center,
    td.column .columns center,
    th.columns .column center,
    th.columns .columns center,
    th.column .column center,
    th.column .columns center {
      min-width: none !important; }

td.columns.last,
td.column.last,
th.columns.last,
th.column.last {
  padding-right: 16px; }

td.columns table:not(.button),
td.column table:not(.button),
th.columns table:not(.button),
th.column table:not(.button) {
  width: 100%; }

td.large-1,
th.large-1 {
  width: 32.33333px;
  padding-left: 8px;
  padding-right: 8px; }

td.large-1.first,
th.large-1.first {
  padding-left: 16px; }

td.large-1.last,
th.large-1.last {
  padding-right: 16px; }

.collapse > tbody > tr > td.large-1,
.collapse > tbody > tr > th.large-1 {
  padding-right: 0;
  padding-left: 0;
  width: 48.33333px; }

.collapse td.large-1.first,
.collapse th.large-1.first,
.collapse td.large-1.last,
.collapse th.large-1.last {
  width: 56.33333px; }

td.large-1 center,
th.large-1 center {
  min-width: 0.33333px; }

.body .columns td.large-1,
.body .column td.large-1,
.body .columns th.large-1,
.body .column th.large-1 {
  width: 8.33333%; }

td.large-2,
th.large-2 {
  width: 80.66667px;
  padding-left: 8px;
  padding-right: 8px; }

td.large-2.first,
th.large-2.first {
  padding-left: 16px; }

td.large-2.last,
th.large-2.last {
  padding-right: 16px; }

.collapse > tbody > tr > td.large-2,
.collapse > tbody > tr > th.large-2 {
  padding-right: 0;
  padding-left: 0;
  width: 96.66667px; }

.collapse td.large-2.first,
.collapse th.large-2.first,
.collapse td.large-2.last,
.collapse th.large-2.last {
  width: 104.66667px; }

td.large-2 center,
th.large-2 center {
  min-width: 48.66667px; }

.body .columns td.large-2,
.body .column td.large-2,
.body .columns th.large-2,
.body .column th.large-2 {
  width: 16.66667%; }

td.large-3,
th.large-3 {
  width: 129px;
  padding-left: 8px;
  padding-right: 8px; }

td.large-3.first,
th.large-3.first {
  padding-left: 16px; }

td.large-3.last,
th.large-3.last {
  padding-right: 16px; }

.collapse > tbody > tr > td.large-3,
.collapse > tbody > tr > th.large-3 {
  padding-right: 0;
  padding-left: 0;
  width: 145px; }

.collapse td.large-3.first,
.collapse th.large-3.first,
.collapse td.large-3.last,
.collapse th.large-3.last {
  width: 153px; }

td.large-3 center,
th.large-3 center {
  min-width: 97px; }

.body .columns td.large-3,
.body .column td.large-3,
.body .columns th.large-3,
.body .column th.large-3 {
  width: 25%; }

td.large-4,
th.large-4 {
  width: 177.33333px;
  padding-left: 8px;
  padding-right: 8px; }

td.large-4.first,
th.large-4.first {
  padding-left: 16px; }

td.large-4.last,
th.large-4.last {
  padding-right: 16px; }

.collapse > tbody > tr > td.large-4,
.collapse > tbody > tr > th.large-4 {
  padding-right: 0;
  padding-left: 0;
  width: 193.33333px; }

.collapse td.large-4.first,
.collapse th.large-4.first,
.collapse td.large-4.last,
.collapse th.large-4.last {
  width: 201.33333px; }

td.large-4 center,
th.large-4 center {
  min-width: 145.33333px; }

.body .columns td.large-4,
.body .column td.large-4,
.body .columns th.large-4,
.body .column th.large-4 {
  width: 33.33333%; }

td.large-5,
th.large-5 {
  width: 225.66667px;
  padding-left: 8px;
  padding-right: 8px; }

td.large-5.first,
th.large-5.first {
  padding-left: 16px; }

td.large-5.last,
th.large-5.last {
  padding-right: 16px; }

.collapse > tbody > tr > td.large-5,
.collapse > tbody > tr > th.large-5 {
  padding-right: 0;
  padding-left: 0;
  width: 241.66667px; }

.collapse td.large-5.first,
.collapse th.large-5.first,
.collapse td.large-5.last,
.collapse th.large-5.last {
  width: 249.66667px; }

td.large-5 center,
th.large-5 center {
  min-width: 193.66667px; }

.body .columns td.large-5,
.body .column td.large-5,
.body .columns th.large-5,
.body .column th.large-5 {
  width: 41.66667%; }

td.large-6,
th.large-6 {
  width: 274px;
  padding-left: 8px;
  padding-right: 8px; }

td.large-6.first,
th.large-6.first {
  padding-left: 16px; }

td.large-6.last,
th.large-6.last {
  padding-right: 16px; }

.collapse > tbody > tr > td.large-6,
.collapse > tbody > tr > th.large-6 {
  padding-right: 0;
  padding-left: 0;
  width: 290px; }

.collapse td.large-6.first,
.collapse th.large-6.first,
.collapse td.large-6.last,
.collapse th.large-6.last {
  width: 298px; }

td.large-6 center,
th.large-6 center {
  min-width: 242px; }

.body .columns td.large-6,
.body .column td.large-6,
.body .columns th.large-6,
.body .column th.large-6 {
  width: 50%; }

td.large-7,
th.large-7 {
  width: 322.33333px;
  padding-left: 8px;
  padding-right: 8px; }

td.large-7.first,
th.large-7.first {
  padding-left: 16px; }

td.large-7.last,
th.large-7.last {
  padding-right: 16px; }

.collapse > tbody > tr > td.large-7,
.collapse > tbody > tr > th.large-7 {
  padding-right: 0;
  padding-left: 0;
  width: 338.33333px; }

.collapse td.large-7.first,
.collapse th.large-7.first,
.collapse td.large-7.last,
.collapse th.large-7.last {
  width: 346.33333px; }

td.large-7 center,
th.large-7 center {
  min-width: 290.33333px; }

.body .columns td.large-7,
.body .column td.large-7,
.body .columns th.large-7,
.body .column th.large-7 {
  width: 58.33333%; }

td.large-8,
th.large-8 {
  width: 370.66667px;
  padding-left: 8px;
  padding-right: 8px; }

td.large-8.first,
th.large-8.first {
  padding-left: 16px; }

td.large-8.last,
th.large-8.last {
  padding-right: 16px; }

.collapse > tbody > tr > td.large-8,
.collapse > tbody > tr > th.large-8 {
  padding-right: 0;
  padding-left: 0;
  width: 386.66667px; }

.collapse td.large-8.first,
.collapse th.large-8.first,
.collapse td.large-8.last,
.collapse th.large-8.last {
  width: 394.66667px; }

td.large-8 center,
th.large-8 center {
  min-width: 338.66667px; }

.body .columns td.large-8,
.body .column td.large-8,
.body .columns th.large-8,
.body .column th.large-8 {
  width: 66.66667%; }

td.large-9,
th.large-9 {
  width: 419px;
  padding-left: 8px;
  padding-right: 8px; }

td.large-9.first,
th.large-9.first {
  padding-left: 16px; }

td.large-9.last,
th.large-9.last {
  padding-right: 16px; }

.collapse > tbody > tr > td.large-9,
.collapse > tbody > tr > th.large-9 {
  padding-right: 0;
  padding-left: 0;
  width: 435px; }

.collapse td.large-9.first,
.collapse th.large-9.first,
.collapse td.large-9.last,
.collapse th.large-9.last {
  width: 443px; }

td.large-9 center,
th.large-9 center {
  min-width: 387px; }

.body .columns td.large-9,
.body .column td.large-9,
.body .columns th.large-9,
.body .column th.large-9 {
  width: 75%; }

td.large-10,
th.large-10 {
  width: 467.33333px;
  padding-left: 8px;
  padding-right: 8px; }

td.large-10.first,
th.large-10.first {
  padding-left: 16px; }

td.large-10.last,
th.large-10.last {
  padding-right: 16px; }

.collapse > tbody > tr > td.large-10,
.collapse > tbody > tr > th.large-10 {
  padding-right: 0;
  padding-left: 0;
  width: 483.33333px; }

.collapse td.large-10.first,
.collapse th.large-10.first,
.collapse td.large-10.last,
.collapse th.large-10.last {
  width: 491.33333px; }

td.large-10 center,
th.large-10 center {
  min-width: 435.33333px; }

.body .columns td.large-10,
.body .column td.large-10,
.body .columns th.large-10,
.body .column th.large-10 {
  width: 83.33333%; }

td.large-11,
th.large-11 {
  width: 515.66667px;
  padding-left: 8px;
  padding-right: 8px; }

td.large-11.first,
th.large-11.first {
  padding-left: 16px; }

td.large-11.last,
th.large-11.last {
  padding-right: 16px; }

.collapse > tbody > tr > td.large-11,
.collapse > tbody > tr > th.large-11 {
  padding-right: 0;
  padding-left: 0;
  width: 531.66667px; }

.collapse td.large-11.first,
.collapse th.large-11.first,
.collapse td.large-11.last,
.collapse th.large-11.last {
  width: 539.66667px; }

td.large-11 center,
th.large-11 center {
  min-width: 483.66667px; }

.body .columns td.large-11,
.body .column td.large-11,
.body .columns th.large-11,
.body .column th.large-11 {
  width: 91.66667%; }

td.large-12,
th.large-12 {
  width: 564px;
  padding-left: 8px;
  padding-right: 8px; }

td.large-12.first,
th.large-12.first {
  padding-left: 16px; }

td.large-12.last,
th.large-12.last {
  padding-right: 16px; }

.collapse > tbody > tr > td.large-12,
.collapse > tbody > tr > th.large-12 {
  padding-right: 0;
  padding-left: 0;
  width: 580px; }

.collapse td.large-12.first,
.collapse th.large-12.first,
.collapse td.large-12.last,
.collapse th.large-12.last {
  width: 588px; }

td.large-12 center,
th.large-12 center {
  min-width: 532px; }

.body .columns td.large-12,
.body .column td.large-12,
.body .columns th.large-12,
.body .column th.large-12 {
  width: 100%; }

td.large-offset-1,
td.large-offset-1.first,
td.large-offset-1.last,
th.large-offset-1,
th.large-offset-1.first,
th.large-offset-1.last {
  padding-left: 64.33333px; }

td.large-offset-2,
td.large-offset-2.first,
td.large-offset-2.last,
th.large-offset-2,
th.large-offset-2.first,
th.large-offset-2.last {
  padding-left: 112.66667px; }

td.large-offset-3,
td.large-offset-3.first,
td.large-offset-3.last,
th.large-offset-3,
th.large-offset-3.first,
th.large-offset-3.last {
  padding-left: 161px; }

td.large-offset-4,
td.large-offset-4.first,
td.large-offset-4.last,
th.large-offset-4,
th.large-offset-4.first,
th.large-offset-4.last {
  padding-left: 209.33333px; }

td.large-offset-5,
td.large-offset-5.first,
td.large-offset-5.last,
th.large-offset-5,
th.large-offset-5.first,
th.large-offset-5.last {
  padding-left: 257.66667px; }

td.large-offset-6,
td.large-offset-6.first,
td.large-offset-6.last,
th.large-offset-6,
th.large-offset-6.first,
th.large-offset-6.last {
  padding-left: 306px; }

td.large-offset-7,
td.large-offset-7.first,
td.large-offset-7.last,
th.large-offset-7,
th.large-offset-7.first,
th.large-offset-7.last {
  padding-left: 354.33333px; }

td.large-offset-8,
td.large-offset-8.first,
td.large-offset-8.last,
th.large-offset-8,
th.large-offset-8.first,
th.large-offset-8.last {
  padding-left: 402.66667px; }

td.large-offset-9,
td.large-offset-9.first,
td.large-offset-9.last,
th.large-offset-9,
th.large-offset-9.first,
th.large-offset-9.last {
  padding-left: 451px; }

td.large-offset-10,
td.large-offset-10.first,
td.large-offset-10.last,
th.large-offset-10,
th.large-offset-10.first,
th.large-offset-10.last {
  padding-left: 499.33333px; }

td.large-offset-11,
td.large-offset-11.first,
td.large-offset-11.last,
th.large-offset-11,
th.large-offset-11.first,
th.large-offset-11.last {
  padding-left: 547.66667px; }

td.expander,
th.expander {
  visibility: hidden;
  width: 0;
  padding: 0 !important; }

table.container.radius {
  border-radius: 0;
  border-collapse: separate; }

.block-grid {
  width: 100%;
  max-width: 580px; }
  .block-grid td {
    display: inline-block;
    padding: 8px; }

.up-2 td {
  width: 274px !important; }

.up-3 td {
  width: 177px !important; }

.up-4 td {
  width: 129px !important; }

.up-5 td {
  width: 100px !important; }

.up-6 td {
  width: 80px !important; }

.up-7 td {
  width: 66px !important; }

.up-8 td {
  width: 56px !important; }

table.text-center,
th.text-center,
td.text-center,
h1.text-center,
h2.text-center,
h3.text-center,
h4.text-center,
h5.text-center,
h6.text-center,
p.text-center,
span.text-center {
  text-align: center; }

table.text-left,
th.text-left,
td.text-left,
h1.text-left,
h2.text-left,
h3.text-left,
h4.text-left,
h5.text-left,
h6.text-left,
p.text-left,
span.text-left {
  text-align: left; }

table.text-right,
th.text-right,
td.text-right,
h1.text-right,
h2.text-right,
h3.text-right,
h4.text-right,
h5.text-right,
h6.text-right,
p.text-right,
span.text-right {
  text-align: right; }

span.text-center {
  display: block;
  width: 100%;
  text-align: center; }

@media only screen and (max-width: 596px) {
  .small-float-center {
    margin: 0 auto !important;
    float: none !important;
    text-align: center !important; }
  .small-text-center {
    text-align: center !important; }
  .small-text-left {
    text-align: left !important; }
  .small-text-right {
    text-align: right !important; } }

img.float-left {
  float: left;
  text-align: left; }

img.float-right {
  float: right;
  text-align: right; }

img.float-center,
img.text-center {
  margin: 0 auto;
  Margin: 0 auto;
  float: none;
  text-align: center; }

table.float-center,
td.float-center,
th.float-center {
  margin: 0 auto;
  Margin: 0 auto;
  float: none;
  text-align: center; }

.hide-for-large {
  display: none !important;
  mso-hide: all;
  overflow: hidden;
  max-height: 0;
  font-size: 0;
  width: 0;
  line-height: 0; }
  @media only screen and (max-width: 596px) {
    .hide-for-large {
      display: block !important;
      width: auto !important;
      overflow: visible !important;
      max-height: none !important;
      font-size: inherit !important;
      line-height: inherit !important; } }

table.body table.container .hide-for-large * {
  mso-hide: all; }

@media only screen and (max-width: 596px) {
  table.body table.container .hide-for-large,
  table.body table.container .row.hide-for-large {
    display: table !important;
    width: 100% !important; } }

@media only screen and (max-width: 596px) {
  table.body table.container .callout-inner.hide-for-large {
    display: table-cell !important;
    width: 100% !important; } }

@media only screen and (max-width: 596px) {
  table.body table.container .show-for-large {
    display: none !important;
    width: 0;
    mso-hide: all;
    overflow: hidden; } }

body,
table.body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
td,
th,
a {
  color: #0a0a0a;
  font-family: Helvetica, Arial, sans-serif;
  font-weight: normal;
  padding: 0;
  margin: 0;
  Margin: 0;
  text-align: left;
  line-height: 1.3; }

h1,
h2,
h3,
h4,
h5,
h6 {
  color: inherit;
  word-wrap: normal;
  font-family: Helvetica, Arial, sans-serif;
  font-weight: normal;
  margin-bottom: 10px;
  Margin-bottom: 10px; }

h1 {
  font-size: 34px; }

h2 {
  font-size: 30px; }

h3 {
  font-size: 28px; }

h4 {
  font-size: 24px; }

h5 {
  font-size: 20px; }

h6 {
  font-size: 18px; }

body,
table.body,
p,
td,
th {
  font-size: 16px;
  line-height: 1.3; }

p {
  margin-bottom: 10px;
  Margin-bottom: 10px; }
  p.lead {
    font-size: 20px;
    line-height: 1.6; }
  p.subheader {
    margin-top: 4px;
    margin-bottom: 8px;
    Margin-top: 4px;
    Margin-bottom: 8px;
    font-weight: normal;
    line-height: 1.4;
    color: #8a8a8a; }

small {
  font-size: 80%;
  color: #cacaca; }

a {
  color: #2199e8;
  text-decoration: none; }
  a:hover {
    color: #147dc2; }
  a:active {
    color: #147dc2; }
  a:visited {
    color: #2199e8; }

h1 a,
h1 a:visited,
h2 a,
h2 a:visited,
h3 a,
h3 a:visited,
h4 a,
h4 a:visited,
h5 a,
h5 a:visited,
h6 a,
h6 a:visited {
  color: #2199e8; }

pre {
  background: #f3f3f3;
  margin: 30px 0;
  Margin: 30px 0; }
  pre code {
    color: #cacaca; }
    pre code span.callout {
      color: #8a8a8a;
      font-weight: bold; }
    pre code span.callout-strong {
      color: #ff6908;
      font-weight: bold; }

table.hr {
  width: 100%; }
  table.hr th {
    height: 0;
    max-width: 580px;
    border-top: 0;
    border-right: 0;
    border-bottom: 1px solid #0a0a0a;
    border-left: 0;
    margin: 20px auto;
    Margin: 20px auto;
    clear: both; }

.stat {
  font-size: 40px;
  line-height: 1; }
  p + .stat {
    margin-top: -16px;
    Margin-top: -16px; }

span.preheader {
  display: none !important;
  visibility: hidden;
  mso-hide: all !important;
  font-size: 1px;
  color: #f3f3f3;
  line-height: 1px;
  max-height: 0px;
  max-width: 0px;
  opacity: 0;
  overflow: hidden; }

table.button {
  width: auto;
  margin: 0 0 16px 0;
  Margin: 0 0 16px 0; }
  table.button table td {
    text-align: left;
    color: #fefefe;
    background: #2199e8;
    border: 2px solid #2199e8; }
    table.button table td a {
      font-family: Helvetica, Arial, sans-serif;
      font-size: 16px;
      font-weight: bold;
      color: #fefefe;
      text-decoration: none;
      display: inline-block;
      padding: 8px 16px 8px 16px;
      border: 0 solid #2199e8;
      border-radius: 3px; }
  table.button.radius table td {
    border-radius: 3px;
    border: none; }
  table.button.rounded table td {
    border-radius: 500px;
    border: none; }

table.button:hover table tr td a,
table.button:active table tr td a,
table.button table tr td a:visited,
table.button.tiny:hover table tr td a,
table.button.tiny:active table tr td a,
table.button.tiny table tr td a:visited,
table.button.small:hover table tr td a,
table.button.small:active table tr td a,
table.button.small table tr td a:visited,
table.button.large:hover table tr td a,
table.button.large:active table tr td a,
table.button.large table tr td a:visited {
  color: #fefefe; }

table.button.tiny table td,
table.button.tiny table a {
  padding: 4px 8px 4px 8px; }

table.button.tiny table a {
  font-size: 10px;
  font-weight: normal; }

table.button.small table td,
table.button.small table a {
  padding: 5px 10px 5px 10px;
  font-size: 12px; }

table.button.large table a {
  padding: 10px 20px 10px 20px;
  font-size: 20px; }

table.button.expand,
table.button.expanded {
  width: 100% !important; }
  table.button.expand table,
  table.button.expanded table {
    width: 100%; }
    table.button.expand table a,
    table.button.expanded table a {
      text-align: center;
      width: 100%;
      padding-left: 0;
      padding-right: 0; }
  table.button.expand center,
  table.button.expanded center {
    min-width: 0; }

table.button:hover table td,
table.button:visited table td,
table.button:active table td {
  background: #147dc2;
  color: #fefefe; }

table.button:hover table a,
table.button:visited table a,
table.button:active table a {
  border: 0 solid #147dc2; }

table.button.secondary table td {
  background: #777777;
  color: #fefefe;
  border: 0px solid #777777; }

table.button.secondary table a {
  color: #fefefe;
  border: 0 solid #777777; }

table.button.secondary:hover table td {
  background: #919191;
  color: #fefefe; }

table.button.secondary:hover table a {
  border: 0 solid #919191; }

table.button.secondary:hover table td a {
  color: #fefefe; }

table.button.secondary:active table td a {
  color: #fefefe; }

table.button.secondary table td a:visited {
  color: #fefefe; }

table.button.success table td {
  background: #3adb76;
  border: 0px solid #3adb76; }

table.button.success table a {
  border: 0 solid #3adb76; }

table.button.success:hover table td {
  background: #23bf5d; }

table.button.success:hover table a {
  border: 0 solid #23bf5d; }

table.button.alert table td {
  background: #ec5840;
  border: 0px solid #ec5840; }

table.button.alert table a {
  border: 0 solid #ec5840; }

table.button.alert:hover table td {
  background: #e23317; }

table.button.alert:hover table a {
  border: 0 solid #e23317; }

table.button.warning table td {
  background: #ffae00;
  border: 0px solid #ffae00; }

table.button.warning table a {
  border: 0px solid #ffae00; }

table.button.warning:hover table td {
  background: #cc8b00; }

table.button.warning:hover table a {
  border: 0px solid #cc8b00; }

table.callout {
  margin-bottom: 16px;
  Margin-bottom: 16px; }

th.callout-inner {
  width: 100%;
  border: 1px solid #cbcbcb;
  padding: 10px;
  background: #fefefe; }
  th.callout-inner.primary {
    background: #def0fc;
    border: 1px solid #444444;
    color: #0a0a0a; }
  th.callout-inner.secondary {
    background: #ebebeb;
    border: 1px solid #444444;
    color: #0a0a0a; }
  th.callout-inner.success {
    background: #e1faea;
    border: 1px solid #1b9448;
    color: #fefefe; }
  th.callout-inner.warning {
    background: #fff3d9;
    border: 1px solid #996800;
    color: #fefefe; }
  th.callout-inner.alert {
    background: #fce6e2;
    border: 1px solid #b42912;
    color: #fefefe; }

.thumbnail {
  border: solid 4px #fefefe;
  box-shadow: 0 0 0 1px rgba(10, 10, 10, 0.2);
  display: inline-block;
  line-height: 0;
  max-width: 100%;
  transition: box-shadow 200ms ease-out;
  border-radius: 3px;
  margin-bottom: 16px; }
  .thumbnail:hover, .thumbnail:focus {
    box-shadow: 0 0 6px 1px rgba(33, 153, 232, 0.5); }

table.menu {
  width: 580px; }
  table.menu td.menu-item,
  table.menu th.menu-item {
    padding: 10px;
    padding-right: 10px; }
    table.menu td.menu-item a,
    table.menu th.menu-item a {
      color: #2199e8; }

table.menu.vertical td.menu-item,
table.menu.vertical th.menu-item {
  padding: 10px;
  padding-right: 0;
  display: block; }
  table.menu.vertical td.menu-item a,
  table.menu.vertical th.menu-item a {
    width: 100%; }

table.menu.vertical td.menu-item table.menu.vertical td.menu-item,
table.menu.vertical td.menu-item table.menu.vertical th.menu-item,
table.menu.vertical th.menu-item table.menu.vertical td.menu-item,
table.menu.vertical th.menu-item table.menu.vertical th.menu-item {
  padding-left: 10px; }

table.menu.text-center a {
  text-align: center; }

.menu[align="center"] {
  width: auto !important; }

body.outlook p {
  display: inline !important; }

@media only screen and (max-width: 596px) {
  table.body img {
    width: auto;
    height: auto; }
  table.body center {
    min-width: 0 !important; }
  table.body .container {
    width: 95% !important; }
  table.body .columns,
  table.body .column {
    height: auto !important;
    -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    padding-left: 16px !important;
    padding-right: 16px !important; }
    table.body .columns .column,
    table.body .columns .columns,
    table.body .column .column,
    table.body .column .columns {
      padding-left: 0 !important;
      padding-right: 0 !important; }
  table.body .collapse .columns,
  table.body .collapse .column {
    padding-left: 0 !important;
    padding-right: 0 !important; }
  td.small-1,
  th.small-1 {
    display: inline-block !important;
    width: 8.33333% !important; }
  td.small-2,
  th.small-2 {
    display: inline-block !important;
    width: 16.66667% !important; }
  td.small-3,
  th.small-3 {
    display: inline-block !important;
    width: 25% !important; }
  td.small-4,
  th.small-4 {
    display: inline-block !important;
    width: 33.33333% !important; }
  td.small-5,
  th.small-5 {
    display: inline-block !important;
    width: 41.66667% !important; }
  td.small-6,
  th.small-6 {
    display: inline-block !important;
    width: 50% !important; }
  td.small-7,
  th.small-7 {
    display: inline-block !important;
    width: 58.33333% !important; }
  td.small-8,
  th.small-8 {
    display: inline-block !important;
    width: 66.66667% !important; }
  td.small-9,
  th.small-9 {
    display: inline-block !important;
    width: 75% !important; }
  td.small-10,
  th.small-10 {
    display: inline-block !important;
    width: 83.33333% !important; }
  td.small-11,
  th.small-11 {
    display: inline-block !important;
    width: 91.66667% !important; }
  td.small-12,
  th.small-12 {
    display: inline-block !important;
    width: 100% !important; }
  .columns td.small-12,
  .column td.small-12,
  .columns th.small-12,
  .column th.small-12 {
    display: block !important;
    width: 100% !important; }
  table.body td.small-offset-1,
  table.body th.small-offset-1 {
    margin-left: 8.33333% !important;
    Margin-left: 8.33333% !important; }
  table.body td.small-offset-2,
  table.body th.small-offset-2 {
    margin-left: 16.66667% !important;
    Margin-left: 16.66667% !important; }
  table.body td.small-offset-3,
  table.body th.small-offset-3 {
    margin-left: 25% !important;
    Margin-left: 25% !important; }
  table.body td.small-offset-4,
  table.body th.small-offset-4 {
    margin-left: 33.33333% !important;
    Margin-left: 33.33333% !important; }
  table.body td.small-offset-5,
  table.body th.small-offset-5 {
    margin-left: 41.66667% !important;
    Margin-left: 41.66667% !important; }
  table.body td.small-offset-6,
  table.body th.small-offset-6 {
    margin-left: 50% !important;
    Margin-left: 50% !important; }
  table.body td.small-offset-7,
  table.body th.small-offset-7 {
    margin-left: 58.33333% !important;
    Margin-left: 58.33333% !important; }
  table.body td.small-offset-8,
  table.body th.small-offset-8 {
    margin-left: 66.66667% !important;
    Margin-left: 66.66667% !important; }
  table.body td.small-offset-9,
  table.body th.small-offset-9 {
    margin-left: 75% !important;
    Margin-left: 75% !important; }
  table.body td.small-offset-10,
  table.body th.small-offset-10 {
    margin-left: 83.33333% !important;
    Margin-left: 83.33333% !important; }
  table.body td.small-offset-11,
  table.body th.small-offset-11 {
    margin-left: 91.66667% !important;
    Margin-left: 91.66667% !important; }
  table.body table.columns td.expander,
  table.body table.columns th.expander {
    display: none !important; }
  table.body .right-text-pad,
  table.body .text-pad-right {
    padding-left: 10px !important; }
  table.body .left-text-pad,
  table.body .text-pad-left {
    padding-right: 10px !important; }
  table.menu {
    width: 100% !important; }
    table.menu td,
    table.menu th {
      width: auto !important;
      display: inline-block !important; }
    table.menu.vertical td,
    table.menu.vertical th, table.menu.small-vertical td,
    table.menu.small-vertical th {
      display: block !important; }
  table.menu[align="center"] {
    width: auto !important; }
  table.button.small-expand,
  table.button.small-expanded {
    width: 100% !important; }
    table.button.small-expand table,
    table.button.small-expanded table {
      width: 100%; }
      table.button.small-expand table a,
      table.button.small-expanded table a {
        text-align: center !important;
        width: 100% !important;
        padding-left: 0 !important;
        padding-right: 0 !important; }
    table.button.small-expand center,
    table.button.small-expanded center {
      min-width: 0; } }

    </style>

    <style>
      body,
      html,
      .body {
        background: #f3f3f3 !important;
      }

      .container.header {
        background: #f3f3f3;
      }

      .body-border {
        border-top: 8px solid {{ APP_SITE_TEMPLATE_COLOR }};
      }
    </style>
  </head>

  <body>
    <!-- <style> -->
    <table class="body" data-made-with-foundation="">
      <tr>
        <td class="float-center" align="center" valign="top">
          <center data-parsed="">

            <table align="center" class="container body-border float-center">
              <tbody>
                <tr>
                  <td>
                    <table class="row">
                      <tbody>
                        <tr>
                          <th class="small-12 large-12 columns first last">
                            <table>
                              <tr>
                                <th>

                                  <table class="spacer">
                                    <tbody>
                                      <tr>
                                        <td>
                                            <h1 class="text-center">{{ EMAIL_HEADER_TITLE }}</h1>
                                        </td>
                                      </tr>
                                    </tbody>
                                  </table>

                                  <center data-parsed=""> <img src="{{ APP_URL_TOP_LOGO }}" align="center" class="float-center"> </center>
                                  <table class="spacer">
                                    <tbody>
                                      <tr>
                                        <td height="16px" style="font-size:16px;line-height:16px;">&#xA0;</td>
                                      </tr>
                                    </tbody>
                                  </table>
                                  <h4>{{ EMAIL_SUB_TITLE }}</h4>

                                  <p>Hi <strong>{{ FULL_NAME }}</strong></p>
                                  <p>&nbsp;</p>

                                  <p>We have received your message and would like to thank you for writing to us and we will reply to you via email shortly.</p>

                                  <p>Below is your inquiry details.</p>
                                  <p>&nbsp;</p>

                                  <p>
                                      <strong>SUBJECT:</strong> {{ SUBJECT }}<br>
                                      <strong>SUBMITTED:</strong> {{ SENT_DT }}<br>
                                      <strong>MESSAGE:</strong><br/>
                                      {{ SENT_MESSAGE|safe }}
                                  </p>
                                  <p>&nbsp;</p>

                                  <p>
                                      This email has been processed using the Django Celery tutorials provided by <a href="{{ BASE_URL }}">{{ SITE_FULL_NAME }}</a>.
                                      You can learn more about this guide on <a href="https://pinoylearnpython.com/django-celery-with-real-time-monitoring-tasks-using-flower">Django Celery with Real-time Monitoring Tasks Using Flower</a>,
                                      you can revisit the tutorial for more details.
                                  </p>

                                  <p>Have a great day ahead, Cheers!</p><br><br>
                                  <p>For your Python and Django Learning Needs!<br/>
                                      The <strong>{{ SITE_FULL_NAME }}</strong> Tutorials
                                  </p>

                                  <center data-parsed="">
                                    <table align="center" class="menu float-center">
                                      <tr>
                                        <td>
                                          <table>
                                            <tr>
                                              <th class="menu-item float-center"><a href="{{ BASE_URL }}">{{ SITE_FULL_NAME }}</a></th>
                                            </tr>
                                          </table>
                                        </td>
                                      </tr>
                                    </table>
                                  </center>
                                </th>
                                <th class="expander"></th>
                              </tr>
                            </table>
                          </th>
                        </tr>
                      </tbody>
                    </table>
                    <table class="spacer">
                      <tbody>
                        <tr>
                          <td height="16px" style="font-size:16px;line-height:16px;">&#xA0;</td>
                        </tr>
                      </tbody>
                    </table>
                  </td>
                </tr>
              </tbody>
            </table>
          </center>
        </td>
      </tr>
    </table>
  </body>

</html>

As usual, we need to create a new link to access our public page, open up your dev/dev/urls.py and insert the code below.

1
path('contactus_with_celery/', myroot.views.vmain.contactus_with_celery_view, name='contactus_with_celery'),

Now, it’s time to upload everything to our web server using the FileZilla. Great, now, try accessing the newly setup page here.

https://dev.pinoylearnpython.com/contactus_with_celery/

Try submitting with the correct email address so that you can receive a copy of your data to your email address. You may notice that the pop-up box is faster to show up that your information entered from the Django Form has been sent directly to your email inbox or spam email.

Great!, as I have check out the Flower monitoring web interface, I fill up the Django form and submit it 2 times to test the workers how exactly they work and behave, it’s lightning fast how these workers process my 2 requests and return without delay.

Meaning, these 2 Celery workers take a turn to each other and distribute the tasks to any vacant workers and process it accordingly, and it’s a proof that these 2 workers are working for us without delay and anytime ready to accept tasks.

Step 7: Test Django Form without Celery

Now, it’s time to test the blocking non-asynchronous Django Views to automatically send an email notification when the Django Form submitted.

First of all, I will re-use the same ContactUs model and remove the Celery triggered events, then I created a new Django View called the contactus_without_celery_view for this illustration. Still at the same dev/myroot/views directory and save this file there. COPY exactly the code snippets below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def contactus_without_celery_view(request):
    """Renders the contact us page without Celery task when sending an email."""
    if request.method == 'GET':

        # Get contact us form to display
        form = Basic_CRUD_Create_Form()
        return render(request, 'myroot/contactus_without_celery.html',
                      {'form': form, 'title': "See the Difference Without the Django Celery Distributed Tasks.",
                       'meta_desc': """To illustrate the difference between the normal sending email without the Django Celery to
                       separate the time-consuming process and sent it to the Celery
                       to process it separately so that the rest of the sequence will keep continuing processing the
                       rest of the codes without delay. - """
+ settings.SITE_FULL_NAME})

    data = dict()
    if request.method == 'POST':
        form = Basic_CRUD_Create_Form(request.POST)
        GET_USER_TZN = request.POST.get('GET_USER_TZN')

        if form.is_valid():

            ''' Begin reCAPTCHA validation '''
            recaptcha_response = request.POST.get('g-recaptcha-response')
            data = {
                'secret': settings.GRECAP_SECRET_KEY,
                'response': recaptcha_response
            }
            r = requests.post(settings.GRECAP_VERIFY_URL, data=data)
            result = r.json()
            ''' End reCAPTCHA validation '''

            if result['success']:
                # Save form data successfully
                form.save()

                # Return some json response back to user
                msg = """ Your inquiry was sent successfully, thank you! """
                data = dict_alert_msg('True', 'Awesome!', msg, 'success')

                subject = form.cleaned_data.get("subject")
                email = form.cleaned_data.get("email")
                full_name = form.cleaned_data.get("full_name")
                message = form.cleaned_data.get("message")

                # Send an email without the Celery triggered event
                do_send_email('email_contactus_with_celery.html',
                              subject,
                              settings.APP_EMAIL_FROM,
                              (email,),
                              'Thank you for getting in touch!',
                              arrow.utcnow().to(GET_USER_TZN).format('MMM DD, YYYY hh:mm a'),
                              'Awesome!',
                              full_name,
                              None,
                              message)

            else:

                # Return some json response back to user
                msg = """Invalid reCAPTCHA, please try again."""
                data = dict_alert_msg('False', 'Oops, Error', msg, 'error')

        else:

            # Extract form.errors
            msg = None
            msg = [(k, v[0]) for k, v in form.errors.items()]
            data = dict_alert_msg('False', 'Oops, Error', msg, 'error')

        return JsonResponse(data)

The only key here is these lines of code, to manually execute the blocking code in which in this example is to automatically send an email notification to the user upon the Django form submission.

1
2
3
4
5
6
7
8
9
10
11
# Send an email without the Celery triggered event
do_send_email('email_contactus_with_celery.html',
              subject,
              settings.APP_EMAIL_FROM,
              (email,),
              'Thank you for getting in touch!',
              arrow.utcnow().to(GET_USER_TZN).format('MMM DD, YYYY hh:mm a'),
              'Awesome!',
              full_name,
              None,
              message)

Instead of separating this block of codes to be executed asynchronously using the Celery process to exclude long pooling scripts and instead of waiting until it will be done, the Celery will then process it in the background and return the results later on as expected.

And this means, we have faster application processes, the only key for the Celery shared_task() event to be triggered is by this .delay() command.

1
2
# Call the celery task async
contactus_send_mail.delay(subject, email, full_name, message, GET_USER_TZN)

Calling the custom functions which is the contactus_send_mail.delay() with parameters from the tasks.py.

Next, create a new access link for this new testing page, open up your dev/dev/urls.py and COPY exactly the code snippets below.

1
path('contactus_without_celery/', myroot.views.vmain.contactus_without_celery_view, name='contactus_without_celery'),

I also have created a new Django template for this example, but, the only difference is the AJAX post URL that points to the contactus_without_celery URL.

Now, it’s time to access this demo page and see the slow difference upon the Django form submission.

https://dev.pinoylearnpython.com/contactus_without_celery/

As you have observed it during the form submission, it will be stuck for a moment and the pop-up information box is not quick to show up. This is a big difference, it will not skip the sending of email code and instead, it waits until it will be completed, then, it will continue again the rest of the codes.

This is how, the strength of the Celery has been appreciated and built for this purpose to separate the long pooling block of codes you have and process it in the background and then return back to us bearing the expected results.

Step 8: Install Django Celery Beat Schedule

Now, for the most part, we need to schedule one task as an example for this demonstration first, but, this is one of the interesting guides as we can’t avoid scheduling a task within our Django project.

In this example, I will use still the ContactUs model, as you have noticed, we don’t delete totally the rows during the delete operation, it’s just marked as “is_deleted=True“, it’s actually an update operation, but, later on, we need to delete physically from the table all the rows marked as “is_deleted=True” for some scheduled timing.

To accomplish this tasks, Django Celery Beat Scheduler is the solutions, and I recommend it personally because, you can maintain the scheduler inside the Django Admin Site, you can change it anytime you want to re-schedule as you wish, unlike from the previous older versions of Celery scheduler is that it’s a fixed hard-coded scheduled embedded from the Python function as a decorator by itself.

First of all, we need to install the Django Celery Beat using the pip command, again, make it sure that you are still activated the VirtualEnv to contain the installation package for this program.

1
pip3 install -U django-celery-beat

It should be quiet straight forward installation process, now, add the “django_celery_beat” in your settings.py.

1
2
3
4
5
# Application definition
INSTALLED_APPS = [
  ...
  'django_celery_beat',
]

Next, the “django_celery_beat” contains a Django Models as well, we need to run the migration command to create its own tables.

1
python3 manage.py migrate

Afterward, these tables will be created inside your default database.

In the meantime, still, remember how to create a new .conf file to include this “django_celery_beat” program in the Supervisor to manage it there. It has it’s own heartbeat for the program as well that needs to be running uninterruptedly. So, create a new config file and named it as the “dev_celery_scheduler.conf“, saved it under the supervisor/conf.d directory.

1
2
3
4
5
6
7
8
9
10
11
[program:dev_celery_scheduler]
command=/root/env_dev/bin/celery -A dev beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
directory=/root/dev/
user=root
numprocs=1
autostart=true
autorestart=true
startsecs=10
stopwaitsecs = 600
killasgroup=true
priority=998

Now, again, change all the necessary Django project name and program name based on your own project name. Then, upload this new config file and update the Supervisor program to take the new changes.

1
2
sudo supervisorctl reread
sudo supervisorctl update

Again, check your Supervisor web interface and see if the newly added program to maintain is there or not and its corresponding status.

Great!, it’s now up and running, it’s ready to accept any scheduled tasks to be executed.

Next, create a new Celery task to delete the physical rows that marked as is_deleted=True and we would like to register this as scheduled tasks. Again, open up your dev/myroot/views/tasks.py, then add a new Python function, on top of it, include the shared_task() that indicates this function registered as the Celery process.

First of all, include the ContactUs model at the header section of the tasks.py.

1
2
3
...
from myroot.models import ContactUs
...

Then, insert these line of codes inside your tasks.py, COPY exactly the code snippets below.

1
2
3
4
5
6
7
@shared_task()
def del_contact_us():
    """
    Scheduled tasks to automatically delete all ContactUs data
    marked as 'is_deleted=True'.
    """

    ContactUs.objects.filter(is_deleted=True).delete()

As you can see, we issue the command .delete(), meaning, that all the rows marked as is_deleted=True will be physically removed from the table and that would be permanently be deleted and can’t undo this changes.

Again, upload the modified tasks.py to your web server using the FileZilla and then restart the Daphne web service and the 2 Celery workers using the Supervisor web interface to easily manage it.

Step 9: Manage Django Celery Beat using the Django Site Administration

Moreover, to manage all the registered Celery process inside the Django Site Admin, by the way, this is the actual screenshot for my Django Site Admin after the migration process. Take a look for a while.

The Periodic Tasks group will be added, now, we can manage the Crontabs, Intervals, Periodic tasks, and the Solar events. First of all, you must set up first the Crontabs or Intervals, before you can create your Periodic Tasks.

But, before you will create the Crontabs or Intervals, make it sure, that you are working with your current timezone or leave it as a default UTC, from our settings.py if you may, as of this time, I modify the CELERY_TIMEZONE to Asia/Manila and disabled the CELERY_ENABLE_UTC to False.

1
2
CELERY_TIMEZONE = 'Asia/Manila'
CELERY_ENABLE_UTC = False

These settings apply to the Celery beat only, don’t be confused with the Django default TIME_ZONE = ‘UTC’, I recommend you to leave it as usual, unless your Django project is only used within your current timezone and you can change this accordingly.

The only reason I retain the default Django TIME_ZONE = ‘UTC’ is that, it’s easy to convert the UTC to the current user’s timezone, meaning, all the records saved into the database tables will be all using the UTC timing especially if you have a global user’s that would use your Django powered site.

1
2
3
4
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True

Hence, the settings.py default should look like the above configurations.

The important warnings about changing the time zone are that, you need to make it as a NULL value for the “last_run_at” column under the “django_celery_beat_periodictask” table and the “total_run_count” to “0” value as well. You can do that inside the MySQL UPDATE command itself or using the Python shell command.

Back to the Django Admin Site, for this testing purpose, I would like to test for immediate results if the Crontabs option. By the way, Crontabs have more options to choose for your scheduling, whereas the Interval has fewer options, meaning, you can only schedule based on the DAYS, HOURS, MINUTES, SECONDS, and MICROSECONDS.

Now, I’m adding a new Crontabs master schedule, I will try every minute the task will be executed, so that, we can see the immediate results from the Flower real-time monitoring itself. The setting for every minute using the Crontabs below.

It should be all (*) only, you can choose the Timezone as well and then save it. Next, go to the Periodic tasks for us to create our first periodic task to execute, by the way, there are few default settings from there as you have noticed it, don’t delete those, if you try to delete, it will auto-insert it again, it’s a default setting for Django Celery Beat configurations.

Afterward, create a new Periodic task and select the “myroot.views.tasks.del_contact_us” to schedule running every minute as an example.

Type in useful descriptions, you can only choose between the Interval or Crontabs options. Also, the Start_time, try running today’s date and time, as you may notice, it uses the UTC, but, don’t bother about it, the Django Admin Site itself uses the TIME_ZONE from the settings.py. In this example, it will run every minute continuously.

Check out your Flower real-time monitoring, and you will see that it will be processed every minute. Later, you can just change it to every day or every week, just check out the Crontabs scheduling lists.

As you can see, the Started column, it runs every minute, and it’s working. You can create schedules as many as you required for your Django powered projects.

In the next event loop on {{ PLP }}.

Congratulations!, the Django Celery is the reality in today’s modern Django powered projects, it has more batteries to power on the Asynchronous distributed tasks queue to make the Django site a very responsive and much faster to bring the results, and increase the user’s experience for our site.

For those who’re not able to successfully launch your Django Celery with Real-time Monitoring Tasks Using Flower tutorial that I’ve laid it before you, or you need more clarifications, don’t worry, leave a comment below and I’m happy to help you to succeed.

That’s all, have fun learning with {{ PLP }}.

To help Filipino students to learn Python programming language with Django to enhance their capabilities in developing robust web-based applications with practical and direct to the point tutorials, step-by-step with actual information that I provided for you. Leave a comment below or email me at [email protected], thank you!