DEV Community

loading...

Building a QuizGenerator With Flask and Fauna

Curious Paul
College Student, tech enthusiast, web developer, python developer, Beta microsoft learn student ambassador
・18 min read

Over the past month or two, I’ve had quite the experience working with FaunaDb, and its many features that don’t fail to amuse me a fair amount each time. I’m primarily a backend developer and one important thing I have to deal with is serving pre-formatted data to clients that request that data.

An example would be having to serve paginated data to clients that need to render data in pages (say frontend of an eCommerce with a list of fancy shoe products). FaunaDb makes this task easy with its paginate function, and in this article, I’ll be going over how to build a Quiz generator using Fauna to serve the data in paginated format.

Table Of Contents

  • Setting up the database with FaunaDb
  • Building Our flask App
  • Conclusion

Setting up the database With FaunaDb

If you’d like to follow along, sign up for a Fauna account. Let’s begin by creating a new database on Fauna, two collections Quiz, and Category, as well as some indexes to help with data retrieval. Go to your Fauna dashboard and create a database as follows:

Alt Text

I’m calling it “quizgen” but you can call it whatever you want, hit the Save button, and it should take you to a new page with the option to create a new collection on there. Click the button to create a new collection to create a new collection.

The first collection I created is called Categories, and this is where we’ll store the different categories of quiz questions that we’ll have on the quiz generator.

Hit the Save button to save your new collection, and create a second one called quiz ;this is where we’ll actually store our quizzes. Let's look at the structure of our quiz documents (documents in FaunaDb refer to rows in a table(collection) in relational databases).

QUIZ Document:

  • quiz_body: this field contains the question
  • A: field representing option A, there are four options in total hence a field for each one.
  • answer: this field holds the option that is the correct answer to the question.

Category Document:

  • Name: this is the only field in a category document, and it’s just a string value that holds the name of the category.

Populating The Categories Collection

The next thing we would do is to add our categories to the categories collection. We’re going to do this manually from the dashboard on the website since the document only has one string field and it’s simple enough for a small number of category documents. We’ll create three documents in the collection with names: art, science, and entertainment. To do this head over to the collections options on the dashboard and choose the categories collection, it should show you the option to create a new document.

It should bring you to this new area, with an editor where you can type syntax to create new documents. In there type in the following, to create a new document to represent the science category:

Alt Text

Click the save button to save, and repeat this same step, for the remaining two categories. Once you are done, all the documents should be displayed on the collections page for that particular collection.

Alt Text

Creating Indexes

Let’s create indexes to help with data retrieval from the database. For our quiz app we’ll have two indexes, described as follows:

  • We want to be able to get a list of all quizzes in a certain category, so we’ll make an index for that, call it quiz_by_category.
  • We also want to be able to get a category document by name. We'd need that to tag each quiz by the name of its category, as we will see later on. We’ll call this category_by_name

To create an index on our Fauna database, you head over to the indexes section and click the create a new index button, this page should look something like this once you click the New Index button\

Alt Text

On this page, we see a couple of fields to fill:

  • Source Collection: This is what collection we hope to use the index we’re about to create on, in this case, we’re using the index(category_by_name) on the *category * collection.

  • Index Name: This is where we name our index, by giving it a descriptive name - such as -category_by_name.

  • Terms: since we say we need to get category by name, we need to be able to specify the name of the category we want to get with the index, and this is where this field comes in. So in this instance, we simply type in “name” this correlates with the name field on the category collection. When we specify the name of the category we wish to fetch with the index, it will use whatever we type here (which should be the name of a field on the collection; much like a column on a table with relational databases), to look for the document with that particular value in that field.

  • Values: Once we find the document(s) with the index, should the whole document be returned or perhaps just its reference ID, or maybe one of its field values; we can specify what fields or attributes we want from the query. And for the purposes of this tutorial, we will choose to specify the name field of the document(s) that the query returns. Leaving this option empty, makes the query return the reference to each of the documents instead, which can then be used to query the collection for the documents.

