Django Tutorial

Making Django Forms Dynamic with JavaScript Fetch

The setup

Jump to:    top    setup    ajax    database    finishing    comments   

     First let's get oriented by looking at the models. We have four models. The player, the categories, the questions in each category, and the answers for each question. Please note that, for brevity, I am not showing all the code here. If you want the full code, just grab it here.
# models.py

from django.conf import settings
from django.db import models

class Player(models.Model):
    player = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    score = models.PositiveSmallIntegerField(default=0)

    def __str__(self):
        return self.player.first_name + " " + self.player.last_name


class Category(models.Model):
    category = models.CharField(max_length=20)

    def __str__(self):
        return self.category


class Question(models.Model):
    question = models.CharField(max_length=200)
    value = models.PositiveSmallIntegerField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    correct_answer = models.OneToOneField('Answer', on_delete=models.CASCADE, related_name='correct_answer', null=True)

    def __str__(self):
        return self.category.category + ": " + self.question


class Answer(models.Model):
    answer = models.CharField(max_length=200)
    question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='answer')

    def __str__(self):
        return self.question.question + ": " + self.answer

     I've also created a form. I won't actually be using the form in the template, I just want to use it to make it easy to validate the form once it is submitted.
# forms.py

from django import forms

class QuizForm(forms.Form):
    category_pk = forms.IntegerField(min_value=0)
    question_pk = forms.IntegerField(min_value=0)
    answer_pk = forms.IntegerField(min_value=0)

     Let's begin with the view. Right now, all it does is get us to the page. Let's initially send it the player, as well as all the categories, all the questions and all the answers. Later, you will see that we don't need to send all the questions and answers, in fact we won't be sending any questions or answers via this view, since that will be handled by JavaScript's fetch. But let's leave it for now.
# views.py

from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required

from .models import Player, Category, Question, Answer
from .forms import QuizForm


@login_required
def quiz(request):

    player, created = Player.objects.get_or_create(player=request.user)

    categories = Category.objects.all()
    questions = Question.objects.all()
    answers = Answer.objects.all()

    context = {
        'player': player,
        'categories': categories,
        'questions': questions,
        'answers': answers,
    }

    return render(request, "quiz/quiz.html", context)

     The template initially only has a basic skeleton, as well as a note welcoming the user. Let's add the initial code to set up the form:
{% extends 'quiz/base.html' %}
{% load crispy_forms_tags %}
{% load static %}

{% block title %}Quiz{% endblock %}

{% block styles %}
    <link rel="stylesheet" href="{% static 'quiz/css/quiz.css' %}">
{% endblock styles %}

{% block content %}

    <h1>Quiz</h1>

    {% if user.is_authenticated %}
        <div style="text-align: right;"><a href="{% url 'logout' %}">Logout</a></div>
    {% endif %}
    <br>

    <p>Ready to take a quiz, {{ user.first_name }}.  To begin, select a category.</p>

    <form action="#" method="post">
        {% csrf_token %}

        <select class="form-select" name="category_pk">
            <option selected disabled>Category</option>
            {% for category in categories %}
                <option value="{{ category.pk }}">{{ category }}</option>
            {% endfor %}
        </select>

        <br>

        <select class="form-select" name="question_pk">
            <option selected disabled value="0">Question</option>
            {% for question in questions %}
                <option value="{{ question.pk }}">{{ question }}</option>
            {% endfor %}
        </select>

        <br>

        <select class="form-select" name="answer_pk">
            <option selected disabled value="0">Answer</option>
            {% for answer in answers %}
                <option value="{{ answer.pk }}">{{ answer }}</option>
            {% endfor %}
        </select>

        <br>

        <p>Score: <strong>{{ player.score }}</strong> </p>

        <input class="btn btn-primary" type="submit" value="submit">

    </form>


{% endblock content %}

