Django and Ajax: Building a Django live recording application
Tags: webdev, django, python, javascript
Published: 2021-07-16 22:46:19

## Motivation

Recently, I was working on the Q and A section of a web application. And the requirements mandated that users should be provided the option of recording questions live in English or any other suppported languages. Not only that, customer support center should have same privilege of responding with live recorded answers. While scurring the web for some solutions, I came across [Recording audio in Django model](https://stackoverflow.com/questions/52716657/recording-audio-in-django-model) but the response is somehow outdated. I decided to re-implement a working example using the technologies he suggested.

## Technologies

- [Django](https://www.djangoproject.com/)
- [Videojs-record](https://collab-project.github.io/videojs-record/#/)
- [Ajax](https://www.w3schools.com/js/js_ajax_intro.asp)
- [HTML](https://www.w3schools.com/html/default.asp)
- [Bulma CSS](https://bulma.io/documentation/overview/start/)

## Assumptions/Prerequisites

First off, it is assummed you are pretty much familiar with [Django](https://www.djangoproject.com/). Since we'll be using a lot of [Ajax](https://www.w3schools.com/js/js_ajax_intro.asp) and JavaScript, you should have a working knowledge of JavaScript. [Bulma CSS](https://bulma.io/documentation/overview/start/) will be used for the presentation, though not required, familiarity with the framework is great.

## Source code

The complete code for this article is on github and can be accessed via:

{% github Sirneij/django-ajax-record %}

As usual, it is currently live at [django-record.herokuapp.com](https://django-record.herokuapp.com/) (there is a storage bug 🐛 for now)

## Step 1 - Setup the project

Launch your terminal, create a directory to house the project, activate virtual environment and install django.

```bash
┌──(sirneij@sirneij)-[~/Documents/Projects/Django]
└─$[sirneij@sirneij Django]$ mkdir django_record && cd django_record


┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
└─$[sirneij@sirneij django_record]$ virtualenv -p python3.8 env

┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
└─$[sirneij@sirneij django_record]$ source env/bin/activate


(env) ┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
└─$[sirneij@sirneij django_record]$ pip install django
```

## Step 2 — Starting a Django Project

Having installed django, start a new project and then an application.

```bash
(env) ┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
└─$[sirneij@sirneij django_record]$ django-admin startproject record .


(env) ┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
└─$[sirneij@sirneij django_record]$ django-admin startapp core
```

## Step 3 - Add application to your project

Open up the created project in the text editor or IDE of choice (I stick with Visual Studio Code) and navigate to your project's `settings.py` file. In the file, locate `INSTALLED_APPS` and append the created application to it, like so:

```python
# record > settings.py
...

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    #Add the created app
    "core.apps.CoreConfig",
]

...
```

Create a `urls.py` in the `core` app folder and paste the following in:

```python
# core > urls.py
from django.urls import path

app_name = "core"

urlpatterns = []

```

Navigate to your project's `urls.py` file and make it look like this:

```python
# record > urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path
from django.urls.conf import include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("core.urls", namespace="core")), # this adds a namespace to our core app using its urls.py file
]

if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```

These lines:

```python
...
if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```

instruct django to serve these files(static and media) when `DEBUG=True` (i.e during development)

## Step 4 - Configure templates, static and media directories

Since we'll be using a lot of templates, static and media files, configure the directories django should look at for them. Don't forget to create these folders at the root of your project.

```python
...

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"], #Add template directory her
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

...

STATIC_URL = "/static/"
STATICFILES_DIRS = (BASE_DIR / "static",)
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_FINDERS = [
    "django.contrib.staticfiles.finders.FileSystemFinder",
    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
]


MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

...
```

Create the `templates`, `static` and `media` directories.

```bash
(env) ┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
└─$[sirneij@sirneij django_record]$ mkdir -p templates static media
```

## Step 5 — Add the index view

To test our setup so far, navigate to your app's `views.py` and append the following:

```python
# core > views.py
...

def index(request):
    context = {
        "page_title": "Voice records",
    }
    return render(request, "core/index.html", context)
```

It is a simple `Function Based View(FBV)` that renders a simple yet-to-be-created template `index.html` which is found in the `core` directory of the `templates` directory. Before creating this directory and `html` file, let's link it up to the `urls.py` file.

```python
# core > urls.py

from django.urls import path

from . import views

app_name = "core"

urlpatterns = [
    path("", views.index, name="index"),
]


```

Now, create the `core` subdirectory in the `templates` folder and append `index.html` to it. But before then, let's work on the layout file for the entire application. I name it `_base.html`.

```bash
(env) ┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
└─$[sirneij@sirneij django_record]$ touch templates/_base.html


(env) ┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
└─$[sirneij@sirneij django_record]$ mkdir templates/core && touch templates/core/index.html
```

Open up this files and make them appear like the following:

```jinja
<!--templates > _base.html-->
{% load static %}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Django Ajax - {% block title %}{% endblock title %}</title>
    <link rel="stylesheet" href="{% static 'assets/css/bulma.min.css' %}" />
  </head>
  <body>
    {% block content %} {% endblock content %}
  </body>
</html>
```

This `_base.html` was copied from Bulma CSS [Starter template](https://bulma.io/documentation/overview/start/#starter-template) and some modifications were made. Notice that I am not using Bulma CSS [CDN](https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css"). I prefer serving my `static` files locally to reduce network calls.

Now to `index.html`:

```jinja
<!--templates > core > index.html -->

<!--inherits the layout-->
{% extends '_base.html' %}
<!--passes the page title-->
{% block title %}{{page_title}}{% endblock title %}
<!--content starts-->
{% block content %}
<section class="section">
  <div class="container">
    <h1 class="title">Hello World</h1>
    <p class="subtitle">My first website with <strong>Bulma</strong>!</p>
  </div>
</section>
{% endblock content %}

```

The comments say it all.

It's time to test it out! Open your terminal and `runserver`!

```bash
(env) ┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
└─$[sirneij@sirneij django_record]$ python manage.py runserver

Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
July 16, 2021 - 19:09:00
Django version 3.2.5, using settings 'record.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```

Neglect the warnings for now. Open up your browser and visit `http://127.0.0.1:8000/`.

![Welcome page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ivflkbj1ge3zza4trku6.png)

From now on, I won't talk much about `HTML` and `CSS`.

## Step 6 — Create a model and view logic

Now to the first half of the real deal. Let's create a simple model to hold the recorded audios and add a view logic for exposing a `POST` `API` for recording so that `Ajax` can consume it later on.

```python
# core > models.py

import uuid

from django.db import models
from django.urls.base import reverse


class Record(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    voice_record = models.FileField(upload_to="records")
    language = models.CharField(max_length=50, null=True, blank=True)

    class Meta:
        verbose_name = "Record"
        verbose_name_plural = "Records"

    def __str__(self):
        return str(self.id)

    def get_absolute_url(self):
        return reverse("record_detail", kwargs={"id": str(self.id)})


```

The model is just a normal one. I am always fund of overriding the default `BigAutoField` django gives `id`. I prefer a `UUID` field. Aside that, the table has only two fields: `voice_records` and `language` which is optional. Our recordings will be stored in the `records` subdirectory of the `media` directory.

Make your `views.py` file appear as follows:

```python
# core > views.py

from django.contrib import messages
from django.http.response import JsonResponse
from django.shortcuts import get_object_or_404, render

from .models import Record


def record(request):
    if request.method == "POST":
        audio_file = request.FILES.get("recorded_audio")
        language = request.POST.get("language")
        record = Record.objects.create(language=language, voice_record=audio_file)
        record.save()
        messages.success(request, "Audio recording successfully added!")
        return JsonResponse(
            {
                "success": True,
            }
        )
    context = {"page_title": "Record audio"}
    return render(request, "core/record.html", context)


def record_detail(request, id):
    record = get_object_or_404(Record, id=id)
    context = {
        "page_title": "Recorded audio detail",
        "record": record,
    }
    return render(request, "core/record_detail.html", context)


def index(request):
    records = Record.objects.all()
    context = {"page_title": "Voice records", "records": records}
    return render(request, "core/index.html", context)


```

The `record` function exposes the creation of the recording and stores it thereafter. For the detail view, `record_detail` handles getting only a single recording and our `index` lists all available recordings in the database.

Let's reflect all these changes in our app's `urls.py` file.

```python
# core > urls.py

from django.urls import path

from . import views

app_name = "core"

urlpatterns = [
    path("", views.index, name="index"),
    path("record/", views.record, name="record"),
    path("record/detail/<uuid:id>/", views.record_detail, name="record_detail"),
]


```

It is time to really create the database so that the table can exist. To do this, simply run `migrations` in your terminal.

```bash
(env) ┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
└─$[sirneij@sirneij django_record]$ python manage.py makemigrations


(env) ┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
└─$[sirneij@sirneij django_record]$ python manage.py migrate
```

You should be greeted with something that looks like:

```bash
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, core, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying core.0001_initial... OK
  Applying sessions.0001_initial... OK
```

## Step 7 - Introducing `videojs-record` and `ajax`

It's time to really record something. To do this, we need a bunch of `.js` files and a couple of `.css`. `jQuery` will be needed too for `ajax`. In the complete version of the project, all these files are included but below is some exerpt:

```jinja
<!-- templates > _base.html -->

{% load static %}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Django Ajax - {% block title %}{% endblock title %}</title>
    <link rel="stylesheet" href="{% static 'assets/css/bulma.min.css' %}" />
    {% block css %}{% endblock css %}
  </head>
  <body>
    <!--header-->
    {% include 'includes/_header.html' %}
    <!--content-->
    {% block content %} {% endblock content %}
    <!-- js-->
    <script src="{% static 'assets/js/jquery.min.js' %}"></script>
    <script>
      const triggerModal = document.getElementById("triggerModal");
      triggerModal.style.display = "none";
      const csrftoken = $("[name=csrfmiddlewaretoken]").val();
      if (csrftoken) {
        function csrfSafeMethod(method) {
          // these HTTP methods do not require CSRF protection
          return /^(GET|HEAD|OPTIONS|TRACE)$/.test(method);
        }
        $.ajaxSetup({
          beforeSend: function (xhr, settings) {
            if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
              xhr.setRequestHeader("X-CSRFToken", csrftoken);
            }
          },
        });
      }
    </script>
    {% block js %}{% endblock js %}
  </body>
</html>

```

This portion:

```javascript
...
      const csrftoken = $("[name=csrfmiddlewaretoken]").val();
      if (csrftoken) {
        function csrfSafeMethod(method) {
          // these HTTP methods do not require CSRF protection
          return /^(GET|HEAD|OPTIONS|TRACE)$/.test(method);
        }
        $.ajaxSetup({
          beforeSend: function (xhr, settings) {
            if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
              xhr.setRequestHeader("X-CSRFToken", csrftoken);
            }
          },
        });
      }
...
```

helps get the `csrf tokens` from the form we'll be processing later without explicitly including its value in all `ajax` `POST` calls. This is pretty handy in applications with many forms which will be processed with `ajax`.

Now to `templates/core/record.html`.

```jinja
<!-- templates > core > record.html -->

<!--inherits the layout-->
{% extends '_base.html' %}
<!--static-->
{% load static %}
<!--title-->
{% block title %}{{page_title}}{% endblock title %}

<!--additional css-->

{% block css %}
<link href="{% static 'assets/css/video-js.css' %}" rel="stylesheet" />
<link href="{% static 'assets/css/all.min.css' %}" rel="stylesheet" />
<link
  href="{% static 'assets/css/videojs.wavesurfer.min.css' %}"
  rel="stylesheet"
/>
<link href="{% static 'assets/css/videojs.record.css' %}" rel="stylesheet" />
<style>
  /* change player background color */
  #createQuestion {
    background-color: #198754;
  }
</style>
{% endblock css %}
<!--content-->
{% block content %}
<section class="section">
  <div class="container">
    <div class="columns">
      <div class="column is-offset-4 is-4">
        <h1 class="title">Record audio</h1>
        <article class="message is-success" id="alert">
          <div class="message-header">
            <p>Recorded successfully!</p>
            <button class="delete" aria-label="delete"></button>
          </div>
          <div class="message-body">
            You have successfully recorded your message. You can now click on
            the Submit button to post it.
          </div>
        </article>
        <form method="POST" enctype="multipart/form-data">
          {% csrf_token %}
          <div class="field">
            <div class="control has-icons-left has-icons-right">
              <input class="input" type="text" placeholder="Language" />
              <span class="icon is-left">
                <i class="fas fa-language"></i>
              </span>
              <span class="icon is-right">
                <i class="fas fa-check"></i>
              </span>
            </div>
            <div class="control has-icons-left has-icons-right">
              <audio id="recordAudio" class="video-js vjs-default-skin"></audio>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
</section>
{% endblock content %}

<!--additional js-->
{% block js %}
<script src="{% static 'assets/js/video.min.js' %}"></script>
<script src="{% static 'assets/js/RecordRTC.js' %}"></script>
<script src="{% static 'assets/js/adapter-latest.js' %}"></script>
<script src="{% static 'assets/js/wavesurfer.js' %}"></script>
<script src="{% static 'assets/js/wavesurfer.microphone.min.js' %}"></script>
<script src="{% static 'assets/js/videojs.wavesurfer.min.js' %}"></script>

<script src="{% static 'assets/js/videojs.record.min.js' %}"></script>
<script src="{% static 'assets/js/browser-workaround.js' %}"></script>

{% endblock js %}


```

All these additional files were included in the [official audio-only example](https://github.com/collab-project/videojs-record/blob/5667e5711f7c2ef3f6f5f6b0f216c3d75dfd1186/examples/audio-only.html#L7) of `videojs-record` library. Visiting `http://localhost:8000/record/` should look like:

![Second image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lfm7rbqwqvzu3uv5rnv6.png)

## Step 8 - Adding recording and `ajax` calls

To have the real fealing of recording, let's do the real thing - recording!

Create a new `.js` file in the `js` subdirectory of your `static` files directory. I call it `real.recording.js`. Populate it with the following:

```javascript
// First lets hide the message
document.getElementById("alert").style.display = "none";
// Next, declare the options that will passed into the recording constructor
const options = {
  controls: true,
  bigPlayButton: false,
  width: 600,
  height: 300,
  fluid: true, // this ensures that it's responsive
  plugins: {
    wavesurfer: {
      backend: "WebAudio",
      waveColor: "#f7fff7", // change the wave color here. Background color was set in the css above
      progressColor: "#ffe66d",
      displayMilliseconds: true,
      debug: true,
      cursorWidth: 1,
      hideScrollbar: true,
      plugins: [
        // enable microphone plugin
        WaveSurfer.microphone.create({
          bufferSize: 4096,
          numberOfInputChannels: 1,
          numberOfOutputChannels: 1,
          constraints: {
            video: false,
            audio: true,
          },
        }),
      ],
    },
    record: {
      audio: true, // only audio is turned on
      video: false, // you can turn this on as well if you prefer video recording.
      maxLength: 60, // how long do you want the recording?
      displayMilliseconds: true,
      debug: true,
    },
  },
};

// apply audio workarounds for certain browsers
applyAudioWorkaround();

// create player and pass the the audio id we created then
var player = videojs("recordAudio", options, function () {
  // print version information at startup
  var msg =
    "Using video.js " +
    videojs.VERSION +
    " with videojs-record " +
    videojs.getPluginVersion("record") +
    ", videojs-wavesurfer " +
    videojs.getPluginVersion("wavesurfer") +
    ", wavesurfer.js " +
    WaveSurfer.VERSION +
    " and recordrtc " +
    RecordRTC.version;
  videojs.log(msg);
});

// error handling
player.on("deviceError", function () {
  console.log("device error:", player.deviceErrorCode);
});

player.on("error", function (element, error) {
  console.error(error);
});

// user clicked the record button and started recording
player.on("startRecord", function () {
  console.log("started recording!");
});

// user completed recording and stream is available
player.on("finishRecord", function () {
  const audioFile = player.recordedData;

  console.log("finished recording: ", audioFile);

  $("#submit").prop("disabled", false);
  document.getElementById("alert").style.display = "block";
});
```

Your `templates/core/record.html` should now look like:

```jinja
<!--inherits the layout-->
{% extends '_base.html' %}
<!--static-->
{% load static %}
<!--title-->
{% block title %}{{page_title}}{% endblock title %}

<!--additional css-->

{% block css %}
<link href="{% static 'assets/css/video-js.css' %}" rel="stylesheet" />
<link href="{% static 'assets/css/all.min.css' %}" rel="stylesheet" />
<link
  href="{% static 'assets/css/videojs.wavesurfer.min.css' %}"
  rel="stylesheet"
/>
<link href="{% static 'assets/css/videojs.record.css' %}" rel="stylesheet" />
<style>
  /* change player background color */
  #recordAudio {
    background-color: #3e8ed0;
  }
</style>
{% endblock css %}
<!--content-->
{% block content %}
<section class="section">
  <div class="container">
    <div class="columns">
      <div class="column is-offset-4 is-4">
        <h1 class="title">Record audio</h1>
        <article class="message is-success" id="alert">
          <div class="message-header">
            <p>Recorded successfully!</p>
            <button class="delete" aria-label="delete"></button>
          </div>
          <div class="message-body">
            You have successfully recorded your message. You can now click on
            the Submit button to post it.
          </div>
        </article>
        <form method="POST" enctype="multipart/form-data">
          {% csrf_token %}
          <div class="field">
            <div class="control has-icons-left has-icons-right">
              <input class="input" type="text" placeholder="Language" />
              <span class="icon is-left">
                <i class="fas fa-language"></i>
              </span>
              <span class="icon is-right">
                <i class="fas fa-check"></i>
              </span>
            </div>
            <div
              class="control has-icons-left has-icons-right"
              style="margin-top: 1rem"
            >
              <audio id="recordAudio" class="video-js vjs-default-skin"></audio>
            </div>
            <div class="control" style="margin-top: 1rem">
              <button class="button is-info" id="submit">Submit</button>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
</section>
{% endblock content %}

<!--additional js-->
{% block js %}
<script src="{% static 'assets/js/video.min.js' %}"></script>
<script src="{% static 'assets/js/RecordRTC.js' %}"></script>
<script src="{% static 'assets/js/adapter-latest.js' %}"></script>
<script src="{% static 'assets/js/wavesurfer.js' %}"></script>
<script src="{% static 'assets/js/wavesurfer.microphone.min.js' %}"></script>
<script src="{% static 'assets/js/videojs.wavesurfer.min.js' %}"></script>

<script src="{% static 'assets/js/videojs.record.min.js' %}"></script>
<script src="{% static 'assets/js/browser-workaround.js' %}"></script>
<script src="{% static 'assets/js/real.recording.js' %}"></script>
{% endblock js %}

```

![Last image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i3s29dhjjv8ku4urtwdx.png)

Ajax proper:

```javascript
...

// Give event listener to the submit button
$("#submit").on("click", function (event) {
  event.preventDefault();
  let btn = $(this);
  //   change the button text and disable it
  btn.html("Submitting...").prop("disabled", true).addClass("disable-btn");
  //   create a new File with the recordedData and its name
  const recordedFile = new File([player.recordedData], `audiorecord.webm`);
  //   grabs the value of the language field
  const language = document.getElementById("language").value;
  //   initializes an empty FormData
  let data = new FormData();
  //   appends the recorded file and language value
  data.append("recorded_audio", recordedFile);
  data.append("language", language);
  //   post url endpoint
  const url = "";
  $.ajax({
    url: url,
    method: "POST",
    data: data,
    dataType: "json",
    success: function (response) {
      if (response.success) {
        document.getElementById("alert").style.display = "block";
        window.location.href = "/";
      } else {
        btn.html("Error").prop("disabled", false);
      }
    },
    error: function (error) {
      console.error(error);
    },
    cache: false,
    processData: false,
    contentType: false,
  });
});

```

## Small update

The `ajax`code might fail or give undesirable output in Firefox browsers if the `event` argument isn't passed in the callback function, followed by the first line `event.preventDefault();`.

That's it! Such a long piece. Do you have some suggestions? Kindly drop them in the comment section.