Once you’ve filled the form, it should like what I have below:

Alt Text

To create the new index hit Save, and you should now see a new index has been created. We can test this index, as you should now see a small search field right on that page after saving the index, simply type in any of the category names we created in the category collection, and hit the search button to query the database with that index, and it should return whatever was specified in the values option from the last step (in which case was the name of the category).

Alt Text
Let’s create one more index to fetch quizzes by category, so repeat the same steps and fill in the required fields as follows:

Alt Text

So note the differences, here we specified the source collection as Quiz since that’s where our data will be coming from and this also doesn’t specify any values, which means we want the reference for each quiz item from the query.

Creating an API key

In order to be able to communicate with Fauna and perform read and writes to our database from our flask app, we need to generate a key, from the security tab on the dashboard, as follows:

Alt Text

Leave everything as is and hit the save button to generate a new key, once it generates it copy it and save it as an environment variable with the name I saved mine with the name “FAUNA_SECRET” in a .env file, but you can use whatever you want.

Building Our Flask App

The flask app will have a fairly basic structure, it’s not going to be a REST API, it’ll render its own templates and serve data to the template itself. If you’re new to Flask kindly take a crash course or view the official docs, it’s intuitive and it’s a great place to begin your flask journey.

I have created a new folder called “quizgen/” and in there I have created a virtual environment to install my dependencies, for this app we’ll need to install Flask, Fauna client for python, and python-dotenv. Once you have activated the virtual environment, you can install the required packages as follows:

pip install flask faunadb python-dotenv

Once that’s done, we’ll make two folders, one to hold our HTML files, and one to hold static files, like images and stylesheets. The first one is called /templates and it’s a name chosen by Flask by default, so Flask will automatically look for HTML files in the /templates folder, the second one is called /static and it’s where our static files go. We’ll also create a file for our Flask app, I call mine app.py as well as a .env file where we’ll store our environment variables. Your folder structure should like the one below if you did all these correctly:

Alt Text

Here’s the code for our flask server in app.py. It has a single route and it renders a template named index.html which is where our interface is built. The flask server will look for this HTML file in the templates folder we created initially so it’s important to name it correctly "templates'' not “template” (all lowercase).

from flask import Flask, render_template, url_for, request
from faunadb import query as q
from faunadb.client import FaunaClient
from dotenv import load_dotenv
import os

load_dotenv() # loads env variables from .env files
client = FaunaClient(secret=os.getenv('FAUNA_SECRET'))

# routes
@app.route("/")
@app.route("/index")
def index():
    return render_template("index.html")

if __name__ == '__main__':
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

This will be edited later on but for now, let’s leave it as so. We’ll now move on to building our interface.

Building The Interface (Frontend)

As we saw in the last part, the server renders a file called “index.html” which we have in fact - not created, so let’s do that. Again all HTML files go in the templates folder so go in the templates folder and create index.html. We’ll add some HTML markup there for our quiz frontend, we’ll also need to add styling, and to do that we’ll add a new file inside of the static/ folder, I’ll call it style.css, and we’ll write some CSS in there for the quiz app. We might also need some JavaScript so I’ll add a new file called script.js in the same directory. With this we should have a folder structure resembling the one below:

Alt Text

In the index.html we simply have the quiz categories which users can select to see a list of questions and a form that they can use to contribute the questions archive. Put the following in the index.html file.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>QuizGenerator</title>
    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100;300;400;900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="{{url_for('static', filename='style.css')}}">