{% block javascript %}
    <script src="{% static 'quiz/js/quiz.js' %}"></script>
{% endblock javascript %}

     Okay, now run it, login using username: admin, password: django-admin (or just create a new user by clicking on 'Signup'), and you see a big problem. I've seeded the database with 4 categories, each with 4 questions, and each question with 4 answer choices. That's 64 answers, and the user can select any of them, without even selecting a category first! Of course, we could have the form just send the category and then have our view filter out the questions and send it back to the client, but that means a page reload, which is what we are trying to avoid.



Introducting AJAX

Jump to:    top    setup    ajax    database    finishing    comments   

     Before we get into the JavaScript, let's add to our views functions that will respond to requests sent by the client through the fetch API. All we need to do is filter the questions based on the category that the user will choose, or filter the answers to the question chosen by the user. This is easy:
# views.py

from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required

from .models import Player, Category, Question, Answer
from .forms import QuizForm

import json


@login_required
def get_questions(request, category_pk):
    questions = Question.objects.filter(category=category_pk)
    return JsonResponse(list(questions.values()), safe=False)

@login_required
def get_answers(request, question_pk):
    answers = Answer.objects.filter(question=question_pk)
    return JsonResponse(list(answers.values()), safe=False)

@login_required
def quiz(request):

    player, created = Player.objects.get_or_create(player=request.user)

    categories = Category.objects.all()
    questions = Question.objects.all()
    answers = Answer.objects.all()

    context = {
        'player': player,
        'categories': categories,
        'questions': questions,
        'answers': answers,
    }

    return render(request, "quiz/quiz.html", context)

     Notice that instead of returning a template, we are returning a JsonResponse. This is what the JavaScript code on the client side will receive. First we turn the QuerySet into one that returns dictionaries instead of model instances with values(), then turn thie queryset into a list of these dictionaries since JavaScript doesn't know what to do with a queryset. Since we are sending a list we need to set safe to false. It's okay to set safe to false as stated in the Django docs, but if you are worried, you could wrap the list in a dictionary like this (but then you will also have to modify the JavaScript to extract the list from this wrapper dictionary):
return JsonResponse({'filtered-answers': list(answers.values())})

     So now the server is ready to respond, but just like with regular views that return templates, we need to add paths to these views as well in our urls.py:
# urls.py

from django.urls import path

from . import views

app_name = 'quiz'

urlpatterns = [
    path('', views.quiz, name='quiz'),
    path('get_questions/<int:category_pk>', views.get_questions, name='get_questions'),
    path('get_answers/<int:question_pk>', views.get_answers, name='get_answers'),
]

     Now we have a way to respond from the server, but now we need to send the server the request using JavaScript fetch. First we need to modify the html template:
{% extends 'quiz/base.html' %}
{% load crispy_forms_tags %}
{% load static %}

{% block title %}Quiz{% endblock %}

{% block styles %}
    <link rel="stylesheet" href="{% static 'quiz/css/quiz.css' %}">
{% endblock styles %}

{% block content %}

    <h1>Quiz</h1>

    {% if user.is_authenticated %}
        <div style="text-align: right;"><a href="{% url 'logout' %}">Logout</a></div>
    {% endif %}
    <br>

    <p>Ready to take a quiz, {{ user.first_name }}.  To begin, select a category.</p>

    <form action="#" method="post">
        {% csrf_token %}

        <select class="form-select" name="category_pk" id="category">
            <option selected disabled>Category</option>
            {% for category in categories %}
                <option value="{{ category.pk }}">{{ category }}</option>
            {% endfor %}
        </select>

        <br>

        <select class="form-select" name="question_pk" id="question">
            <option selected disabled value="0">Question</option>
        </select>

        <br>

        <select class="form-select" name="answer_pk" id="answer">
            <option selected disabled value="0">Answer</option>
        </select>

        <br>

        <p>Score: <strong>{{ player.score }}</strong> </p>

        <input class="btn btn-primary" type="submit" value="submit" id="submit">

    </form>


{% endblock content %}

