Giggle GPT Joke Search Engine – Basic Setup (1/6)

4.5/5 - (2 votes)

Hey there! I’m Dirk van Meerveld, your host and guide for this Python Django tutorial series. In this tutorial, we’ll build a fun project while exploring the fundamental concepts of Django and how we can leverage ChatGPT to our advantage, both in our projects and our development environment.

This is part 1 of the following series:

So, get cozy, and let’s dive right in!

Giggle - Creating a Joke Search Engine from Scratch with ChatGPT

πŸŽ“ Full 6-video course with downloadable PDF certificates: Giggle – Creating a Joke Search Engine from Scratch with ChatGPT

Making a virtual environment.

Before we jump into creating our new Django project, let’s set up a virtual environment. Think of it as a clean slate of Python where no other extensions are installed.

With a virtual environment, we can install the specific packages we need without affecting our system-wide Python environment. It’s particularly handy when different Django projects require different versions of various packages.

By creating a separate virtual environment for each Django project, we can avoid conflicts and future headaches. In a nutshell, we isolate each Django project within its own environment instead of running them all in the system-wide environment.

To create a new virtual environment, open your command line terminal and type the following command

$ python -m venv venv

We run the built-in Python module called ‘venv‘ using the -m flag, and then specify the name of the folder for the virtual environment with the second ‘venv‘ argument.

Activating our virtual environment.

Our virtual environment comes with a built-in way to activate it. This may differ depending on which terminal style you are using.

Linux(bash):

$ source venv/Scripts/activate

Windows:

$ venv/Scripts/activate (uses the.bat file)

Now that your virtual environment is activated, you should see (venv) in your terminal before or on the current line. First we have to install Django. Remember that this is a new virtual environment so it is a blank slate. Nothing is installed.

$ pip install Django (possibly pip3)

Starting a new Django project

With that installed, we will ask Django to start up a new project for us. A Django project folder contains the basic configuration files for itself and the attached applications. Inside one project we can create/install numerous applications.

$ django-admin startproject project .

We ask the django-admin to fire up a new project for us, then define the folder name to be ‘project’ (you can use a different name if you want), and then use the ‘.‘ to tell Django to use the current folder (to avoid extra folder nesting).

You will now see several files in the project folder.

  • The settings.py contains project-wide settings for your Django project.
  • The urls.py is like the incoming router that will retrieve requests to your server and forward them to the relevant functions or applications within your overall Django project.
  • There is also a manage.py file which is outside of the project folder and in your base directory. This file is used to run Django management commands.

Go ahead and run the following command (migrates changes to the database, we’ll get back to this soon so don’t worry about it).

$ python manage.py migrate

Now we’ll start up the Django development server.

$ python manage.py runserver

And you’ll see something like the following:

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

System check identified no issues (0 silenced).
May 23, 2023 - 15:16:15
Django version 4.2.1, using settings 'project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

Django is now serving our project at http://127.0.0.1:8000 and you can go ahead and Ctrl-click the link in your terminal to open the page. You should see a nice picture of a rocket ship indicating the installation was successful.

You can quit the development server at any time by pressing Ctrl-C in the terminal, or you can open a second terminal with the + button and use it for commands instead.

Starting a new application

We will tell Django to add a new application to our project by running the following command.

$ python manage.py startapp giggle

You should see a ‘giggle‘ folder appear at the same level as your ‘project‘ folder. We will get back to the files and what they are. First go inside your ‘giggle‘ folder and then create a folder named ‘templates‘ inside, and then yet another folder named ‘giggle‘ inside of the ‘templates‘ folder, like so:

> giggle
    > templates
        > giggle

We will learn about the templating system step by step but the basic reason for the double giggle folder is that a Django project with many applications will squash all the template folders together when running the server.

That means that if all your template folders have files named index.html in them they will all end up in the same place with the same name and it will be hard to know which to serve up. This is why we have another separate level of nesting with another ‘giggle‘ folder inside the templates folder.

Now inside the giggle > templates > giggle folder create an index.html file.