</head>
<body>
    <div class="container">
        <h1 class="header-text">Quiz Generator</h1>
        <p>
            Welcome user, to begin pick from a list of categories to view the questions
            we have in our archive.
        </p>
        <div class="categories">
            <a href="{{url_for('get_quiz_by_category', category='science')}}">
                <div id="science" class="catitem">
                    <div class="imgdiv">
                        <img src="../static/undraw_science_fqhl.svg" alt="">
                    </div>
                    <p>
                        Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo, expedita?
                    </p>
                </div>
            </a>
            <a href="{{url_for('get_quiz_by_category', category='art')}}">
                <div id="art" class="catitem">
                    <div class="imgdiv">
                        <img src="../static/undraw_art_museum_8or4.svg" alt="">
                    </div>
                    <p>
                        Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo, expedita?
                    </p>
                </div>
            </a>

            <a href="{{url_for('get_quiz_by_category', category='entertainment')}}">
                <div id="entertainment" class="catitem">
                    <div class="imgdiv">
                        <img src="../static/undraw_horror_movie_3988.svg" alt="">
                    </div>
                    <p>
                        Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo, expedita?
                    </p>
                </div>
            </a>
        </div>

        <div class="contribute">
            <h2>Help grow this platform, by contributing to our archive</h2>
            <form action="{{url_for('add_quiz')}}", method="POST">
                <span>Pick a category</span>
                <select name="category" id="" aria-placeholder="Pick a category">
                    <option value="">--Please choose an option--</option>
                    <option value="science">Science</option>
                    <option value="art">Art</option>
                    <option value="entertainment">Entertainment</option>
                </select>
                <div class="quiz-details">
                    <h3>Give us the details</h3>
                    <textarea name="quiz-body" id="" placeholder="Type in the question"></textarea>
                    <div class="quiz-options">
                        <input type="text" name="A" id="" placeholder="Option A">
                        <input type="text" name="B" id="" placeholder="Option B">
                        <input type="text" name="C" id="" placeholder="Option C">
                        <input type="text" name="D" id="" placeholder="Option D">
                        <br>
                        <input type="text" name="ans" id="" placeholder="Tell us the answer">
                    </div>
                </div>
                <button type="submit">Submit</button>
            </form>
        </div>
    </div>
<script src="{{url_for('static', filename='script.js')}}"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Pretty basic HTML syntax, note that the links, form action, and HREFs in the code use the {{url_for}}, this is flask’s template engine(Jinja2) directive which is what helps to link to the server’s endpoint(s) directly, so we don’t have to use javascript to make calls to the server.

Next, we’ll add some styling to the style.css file as follows:

body {
    font-family: 'Noto Sans TC', sans-serif;
    background-color: #f5f5f5;
}

.header-text {
    text-align: center;
}

.container {
    width: 80%;
    margin:0 auto;
    padding: 0;
    overflow:hidden;
    text-align: center;
}

.categories {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.catitem {
    width: 15rem;
    border: 1px dashed #333;
    height: 15rem;
    border-radius: 10px;
    padding:0 10px 0 10px;
    margin-bottom: 2rem;
    box-shadow: 5px 5px 10px #acabab;
    transition: 0.35s ease-in-out;
}

.catitem:hover {
    cursor: pointer;
    padding:0 5px;
    transform:scale(0.9);
}

.imgdiv img {
   height: 100px;
   width: 100%;
}

.imgdiv {
    width: 100%;
    background-color: #777;
    opacity: 0.5;
    box-shadow: 5px 5px 3px #333;
}

.categories:last-child {
    margin-bottom: 0;
}

.contribute {
    display:flex;
    flex-direction: column;
    align-items: center;
}

.contribute form select{
    padding:5px;
    border:none;
    background-color: f4f4f4;
    border-radius: 2px;
}

.contribute textarea {
    margin: 0 auto;
    width: 80%;
    border: none;
    background-color: #f4f4f4;
    border-radius: 5px;
    box-shadow: 2px 2px 4px #35acfc;
    margin-bottom: 10px;
}

.quiz-details {
    display: flex;
    flex-direction: column;
    margin-top:5px;
}

.quiz-details input {
    margin-bottom: 10px;
    padding: 7px;
    background-color: #f4f4f4;
    box-shadow: 2px 2px 4px #35acfc;
    border:none;
    border: 5px;
}

.quiz-details:last-child{
    margin-bottom: none;
}

.contribute button {
    border-radius: 10px;
    background-color:#35acfc;
    border:1px solid #fff;
    padding:5px;
    color:#fff;
}

.quiz-container {
    width: 80%;
    margin: 0 auto;
    background: #f4f4f4;
}

.quiz-section {
    display: flex;
    flex-direction: column;
}

.quiz-container:last-child{
    margin-bottom: 0;
}

.quizitem {
    padding:5px 8px;
    background: #6fc4fc;
    border-radius: 10px;
    width: 50%;
    margin-bottom: 20px;
}

#science {
    background-color: #fcc435;
}