{% block javascript %}
    <script src="{% static 'quiz/js/quiz.js' %}"></script>
{% endblock javascript %}

     Notice that we removed the for loops in the select for the questions and for the answers since we will be fetching those with JavaScript. We also added an id attribute to the each of the select tags as well as the submit button. This is how we can target them in our JavaScript. Note that we no longer need to send all the questions and answers from the initial view anymore:
# views.py

from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required

from .models import Player, Category, Question, Answer
from .forms import QuizForm

import json


@login_required
def get_questions(request, category_pk):
    questions = Question.objects.filter(category=category_pk)
    return JsonResponse(list(questions.values()), safe=False)

@login_required
def get_answers(request, question_pk):
    answers = Answer.objects.filter(question=question_pk)
    return JsonResponse(list(answers.values()), safe=False)

@login_required
def quiz(request):

    player, created = Player.objects.get_or_create(player=request.user)

    categories = Category.objects.all()

    context = {
        'player': player,
        'categories': categories,
    }

    return render(request, "quiz/quiz.html", context)

     Of course, now if you run it, no questions or answers will show up. Now we finally come to the JavaScript to actually fetch them from the server using AJAX, and update the forms. You will find the javascript file in the path "/quiz/static/quiz/js/quiz.js".
// quiz.js

document.addEventListener('DOMContentLoaded', () => {

    // Target the select tags in the DOM (the html template)
    let category = document.getElementById('category');
    let question = document.getElementById('question');
    let answer = document.getElementById('answer');
    let submit = document.getElementById('submit');

    // Hide the questions and answers until the user selects a category
    question.style.display = 'none';
    answer.style.display = 'none';

    // Disable (you could also hide) the button until the form is completed
    submit.disabled = true;


    ////////////////////////////////////////////////////////////////////////
    ///////////////////////// GET QUESTIONS ////////////////////////////////
    ////////////////////////////////////////////////////////////////////////

    async function get_questions(e) {

        answer.style.display = 'none';
        answer.value = 0;
        question.value = 0;
        submit.disabled = true;

        fetch(`/get_questions/${e.target.value}`)
        .then(response => {
            if (!response.ok) {
                throw new Error(`HTTP error: ${response.status}`);
            }
            return response.json();
        })
        .then(data => {
            console.log(`Success: ${data}`);
            for (i = 0; i < data.length; i++) {
                question.options[i+1] = new Option(data[i].question, data[i].id);
            }
            question.style.display = 'block';
        })
        .catch((error) => {
            console.error(`Error: ${error}`);
        });
    }

    category.addEventListener('input', get_questions);


    ////////////////////////////////////////////////////////////////////////
    ///////////////////////// GET ANSWERS //////////////////////////////////
    ////////////////////////////////////////////////////////////////////////

    async function get_answers(e) {

        answer.value = 0;
        submit.disabled = true;

        fetch(`/get_answers/${e.target.value}`)
        .then(response => {
            if (!response.ok) {
                throw new Error(`HTTP error: ${response.status}`);
            }
            return response.json();
        })
        .then(data => {
            console.log(`Success: ${data}`);
            for (i = 0; i < data.length; i++) {
                answer.options[i+1] = new Option(data[i].answer, data[i].id);
            }
            answer.style.display = 'block';
        })
        .catch((error) => {
            console.error(`Error: ${error}`);
        });
    }

    question.addEventListener('input', get_answers);


    ////////////////////////////////////////////////////////////////////////
    /////////////////////// Enable/Disable SUBMIT //////////////////////////
    ////////////////////////////////////////////////////////////////////////

    answer.addEventListener('input', () => {
        submit.disabled = false;
    });

});

     The fetch() function is what sends the request, and all you need to supply is the url of the path. Fetch can also do much more. It can also send information to the server using a post request, but for this tutorial we will not be using fetch to modify the database, only to get information from it. Never use a get request like we are doing here to modify the database! Now the fetch() function is an asynchronous function, which means that while it fetching the data, the rest of the page works, and the rest of the code continues to be executed line by line. What fetch() returns is called a promise. In other words, fetch() is promising that it will get back to you with a response (or error, if it couldn't fetch the data). The promise will be resolved into a response, which includes a lot of information, including whether the connection was succesful, and that there was a response from the server (Django view). We can check that everything went okay with ok property of the response.

     If the response is indeed ok, then we can actually start reading in the data, and that is why we have a then() method attached to the fetch(). The then() method waits until the response is received before acting, which is why it is called then()! In thie first then() what is done is the actual data is read, and that is what the json() method will do. This method is also asynchronous, and so it has it's own then() method attached to it. This is when the data is actually read.

     The data is that list we sent from the server! Now we can loop through the array and add an Option to the select. If something goes wrong the catch() method will be activated instead of the then(). Also note that we have addEventListener() methods with 'input' as the event. This input event will fire when the user selects an option, and then the event listener will fire the function. So for example, when the user selects a category, the event listener attached to category will fire the get_questions function. The e.target.value will be the value of the selected option, which is the primary key of the category, which we need so that we can get the questions for that particular category. Note that we add the question's primary key as well as the question itself when we create the Option:
question.options[i+1] = new Option(data[i].question, data[i].id);

     If you run the code now, it shoud work. NOTE: Your browser stores javascript in a cache when you reload a page, so you will need to do a Hard Refresh. To do this on most Windows or Linux browsers, like FireFox and Chrome you hold down Ctrl and press F5. For other browsers, computers, and further instructions, check out https://en.wikipedia.org/wiki/Wikipedia:Bypass_your_cache. While the code should work, you will notice two problems. First, when the form is submitted, the page reloads, but no information is sent to our inital quiz view. No score is kept. Second, we want to make it so that a user can answer a question only once.



Incorporating the Database

Jump to:    top    setup    ajax    database    finishing    comments   

     We need to do two things now. First, add fields to our model to record which questions the user has already answered, and also record which categories the user has completely done (in other words, categories in which the user has already answered all the questions of that category). These are things we do not want to allow a user to select again. Thus we will add the fields questions_answered and categories_done to handle these in our models:
# models.py

from django.conf import settings
from django.db import models

class Player(models.Model):
    player = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    questions_answered = models.ManyToManyField('Question', blank=True)
    categories_done = models.ManyToManyField('Category', blank = True)
    score = models.PositiveSmallIntegerField(default=0)

    def __str__(self):
        return self.player.first_name + " " + self.player.last_name


class Category(models.Model):
    category = models.CharField(max_length=20)

    def __str__(self):
        return self.category


class Question(models.Model):
    question = models.CharField(max_length=200)
    value = models.PositiveSmallIntegerField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    correct_answer = models.OneToOneField('Answer', on_delete=models.CASCADE, related_name='correct_answer', null=True)

    def __str__(self):
        return self.category.category + ": " + self.question


class Answer(models.Model):
    answer = models.CharField(max_length=200)
    question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='answer')

    def __str__(self):
        return self.question.question + ": " + self.answer

     We have modified the models so we need to quit the server (Ctrl-c), and run migrations:
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
     Now we can modify our view to update the player's score if the player has answered correctly, update which question the player has answered, and finally check if all th questions in that category have now been answered by the player.