> giggle
    > templates
        > giggle
            index.html

And add some basic boilerplate HTML to get started. (Note that if you have Emmet shortcuts installed you can type a !+Enter to generate most of this instantly, just changing the title and adding the paragraph tag as shown below).

<!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>Giggle search</title>
</head>
<body>
    <p>Welcome to giggle.</p>
</body>
</html>

Routing

Now we’ll go to the urls.py file inside the project folder

> project
    urls.py

Here we’ll add include to the imports and add another path to the urlpatterns list.

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('giggle.urls')),
    path('admin/', admin.site.urls),
]

urls.py in the project folder is like the root router of our project. It will take all incoming requests and patch them through to the next station.

The admin/ path was already defined for us and refers to the Django admin panel that comes preinstalled (www.oursite.com/admin). We added the first line which has an empty path, so all root directory traffic (www.oursite.com) will be routed through this path.

We’ve told it to include giggle.urls, which basically says to go to the giggle project and find another urls.py file there for your next clue. Make sure you also add the include import at the top as shown in the code above.

We’ll have to create the ‘giggle.urls‘ we are referring to as it does not currently exist. Go ahead and create another file called urls.py (a new empty file) but place it not in the project folder but in the giggle folder instead:

> giggle
    urls.py

Inside our giggle > urls.py file we will also create a basic ‘router’.

from django.urls import path
from . import views

urlpatterns = [
    path('', views.giggle_search, name='giggle_search'),
]

On the second line we import views (views.py – our next stop) from the current folder, as represented by the dot. We then have another urlpatterns list to hold the routes, just like our previous urls.py file.

We provide an empty path again to just catch all traffic coming here and then route it all to views.giggle_search (a function named ‘giggle_search‘ in our views.py file which we’ll create next).

The final name argument is a namespace that we can use later on in our templates and view functions to create links between pages without having to hardcode link addresses to specific HTML files, meaning the routing won’t break if we change page or template locations.

View functions

Our next stop is views.py, which is located in our giggle folder.

> giggle
    views.py

In here we will keep the render function import Django provided for us, and now we will define a function called giggle_search. Remember we linked from the urls.py in the project folder to the urls.py in the giggle folder to the views.py file giggle_search function.

from django.shortcuts import render

def giggle_search(request):
    return render(request, './giggle/index.html')

The request object is the HTTP request object you might already be familiar with if you’ve done more web work. We’ll make use of it later. For now, we just return a render passing in the name of our template to render.

Note we consider the templates folder as the basis to start from and then tell it to go into the giggle folder and then index.html. This is why we had the double folder structure with a second folder named giggle.

Next, we will go to settings.py which is found in the ‘project‘ folder.

> project
    settings.py

This file contains the configuration settings for our entire project and the installed apps. For now, find the list named INSTALLED_APPS, and then add ‘giggle‘ to the list to tell Django we created a new app called ‘giggle‘ we want to start using.

INSTALLED_APPS = [
'giggle',
'django.contrib.admin',
'django.contrib.auth',
etc....
]

Now start your Django server from the command line if you haven’t got it running already.

$ python manage.py runserver

If you open up the main page now you should no longer see the rocket-ship we saw before but your own html page which should read a small ‘Welcome to giggle‘. We are serving our own page on the server!

Using templates

Django uses a template engine. What this means is that we do not have to hardcode all our HTML separately. We can actually make snippets and combine them all together into individual pages, also inserting variables and database data.

The easiest way to explain how this works and get a feel for it is just to show you how it works and get your hands dirty over the coming tutorial parts.

Rename the index.html we created to become base.html instead

> giggle
    > templates
        > giggle
            base.html (renamed from index.html)

Inside our newly renamed base.html file, remove the paragraph tag and replace it as below. All the boilerplate is the same as before.

<!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>Giggle search</title>
</head>
<body>
    <div class="container">
        {% block content %}{% endblock %} 
    </div>
</body>
</html>

We added a div with a class of container so we can target it using CSS later.