#art {
    background-color: #35acfc;
}

#entertainment{
    background-color: #56fc35;
}

@media (min-width:768px) {
    .catitem {
        width: 40rem;
    }
}
Enter fullscreen mode Exit fullscreen mode

I also added some effects with javascript, so add the following in script.js

const _science = document.getElementById('science')
const _art = document.getElementById('art')
const _entertainment = document.getElementById('entertainment')


_science.addEventListener('click', ()=>{
    _art.style.transition = '0.8s'
    _art.style.transform = 'translateY(500%)'
    _entertainment.style.transition = '0.8s'
    _entertainment.style.transform = 'translateY(500%)'
})

_art.addEventListener('click', ()=>{
    _science.style.transition = '0.8s'
    _science.style.transform = 'translateY(500%)'
    _entertainment.style.transition = '0.8s'
    _entertainment.style.transform = 'translateY(500%)'
    _art.style.transform = 'translateY(-120%)'
})

_entertainment.addEventListener('click', ()=>{
    _art.style.transition = '0.8s'
    _art.style.transform = 'translateY(500%)'
    _science.style.transition = '0.8s'
    _science.style.transform = 'translateY(500%)'
    _entertainment.style.transform = 'translateY(-220%)'
})
Enter fullscreen mode Exit fullscreen mode

Once we have all these, we can start our server and hit the /index route to see our page with its design. So from the terminal run :

python app.py
Once the server is running, go to localhost:5000/ or localhost:5000/index to see the page.

Alt Text

Here’s what it should like if all goes well. Try clicking on any of the categories to see the effect. Now it’s time to add the rest of the functionalities. Currently, if you click any of the categories it does nothing other than making the other ones disappear for the sake of emphasis. That’s certainly not the only thing we want to happen when we choose a category, we also want to see the list of questions once we click on any category. We also want to be able to send new quizzes to our database from the form we added on the homepage. Let’s start with that.

Handling new quiz entries

We want users to be able to contribute to the quiz archive, and for that reason, we added a form to the homepage and specified a form-action which is where the form data is to be sent, if you check back on the index.html file we made earlier you should see it’s specified as follows:

Alt Text

This means the form data is to be sent to an endpoint(handler method) called add_quiz, so let’s add that endpoint, shall we. In the app.py file, we’ll add a new endpoint that takes in data from the form and sends it to our Fauna database, as follows:

@app.route("/addquiz", methods=['POST'])
def add_quiz():
    _data = request.form
    # search for category specified using index
    get_category = client.query(q.get(q.match(q.index('category_by_name'), _data['category'])))
    new_quiz = client.query(
        q.create(q.collection('Quiz'), {
            "data":{
                "quiz_body":_data['quiz-body'],
                "A": _data["A"],
                "B": _data["B"],
                "C": _data["C"],
                "D": _data["D"],
                "answer": _data["ans"],
                "category": get_category['data']['name']
            }
        })
    )

    return {
        "status":"sent"
    }, 200

Enter fullscreen mode Exit fullscreen mode

Observe that the name of the handler method is add_quiz which is what is specified in the form-action, and not addquiz as is with the route for the endpoint, so make sure to specify the handler method’s name and not the name used with the URL Let’s test this if your server stopped running just re-run it and go to the homepage and fill in a quiz question, hit the submit button if all goes well you should see the happy response from the server, as follows:

Alt Text

You can go back to the homepage if you wish to fill in another question, which you probably should for the sake of further tests, as we will see later on.

Fetching Quizzes By Category and Pagination

Once a category is chosen on the homepage we want the list of quizzes under that category, so for that, I’ll create a new endpoint in app.py as follows:

@app.route("/quizbycategory", defaults={'category':None}, methods=['POST'])
@app.route("/quizbycategory/<category>")
def get_quiz_by_category(category):
    if request.method == 'GET':
        # get categoruy
        get_category = client.query(q.get(q.match(q.index('category_by_name'), category)))
        query = client.query(
            q.map_(
                lambda var: q.get(var),
                q.paginate(
                    q.match(
                        q.index("quiz_by_category"),
                        get_category['data']['name']
                    ),
                    size=3
                )
            )
        )
        result = [i['data'] for i in query['data']]
        _vars['category_name'] = category
        if 'after' in query.keys():
            _vars['after'] = query['after'][0]
        #print(_vars)
        print(query)
        return render_template('quizpage.html', result=result)
Enter fullscreen mode Exit fullscreen mode

This endpoint has two possible forms, it may or may not be called with the additional query parameter , I do this so I can use the same endpoint for two different purposes; one is to retrieve data from the database, and the other is to move between pages. The code displayed above shows the first part of these, which is to get data from the database (which in this case is to get the quizzes from the database when a category is selected. Earlier in the index.html file, we wrapped each category in a tag with its HREF property set to this endpoint(the one with the placeholder), with the name of the category as the query parameter, as shown below:

Alt Text

So once each one is clicked a GET request is sent to that endpoint. On the endpoint, we get the category specified by the query parameter from the request, and a query is sent to our Fauna database to get the category with that name, and then we send a new query to fetch all quizzes that are in that category, using our quiz_by_category index. We then use Fauna’s paginate function to paginate the received data into sections of 3 using the size parameter, so the query returns an array of three items, which is what we will display per page.

We use the map function to map the lambda function (which basically fetches the actual document), to each item in the array that’s returned from the paginate function, because the paginate function returns the reference of each item it finds by default instead of the document, which is what we’d need to get the quiz info.
In addition to the array containing the items found from the query, Fauna’s paginate function also includes a cursor, as the name implies it points to the reference of the document that begins the next set of paginated data. Usually, the returned data looks like this:

Alt Text

Observe the cursor called after and the data which is what contains our quiz documents. We need to store this after cursor because we’ll need it to query for the next page of data. We also need to keep track of the current category, so I created a dictionary to hold our temporary values for us right above the first endpoint, as such:

Alt Text

Note that there’s also a before cursor here, it is used to refer to the last item from the last page so we can go back to a previous page, it’s not part of the returned data as shown above because there’s nothing before the first set of paginated data. When you reach the end of all the data, it doesn’t return an after cursor but only a before cursor. Lastly, we return a new template for our quizzes called quizpage.html so head over to the templates folder and create a new file called quizpage.html and add the following markup (PS: we already added styling for this page in style.css earlier, score points to you if you noticed)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>QuizPage- {{result[0]['category']}}</title>
    <link rel="stylesheet" href="{{url_for('static', filename='style.css')}}">
</head>
<body>
    <div class="quiz-container">
        <div class="heading">
            <h1>
                Category: {{result[0]['category']}}
            </h1>
        </div>
        <div class="quiz-section">
            {% for item in result %}
                <div class="quizitem">
                    {{item.quiz_body}}
                    <div class="quiz-options">
                        <input type="radio" id="A">
                        <label for="A">A. {{item.A}}</label><br>
                        <input type="radio" id="B">
                        <label for="B">B. {{item.B}}</label><br>
                        <input type="radio" id="C">
                        <label for="C">C. {{item.C}}</label><br>
                        <input type="radio" id="D">
                        <label for="D">D. {{item.D}}</label><br>
                    </div>
                    <div class="quiz-info">
                        <span><h3>Ans: {{item.answer}}</h3></span>
                    </div>
                </div>
            {% endfor %}
        </div>

        <div class="transit-pages">
            <form action="{{url_for('get_quiz_by_category')}}" method="POST">
               <button type="submit" name="next" class="transit-btn">Next>></button>
               <button type="submit" name="prev" class="transit-btn">Prev<<</button>
            </form>
        </div>
    </div>

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Once you have all these, save and re-run your server, before trying it out you may want to add some quizzes, with the form on the homepage. Once that’s done go to the homepage and click on the category with some quizzes in it to see the quizzes displayed as such:

Alt Text

Note the form at the bottom of quizpage.html, with the two buttons Next and Prev, we will use those to send requests to get next or previous data sets.

Moving between pages

In order to move between pages, we make use of the cursors and in the last step we store our cursor (after cursor) in a dictionary, which we can access anywhere we need it. To this effect we added two buttons into a little form we added to the quizpage.html file. These submit buttons, send a POST request to the second form of the endpoint (URL) mentioned in the previous section. As such the code to handle this lies under the elif block, in continuation of the code in the previous sections section, as follows:

elif request.method == 'POST':
        if 'next' in request.form.keys():
            get_category = client.query(q.get(q.match(q.index('category_by_name'), _vars['category_name'])))
            query = client.query(
                q.map_(
                    lambda var: q.get(var),
                    q.paginate(
                        q.match(
                            q.index("quiz_by_category"),
                            get_category['data']['name']
                        ),
                        size=3,
                        after=_vars['after']
                    )
                )
            )
            result = [i['data'] for i in query['data']]
            if 'before' in query.keys():
                _vars['before'] = query['before'][0]
            if 'after' in query.keys():
                _vars['after'] = query['after'][0]
            return render_template('quizpage.html', result=result)
        if 'prev' in request.form.keys():
            #print(_vars)
            get_category = client.query(q.get(q.match(q.index('category_by_name'), _vars['category_name'])))
            query = client.query(
                q.map_(
                    lambda var: q.get(var),
                    q.paginate(
                        q.match(
                            q.index("quiz_by_category"),
                            get_category['data']['name']
                        ),
                        size=3,
                        before=_vars['before']
                    )
                )
            )
            result = [i['data'] for i in query['data']]
            if 'after' in query.keys():
                _vars['after'] = query['after'][0]
            if 'before' in query.keys():
                _vars['before'] = query['before'][0]
            return render_template('quizpage.html', result=result)
Enter fullscreen mode Exit fullscreen mode

Here, we check to see if the next button or the prev button has been pressed, and we specify the after cursor in our query, in the event that that the next button was pressed, once we receive our results from the query we prepare the data and re-render quizpage.html with the newly returned data, which should now contain the next set of paginated data, thanks to our cursor, we also make sure to set new values for our after and before cursor, if the current set isn’t the last set of data.
We repeat the same thing, for when the prev button is pressed.

Once you’re done with this you can go back to the quiz pages and click the next button to see more quizzes. Make sure you have enough quizzes ( at least one extra quiz if anything) because we didn’t add a check to see if the query returns an empty list or not. You should see that it loads the next set of quizzes, and if you click the prev button it should take you to the previous page. You can add questions to other categories and try them out too.

Conclusion

In this article, we’ve covered some interesting concepts of data retrieval with Fauna’s API, and data handling with our flask server and Fauna’s suite of functions including the paginate function. The quiz generator we built will be hosted live for public use for free and can be accessed via this link, you can also find the source code here on github. If you have any questions about Fauna or Flask or this tutorial please leave a comment. You can also reach out to me on twitter, via @curiouspaul2, or on github @curiouspaul1 .

Discussion (0)