# views.py

from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required

from .models import Player, Category, Question, Answer
from .forms import QuizForm

import json


@login_required
def get_questions(request, category_pk):
    questions = Question.objects.filter(category=category_pk)
    return JsonResponse(list(questions.values()), safe=False)


@login_required
def get_answers(request, question_pk):
    answers = Answer.objects.filter(question=question_pk)
    return JsonResponse(list(answers.values()), safe=False)


@login_required
def quiz(request):

    player, created = Player.objects.get_or_create(player=request.user)
    categories = Category.objects.all()

    # I'm not sending this form to the template because I don't want the entire
    # form to be displayed - I want JavaScript to load parts of the form, but
    # I will be using it to check that the information sent through POST is valid
    form = QuizForm(request.POST or None)

    if form.is_valid():

        # Get the data from the submitted form
        category = get_object_or_404(Category, pk=request.POST.get('category_pk'))
        question = get_object_or_404(Question, pk=request.POST.get('question_pk'))
        answer = get_object_or_404(Answer, pk=request.POST.get('answer_pk'))

        # Increase the score if the player has answered correctly, as long as
        # the player did not previously answer this question
        if answer == question.correct_answer and \
                     question not in player.questions_answered.all():
            player.score = player.score + 1
            player.save()

        # Add the question to the questions the player has answered
        player.questions_answered.add(question)

        # Check to see if ALL questions in this category have now been answered
        # Using a set since I don't care about order, but there should be no
        # duplicates, and python sets do not allow duplicates!
        questions_in_this_category_answered = set(player.questions_answered.filter(category=category))
        all_questions_in_this_category = set(Question.objects.filter(category=category))

        # If the player has answered ALL the questions of this category, then
        # add it to categories_done.  We can easily check this by seeing if
        # the two sets are equal, the set that includes all the questions answered
        # of the particular category and all the questions in the category
        # JavaScript will handle this information -
        # it will disable categories in categories_done
        if questions_in_this_category_answered == all_questions_in_this_category:
            player.categories_done.add(category)

        return redirect('quiz:quiz')


    context = {
        'player': player,
        'categories': categories,
        'categories_done': player.categories_done,
    }

    return render(request, "quiz/quiz.html", context)
     There's a lot that we added in this view. I've tried to explain this in the comments above. If we run the project now, it will indeed capture the score, the questions answered, and the categories done to our database, but that will not prevent the user from answering the same questions over and over again. We now need to get JavaScript to disable questions that have already been answered, and categories whose questions have all been answered by the player.