Inside the div we see template syntax. The curly brackets start and end template syntax ‘mode’ and the percent sign indicates that a special function or command will run. In this case we are specifying the beginning and the end of the content block that will go inside this page. We are in effect creating a hole of sorts, into which we can insert code from other files.

We’ll create another file named giggle_search.html in the same directory

> giggle
    > templates
        > giggle
            base.html
            giggle_search.html

And inside will go the following code:

{% extends "./base.html" %}

{% block content %} 
    <p>Welcome to giggle.</p>
{% endblock %}

Note the curly braces to enter template syntax mode and the percent sign again. The code is easy to read.

We extend the base.html file in the same directory, so we’re basically saying give us base.html and plug the below into the hole we created. Then we define the actual content block we want to insert with the block content tag, inside which we have a single lonely paragraph HTML element.

We then close the content block. Template syntax, unlike Python, does not rely on indentation and pretty much anything you open will need to be manually closed again, so make sure not to forget to do so.

Now we need to fix our views.py file in the ‘giggle‘ folder, and change the return line to reflect the changed name of the template file.

...
def giggle_search(request):
    return render(request, './giggle/giggle_search.html')

Upon running the server again, you should notice that nothing has changed, except all our boilerplate is now in a separate file and we can insert whatever content we want into the page easily, keeping our HTML DRY, free of repetitive boilerplate code.

Static files

Next, create a folder called ‘static‘ inside of the ‘giggle‘ directory. Inside, copy the giggle.png logo provided below, and create an empty file called styles.css.

> giggle
    > static
        giggle.png
        styles.css (empty file)

The static folder is used for static files like CSS and images, and we can ask Django to load these for us for easy insertion into our templates.

Go back to our base.html file and insert the load static template tag directly after the HTML head element open

<head>
    {% load static %} 
    ...

Then go to bootstrapcdn.com and copy the HTML code under both the CSS and Javascript headings into the head. We will be using Bootstrap to save some time on CSS and not make this into a CSS tutorial.

Also, go ahead and include the following two lines to link Material Icons for easy icon insertion and our own CSS file, which is empty so far.

Note the href has template syntax inside which first defines that we want to open a static file and then provides the name inside the static folder we want to open.

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<link href="{% static 'styles.css' %}" rel="stylesheet" />

You should now have something like the following as your base.html file:

<!DOCTYPE html>
<html lang="en">
<head>
    {% load static %} 
    <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>Giggle search</title>
    <!-- css & icons -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js" integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V" crossorigin="anonymous"></script>
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
    <link href="{% static 'styles.css' %}" rel="stylesheet" />
</head>
<body>
    <div class="container">
        {% block content %}{% endblock %} 
    </div>
</body>
</html>

Next, we’ll head over to the giggle_search.html file and start creating some basic content. First add the load static tag at the top of the file so we can use our giggle image.

{% extends "./base.html" %}
{% load static %}
...

Now let’s place some actual content inside our content block

{% block content %} 
    <header>
        <img class="img-fluid" src="{% static 'giggle.png' %}" alt="Giggle-logo" />
    </header>
    <main>
        <div class="input-group mb-5">
            <input type="text" class="form-control" placeholder="Input topic" />
            <button class="btn btn-outline-secondary" type="submit">
                <span class="material-icons mt-2"> search </span>
            </button>
        </div>
        <div class="results">
            <h3>Search Results</h3>
            <p>Your search results will appear here.</p>
        </div>
    </main>
{% endblock %}

Inside the header we have an image with a class of img-fluid (Bootstrap shortcut for responsive images). The source is defined using the tag we saw above for the styles.css file which opens with the curly brackets and percent sign to then define that we want to go into static and then the file we want inside single quotes.

In the main div we have a class of input-group (Bootstrap shortcut for styling forms) and a class of mb-5 which is bootstrap for margin-bottom-5.

There is an input field and a button inside, again with a couple of bootstrap helper classes to quickly style them for the purposes of this tutorial. The span inside uses the material-icons class which combined with the span’s content of search will bring up the search icon for us.