Finishing Touches

Jump to:    top    setup    ajax    database    finishing    comments   

     First, let's disable the categories for which the player has answered all the questions. We're going to do that without any JavaScript. You may be wondering at this point why we do not use JavaScript to update the categories. We are only using them to update the questions and answers. Good question! The answer is that we could do the same for categories as we do for questions and answers and you probably would if you were creating a game with different levels where new categories would be created in each level, etc... And there's nothing stopping you from doing just that! I just want to keep things simple to explain the concept so that you can take it further, or adjust it to your specific needs. In the example that I am using, when the user clicks the submit button, they sort of expect a page reload, or at least tolerate one. That is not the case when they simply choose a category and want to select a question! Okay, so to actually disable the categories the player has exhausted, it's pretty simple:
<!-- quiz.html -->

{% extends 'quiz/base.html' %}
{% load crispy_forms_tags %}
{% load static %}

{% block title %}Quiz{% endblock %}

{% block styles %}
    <link rel="stylesheet" href="{% static 'quiz/css/quiz.css' %}">
{% endblock styles %}

{% block content %}

    <h1>Quiz</h1>

    {% if user.is_authenticated %}
        <div style="text-align: right;"><a href="{% url 'logout' %}">Logout</a></div>
    {% endif %}
    <br>

    <p>Ready to take a quiz, {{ user.first_name }}.  To begin, select a category.</p>

    <form action="#" method="post">
        {% csrf_token %}

        <select class="form-select" name="category_pk" id="category">
            <option selected disabled>Category</option>
            {% for category in categories %}
                {% if category not in categories_done.all %}
                    <option value="{{ category.pk }}">{{ category }}</option>
                {% else %}
                    <option disabled value="{{ category.pk }}">{{ category }}</option>
                {% endif %}
            {% endfor %}
        </select>

        <br>

        <select class="form-select" name="question_pk" id="question">
            <option selected disabled value="0">Question</option>
        </select>

        <br>

        <select class="form-select" name="answer_pk" id="answer">
            <option selected disabled value="0">Answer</option>
        </select>

        <br>

        <p>Score: <strong>{{ player.score }}</strong> </p>

        <input class="btn btn-primary" type="submit" value="submit" id="submit">

    </form>


{% endblock content %}

{% block javascript %}
    <script src="{% static 'quiz/js/quiz.js' %}"></script>
{% endblock javascript %}
     Now, to disable the questions answered, we have a little bit more work to do because we don't know which category the player is going to select. At this point you might be wondering, why not just send all the questions the player has answered now. That would make things simpler and avoid any JavaScript.

     The answer is, again, you can!. And for such a simple little app like the one I'm showing you how to build, that would be totally fine. But again, I'm using this as an example of the techniques you can use. Imagine, for example that you had thousands of questions in your app, with tens of thousands of answers, and hundreds of players (and that would still be considered a relatively small app in many cases!). Do you really want to send all that information through, slowing everything down, just to extract a bit of information? The point I am trying to make is that by using JavaScript fetch (or AJAX in general), you can send tiny bits of information back and forth to update parts of a web page very quickly. That's the whole foundation of React!

     Hopefully, I've now convinced you about the need for AJAX. Well, we need to get the questions answered for a particular player, on a particular category, so we need to add a view to handle that request. But thaa's easy! It's basically what we've already done with getting the questions and answers in the first place. Check out the get_questions_answered view functin added below:
# views.py

from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required

from .models import Player, Category, Question, Answer
from .forms import QuizForm

import json


@login_required
def get_questions(request, category_pk):
    questions = Question.objects.filter(category=category_pk)
    return JsonResponse(list(questions.values()), safe=False)


@login_required
def get_answers(request, question_pk):
    answers = Answer.objects.filter(question=question_pk)
    return JsonResponse(list(answers.values()), safe=False)


@login_required
def get_questions_answered(request):
    questions_answered = []
    for question in request.user.player.questions_answered.all():
        questions_answered.append(question.id)
    return JsonResponse(questions_answered, safe=False)


@login_required
def quiz(request):

    player, created = Player.objects.get_or_create(player=request.user)
    categories = Category.objects.all()

    # I'm not sending this form to the template because I don't want the entire
    # form to be displayed - I want JavaScript to load parts of the form, but
    # I will be using it to check that the information sent through POST is valid
    form = QuizForm(request.POST or None)

    if form.is_valid():

        # Get the data from the submitted form
        category = get_object_or_404(Category, pk=request.POST.get('category_pk'))
        question = get_object_or_404(Question, pk=request.POST.get('question_pk'))
        answer = get_object_or_404(Answer, pk=request.POST.get('answer_pk'))

        # Increase the score if the player has answered correctly, as long as
        # the player did not previously answer this question
        if answer == question.correct_answer and \
                     question not in player.questions_answered.all():
            player.score = player.score + 1
            player.save()

        # Add the question to the questions the player has answered
        player.questions_answered.add(question)

        # Check to see if ALL questions in this category have now been answered
        # Using a set since I don't care about order, but there should be no
        # duplicates, and python sets do not allow duplicates!
        questions_in_this_category_answered = set(player.questions_answered.filter(category=category))
        all_questions_in_this_category = set(Question.objects.filter(category=category))

        # If the player has answered ALL the questions of this category, then
        # add it to categories_done.  We can easily check this by seeing if
        # the two sets are equal, the set that includes all the questions answered
        # of the particular category and all the questions in the category
        # JavaScript will handle this information -
        # it will disable categories in categories_done
        if questions_in_this_category_answered == all_questions_in_this_category:
            player.categories_done.add(category)

        return redirect('quiz:quiz')


    context = {
        'player': player,
        'categories': categories,
        'categories_done': player.categories_done,
    }

    return render(request, "quiz/quiz.html", context)
     Did you forget to add a path in your urls.py to handle that request? No worries, that is common. In Django you have a path in urls.py that connects to a view in your views.py, which renders a template in your templates folder. At least that's in FBV (function based views). Don't get me started on CBV (class based views). CBV's are a powerful, different way of making code reusable and makes it easier to use the DRY (Don't Repeat Yourself) idea in python and Django. But that's a whole other lesson (and frankly, I have literally no experience writing CBV's). So, here is the urls.py now with the new view get_questions_answered path):