The mt-2 class on the span element is a margin-top-2 bootstrap class to help with alignment. The div below just has some fake search results.

Now if you reload the page again, you should see a decent page. You may need to either do a hard refresh of your browser (Ctrl+F5 for Chrome) or stop and restart your Development server for the changes to take effect.

The CSS could use a little work, so let’s go to our styles.css file inside the static folder.

html, body {
    height: 100%;
    background-color: whitesmoke;
}

.container {
    max-width: 500px;
    padding: 10rem 0 10rem 0;
}

The container class selected in the second selector is the one we defined on the div in the base.html file to wrap the content.

If you refresh your development server and do not see any changes remember you may sometimes need a hard browser refresh, to stop the browser from caching old versions of your styles.css.

Also make sure you saved the changes to your files, Django will not serve up the changes unless you have saved them.

Databases and the Django ORM

We will come back to this in more detail in the following parts of the tutorial, so for now a brief summary.

We will be using an SQL database to store our data. Normally you have to use SQL queries to create, read, update and delete entries in these databases. If you don’t know SQL, don’t fret.

Django uses an ORM (Object Relational Mapper) which allows us to simply create objects and save them using Python code. Django will generate the SQL queries for us behind the scenes and take care of the actual database operations.

Actually, Django has already set up an SQL database for you!

In your base folder should be a file called 'db.sqlite3'. If you have a program like 'SQLite browser' installed you can go ahead and open it and check out what’s inside. You can also use command-line SQL tools if you’re an experienced SQL user.

You don’t have to do either though, you can just follow along and check the provided images. You don’t need to know SQL or install SQLite browser to continue with this tutorial.

You will notice our SQLite database already has several tables inside for it’s built-in user and admin modules. These come pregenerated. Let’s create a table of our own using only Django.

Go to models.py inside the ‘giggle‘ folder. This file is where we will define our models. Each model is an object that Django will translate into a database table schema for us. A model defines the rules we will use to save a certain type of data in the database. Django will then create the actual table in the database for us and also help with our CRUD (create, read, update and delete) operations on the data later.

Inside the models.py add the following code:

from django.db import models

class Joke(models.Model):
    query = models.CharField(max_length=50)
    response = models.TextField()
    created_at = models.DateTimeField(
        verbose_name='Created at',
        auto_now_add=True,
    )

Django provides an object called ‘models‘ for us to import. We then define a new class that we called ‘Joke‘ which extends ‘models.Model‘, which Django has created to allow us to easily make models. Then we define the fields we want to use. These will become our table columns.

First we want to save a field we name ‘query‘ which is a character field that can hold a maximum of 50 characters.

Then we define the response, which uses a TextField of undefined length. Finally, we add a created_at field which is of type DateTimeField. The verbose name allows us to specify a better version with correct English grammar for the Django admin panel which we’ll look at later.

The auto_now_add, as the name suggests, will automatically add the current date and time whenever an object is created, so we don’t have to actually provide a date and time when creating an object, the current date-time will be inserted automatically.

Django will take care of translating these to appropriate data types for the database we are using. Django will also automatically generate and add primary keys for us. More on that in a later section.

Django migrations

Whenever we change models in a Django application, we will need to migrate these changes to the database. We just wrote some Django code, and now Django has to migrate these changes to the SQL database for us.

This is a two-step process. Go to your terminal and close the development server if it’s still running using Ctrl+C. Then type the following command.

$ python manage.py makemigrations

Inside the ‘giggle‘ folder is a folder called ‘migrations‘.

Django has just created a new file called '0001_initial.py' inside this directory, which contains instructions for Django on how to migrate the changes to the database. You don’t have to do anything with this file. These files will build up with each change and serve as sort of a history.

To actually apply the changes to the database, run the following command:

$ python manage.py migrate

And if we check our db.sqlite3 file now, we will see there is a new table called giggle_joke! We will come back to this in a later part to actually start saving some data.

Posting data

We’ll go back to giggle_search.html in our templates/giggle folder and start working on having the webpage actually post some data we can receive. We need the user query before we can do anything.