from django.urls import path

from . import views

app_name = 'quiz'

urlpatterns = [
    path('', views.quiz, name='quiz'),
    path('get_questions/<int:category_pk>', views.get_questions, name='get_questions'),
    path('get_answers/<int:question_pk>', views.get_answers, name='get_answers'),
    path('get_questions_answered', views.get_questions_answered, name='get_questions_answered'),
]
     The final step is to use JavaScript to target the options in the select tags and disable those questions already answered, and the categories completely done by the player:
// quiz.js

document.addEventListener('DOMContentLoaded', () => {

    ////////////////////////////////////////////////////////////////////
    //////////////////////// INITIALIZE STUFF //////////////////////////
    ////////////////////////////////////////////////////////////////////
    const category = document.getElementById('category');
    const question = document.getElementById('question');
    const answer = document.getElementById('answer');
    const submit = document.getElementById('submit');

    question.style.display = 'none';
    answer.style.display = 'none';
    submit.disabled = true;


    ////////////////////////////////////////////////////////////////////
    //////////////////////// GET QUESTIONS /////////////////////////////
    ////////////////////////////////////////////////////////////////////

    async function get_questions(e) {

        answer.style.display = 'none';
        answer.value = 0;
        question.value = 0;
        submit.disabled = true;

        const get_questions_answered = await fetch('/get_questions_answered');
        const questions_answered = await get_questions_answered.json();

        const get_questions = await fetch('/get_questions/' + e.target.value);
        const questions = await get_questions.json();

        for (let i = 0; i < questions.length; i++) {
            question.options[i+1] = new Option(questions[i].question, questions[i].id);
            if (questions_answered.includes(questions[i].id))
            question.options[i+1].disabled = true;
        }

        question.style.display = 'block';

    }

    category.addEventListener('input', get_questions);



    ////////////////////////////////////////////////////////////////////
    //////////////////////// GET ANSWERS ///////////////////////////////
    ////////////////////////////////////////////////////////////////////

    async function get_answers(e) {

        answer.value = 0;
        submit.disabled = true;

        const response = await fetch('/get_answers/' + e.target.value);
        const answers = await response.json();

        for (let i = 0; i < answers.length; i++)
            answer.options[i+1] = new Option(answers[i].answer, answers[i].id);

        answer.style.display = 'block';

    }

    question.addEventListener('input', get_answers);



    ////////////////////////////////////////////////////////////////////
    ////////////////////// Enable/Disable SUBMIT ///////////////////////
    ////////////////////////////////////////////////////////////////////

    answer.addEventListener('input', () => {
        submit.disabled = false;
    });

});
     I hope that you actually learned how to incorporate JavaScript in general, AJAX and fetch() in particular to make Django dynamic. Look, nowadays, there are frameworks for everything. Django is just a python framework. Frameworks are good. They make routine coding easier by not having every coder try to reinvent the wheel; come up with new ways to do things that have been done so many times before that frameworks have come about to take care of them. And frameworks do their job well! I certainly don't have the knowledge or patience to start with pure python and create a website from that! However, sometimes, frameworks like htmx (and I'm not knocking it- it is a powerful, useful tool that, depending on what you're looking to do, might be well worth your while), are overkill, and can limit flexibility. I've use nothing aside from vanilla JavaScript here, and the code is less thant 100 lines, powerful, and very flexible, so you can adjust it to your particular needs. Moral of the story: If a framework fits your needs (and you can often find one that does), then use it!, don't reinvent the wheel. But, at least understand the basics for when you need to make something custom. And after all, why would anyone hire you as a developer if you can't make custom things!



Comments

Jump to:    top    setup    ajax    database    finishing    comments   

Keep in mind that this little tutorial is still a work in progress. Of course I would appreciate any suggestions.

To comment you must log in with any account below:
Login with Google Login with Github Login