<header>
    <img class="img-fluid" src="{% static 'giggle.png' %}" alt="Giggle-logo" />
</header>
<main>
    <form class="input-group mb-5" action="/" method="post">
        {% csrf_token %}
        <input type="text" name="query" class="form-control" placeholder="Input topic" />
        <button class="btn btn-outline-secondary" type="submit">
            <span class="material-icons mt-2"> search </span>
        </button>
    </form>
    <div class="results">
        <h3>Search Results</h3>
        <p>Your search results will appear here.</p>
    </div>
</main>

First we changed the div into a form, making sure to also change the closing element. We added an action of slash, indicating we will be posting to the exact same address we are currently on, and added a method of post. On the input element we added a name of “query” to capture the user’s query. You might have noticed the template syntax csrf_token call in between.

Django’s CSRF token is a security measure that helps protect against cross-site request forgery attacks. It’s a unique token generated for each user session and included in forms or AJAX requests. When a request is made, Django checks if the token matches, ensuring that the request originated from the same site and not from a malicious source.

A cross-site request forgery attack (CSRF) is when a malicious third-party website tricks a user’s browser into performing an unintended action on another website where the user is authenticated or logged in, without the user being aware of this. The CSRF token ensures the user is actually visiting the real website themselves and intending to submit a request.

Next go to views.py inside our ‘giggle‘ folder and edit the giggle_search function

...
def giggle_search(request):
    if request.method == 'POST':
        print('This is a post request')

    return render(request, './giggle/giggle_search.html')

The request object contains the method. Remember we set the method attribute to post on the HTML form. We can test if the request method is POST. Then we can take a certain action. If you reload the page now and type something in the search box then hit enter, it will show in your Django server console.

Now change the print request to print(request.POST)

...
if request.method == 'POST':
    print(request.POST)
...

This will get the post data from the request object and print it to the console.

<QueryDict: {'csrfmiddlewaretoken': ['3JYtCWlDakk01H05RcO3BROIDUlKzxTuplvrSwbO8Y2NwClF0M4jIgNHON3NGtMx'], 'query': ['test']}>

If you type a query on your site and hit enter now you should see something like the above, containing the csrfmiddlewaretoken and your query inside a QueryDict data structure.

Passing dynamic data to our templates

Now let’s pass this query string back to our template so we can render it and work with this dynamic data. Fix your giggle_search function one more time.

def giggle_search(request):
    context = {}
    if request.method == 'POST':
        query = request.POST.get('query')
        context['query'] = query

    return render(request, './giggle/giggle_search.html', context)

First we create a context object.

Context is a frequently used name for data we pass down to some sort of template that will use this context data. We use the .get() method on the request.POST and type the name of the key-value pair we want to extract, which is the 'query'.

Then we set the 'query' key in our context dictionary to equal this query.

Finally, we return the render as always, but a third argument has been added to the render function, which is the context dictionary we are sending along to the template.

Note that if there is no post data, and the user is just loading the page for the first time, the method is a GET request. Therefore the if statement fails and the page is just rendered as it had been in the past. We can use this single view function to handle both the GET and POST methods in this way.

Go to giggle_search.html and replace only the contents of the div with a class of ‘results‘ as follows, making sure to keep the surrounding elements untouched:

...
<div class="results">
    <h3>Search Results</h3>
    {% if query %}
    <p>Your search query was {{ query }}</p>
    {% else %}
    <p>Your search results will appear here.</p>
    {% endif %}
</div>
...

You’ll see that the template syntax also includes an if statement. If there is a variable named query (inside the context dictionary we passed along to this template), the next line will insert that variable inside the paragraph using a double curly brace syntax with the variable name inside. The double curly braces is how you insert variables in template syntax.

Then we have an else statement, in case there is no variable named query, which probably just means the user loaded the page for the first time. Finally, we always have to close if statements in template syntax. It will not auto-close or adhere to indentation like Python.

Now if you load your page and type a query of ‘test’ your website should display: "Your search query was test". Good job! That’s it for part 1, hope to see you soon in the next one.