Build a Driver Accreditation Workflow

Build a Django workflow to approve driver applications and publish a directory.

Introduction

30 Second Summary

Paper forms and scattered messages make it hard to tell which drivers have been reviewed. Residents need a clear way to see who has actually been approved.

In this project, you will build a local driver accreditation workflow with Django, MySQL, plus Tailwind CSS. Approval in Django admin controls which drivers appear in the public directory.

What You'll Build

You will demo a local portal where a sample driver application moves from a mobile-friendly form through administrator review before appearing in a resident-facing directory.

By the end of this project, you'll have:

  • An application workflow that accepts sample accreditation details plus a fake license file. Each saved submission returns a numeric reference number.
  • A searchable administrative review flow for finding submitted applications. You can inspect each fake document before setting the driver's status.
  • A public approved-driver directory where residents see approved records. Pending or rejected submissions stay hidden.
  • Secret Mission: Add a private status tracker that reveals an application's status only when its reference number matches its body number.

Are there any prerequisites?

You need internet access plus administrator access on a Windows 10 or newer device.

Use only fake sample data throughout the project.

Before We Start

Before the hands-on work begins, lock in how driver applications move from submission to review. This approval rule keeps unverified drivers out of the public directory.

Set Up the Windows Development Environment

The accreditation workflow needs a dependable local foundation before it can accept driver data. Every tool must use compatible versions so setup problems do not surface halfway through the build.

You will prepare Visual Studio Code, Python, Django, MySQL Community Server, and Tailwind CSS on Windows. The result is an isolated project that runs without adding Node.js.

In this step, get ready to:
  • Verify the required Windows development tools.
  • Create an isolated Django project with pinned dependencies.
  • Connect Django to MySQL and run the Tailwind watcher beside the development server.
Verify the required Windows tools

Version checks reveal which parts of the local stack are ready. Matching the project versions now prevents compatibility errors during the build.

  • Press the Windows key to open Windows search.
  • Type Command Prompt into the search bar.
  • Press Enter to open Command Prompt.
  • Check the installed Python version by running this command:
py --version

What does this check prove?

The Windows Python launcher reports the runtime it uses for new projects. This project targets Python 3.14.7.

✔️ I see version 3.14.7

Python 3.14.7 is ready. It supports the Django release used in this project.

ⓧ I see an older version

The current Python release is below the project target. Install Python 3.14.7 before creating the virtual environment.

  • Open the official Python installer page.
  • Download the Windows installer for Python 3.14.7.
  • Run the downloaded installer.
  • Close Command Prompt after installation.
  • Open a fresh Command Prompt through Windows search.
  • Repeat the Python version check.

ⓧ The Python command is missing

The Windows Python launcher is unavailable. The official installer provides the runtime required by this project.

  • Open the official Python installer page.
  • Download the Windows installer for Python 3.14.7.
  • Run the downloaded installer.
  • Close Command Prompt after installation.
  • Open a fresh Command Prompt through Windows search.
  • Repeat the Python version check.
  • Check whether Visual Studio Code is available from Command Prompt by running this command:
code --version

What does this check prove?

The command confirms that Visual Studio Code is installed. It also confirms that Windows can launch the editor from Command Prompt.

✔️ Visual Studio Code responds

Visual Studio Code is ready to open the project folder.

ⓧ The command is missing

Visual Studio Code needs to be installed or repaired. The User setup is recommended for most Windows users.

  • Open the official Visual Studio Code Windows setup guide.
  • Download the Visual Studio Code User setup.
  • Run the downloaded setup.
  • Close Command Prompt after installation.
  • Open a fresh Command Prompt through Windows search.
  • Repeat the Visual Studio Code version check.
  • Check whether the MySQL client is available from Command Prompt by running this command:
mysql --version

What does this check prove?

The MySQL client reports the installed release when its executable directory is available on the Windows system path. This project uses MySQL Community Server 8.4.11.

✔️ I see version 8.4.11

MySQL Community Server 8.4.11 is ready for the portal database.

ⓧ I see an older version

The installed MySQL release is below the project target. Use the official MSI to update the local server.

  • Open the official MySQL Community Server download page.
  • Download the MySQL Community Server 8.4.11 MSI.
  • Run the MSI with administrator access.
  • Complete MySQL Configurator for the local server.
  • Create an administrator password when the configurator requests one.
  • Open a fresh Command Prompt after configuration.
  • Repeat the MySQL version check.

ⓧ The MySQL command is missing

MySQL Community Server may be missing. Its client directory may also be unavailable on the Windows system path.

  • Open the official MySQL Community Server download page.
  • Download the MySQL Community Server 8.4.11 MSI.
  • Run the MSI with administrator access.
  • Complete MySQL Configurator for the local server.
  • Create an administrator password when the configurator requests one.
  • Open a fresh Command Prompt after configuration.
  • Repeat the MySQL version check.

Still seeing an older or missing tool?

Close every Command Prompt window after an installation. A fresh window reloads the command paths created by the installer.

If the check still fails, ask for help diagnosing your Windows installation.

Create the isolated Django project

A dedicated project folder keeps the portal files easy to find. A virtual environment keeps its Python packages separate from other projects on your computer.

  • Press the Windows key to open Windows search.
  • Type File Explorer into the search bar.
  • Press Enter to open File Explorer.
  • Select Desktop in the left sidebar.
  • Create a new folder named tricycle-portal on your Desktop.
  • Press the Windows key to open Windows search again.
  • Type Visual Studio Code into the search bar.
  • Press Enter to open Visual Studio Code.
  • Click File in the top menu.
  • Click Open Folder.
  • Select the tricycle-portal folder on your Desktop.
  • Click Select Folder.

Asked whether you trust the folder?

Visual Studio Code may ask whether you trust the folder's authors. Confirm that you trust it because you created this empty project folder yourself.

The standalone Tailwind executable compiles CSS without requiring Node.js. Choose the tab that matches the Tailwind file currently on your computer.

✔️ I already have version 4.3.3

Your existing Tailwind CSS 4.3.3 Windows standalone executable can be reused.

  • Move the executable into the open tricycle-portal folder.
  • Rename the executable to tailwindcss.exe.
  • Confirm that tailwindcss.exe appears in the Visual Studio Code Explorer sidebar.

ⓧ I have an older executable

Replace the older executable with the project release. Keeping the exact release makes the build command repeatable.

  • Move the older executable outside the tricycle-portal folder.
  • Download the official Windows standalone executable for Tailwind CSS 4.3.3.
  • Move the downloaded executable into the tricycle-portal folder.
  • Rename the executable to tailwindcss.exe.
  • Confirm that tailwindcss.exe appears in the Visual Studio Code Explorer sidebar.

ⓧ I do not have Tailwind yet

Download the project release from the official Tailwind repository. The file is a self-contained Windows executable.

  • Download the official Windows standalone executable for Tailwind CSS 4.3.3.
  • Move the downloaded executable into the tricycle-portal folder.
  • Rename the executable to tailwindcss.exe.
  • Confirm that tailwindcss.exe appears in the Visual Studio Code Explorer sidebar.

Why the standalone Tailwind CLI?

The standalone executable contains everything needed to compile Tailwind styles. It keeps this Python project free from an additional Node.js installation.

  • Click Terminal in the Visual Studio Code top menu.
  • Click New Terminal.
  • Select Command Prompt as the terminal profile if another shell opens.
  • Create the isolated Python environment in the open project folder by running this command:
py -m venv .venv

What does this command do?

The command creates a Python environment inside .venv. Packages installed there stay scoped to the accreditation portal.

You will see a new .venv folder in the Explorer sidebar. That folder is the isolated home for the project's Python packages.

  • Activate the virtual environment in Command Prompt by running this command:
.venv\Scripts\activate.bat

What does activation change?

Activation points package commands at the local environment. The Command Prompt line begins with (.venv) when activation succeeds.

  • Click the New File icon beside the TRICYCLE-PORTAL heading in the Explorer sidebar.
  • Name the new file requirements.txt.
  • Add the pinned project dependencies to requirements.txt using this code:
Django==6.1.1
mysqlclient==2.3.0

Why pin these dependencies?

  • Django 6.1.1 keeps the framework behavior aligned with the project code.
  • mysqlclient 2.3.0 gives Django its recommended native MySQL driver.
  • Save requirements.txt.
  • Install the pinned dependencies into the active virtual environment by running this command:
py -m pip install -r requirements.txt

What does this installation do?

The command reads both package pins from requirements.txt. It installs them inside the active .venv environment.

The installation output should finish without a package error. Your project now has Django 6.1.1 plus mysqlclient 2.3.0.

Dependency installation failed?

  • Confirm that (.venv) appears at the start of the Command Prompt line.
  • Confirm that requirements.txt contains the two package pins shown above.
  • Confirm that your internet connection is active.

If installation still fails, ask for help diagnosing the package output.

  • Create the Django project inside tricycle-portal by running this command:
django-admin startproject tricycle_portal .

What does the project scaffold create?

Django creates manage.py beside the tricycle_portal package. That package contains the project settings and root URL configuration.

The Explorer sidebar now shows manage.py plus the tricycle_portal package.

  • Create the accreditation app inside the project by running this command:
py manage.py startapp accreditation

What does the app scaffold create?

A Django app groups one area of project behavior. The accreditation app will own the driver application workflow.

The Explorer sidebar now shows an accreditation folder containing files such as admin.py, models.py, and views.py.

Connect MySQL and run the stack

The project scaffold still needs a database that belongs only to this application. A dedicated MySQL account limits the application's privileges to the tricycle_portal database.

You will also configure the shared file locations used by templates, compiled CSS, and fake media uploads. The final check proves that every layer can run together.

  • Sign in to MySQL as the local administrator by running this command:
mysql -u root -p

How does the password prompt work?

The -p option asks for the administrator password after the command starts. The password stays out of the command itself.

  • Enter the MySQL administrator password you created during MySQL Configurator.
  • Create the project database and application account by running these statements at the MySQL prompt:
CREATE DATABASE tricycle_portal CHARACTER SET utf8mb4;
CREATE USER 'tricycle_app'@'localhost' IDENTIFIED BY 'your-strong-password';
GRANT ALL ON tricycle_portal.* TO 'tricycle_app'@'localhost';

What do these statements create?

  • The first statement creates the local tricycle_portal database with the utf8mb4 character set.
  • The second statement creates the local tricycle_app account with the tutorial password.
  • The third statement grants that account all privileges on tricycle_portal.*.

That is the database boundary in place. Django can now use its own account without relying on the MySQL administrator.

MySQL rejected a statement?

  • Confirm that the MySQL prompt is active.
  • Check the quotes around tricycle_app and localhost.
  • Check that each statement ends with its terminator.

If a database or account already exists, ask for help resolving the MySQL state safely.

  • Close the MySQL terminal after the grant succeeds.
  • Create a templates folder beside manage.py using the Explorer sidebar.
  • Create a static folder beside manage.py using the Explorer sidebar.
  • Create a src folder inside static using the Explorer sidebar.
  • Create a css folder inside static using the Explorer sidebar.
  • Create a media folder beside manage.py using the Explorer sidebar.

Where do these folders fit?

The templates folder will hold shared page layouts. The static folder holds editable and compiled styles.

The media folder will hold fake driver license uploads during local development.

  • Open tricycle_portal/settings.py from the Explorer sidebar.
  • Replace the section from SECRET_KEY through INSTALLED_APPS with this project configuration:
SECRET_KEY = "your-django-secret-key-here"
DEBUG = True
ALLOWED_HOSTS = []

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

What does this configuration change?

  • The placeholder secret keeps the local tutorial configuration consistent.
  • Debug mode provides useful details during local development.
  • The accreditation entry registers the project app with Django.
  • Find the existing DATABASES block in tricycle_portal/settings.py.
  • Replace that block with the MySQL connection settings:
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": "tricycle_portal",
        "USER": "tricycle_app",
        "PASSWORD": "your-strong-password",
        "HOST": "localhost",
    }
}

How does Django reach MySQL?

  • The engine selects Django's MySQL backend.
  • The database name identifies the schema created earlier.
  • The user and password match the local application account.
  • The host keeps the connection on this Windows device.
  • Save tricycle_portal/settings.py.
  • Apply Django's initial database migrations to MySQL by running this command in an activated Command Prompt terminal:
py manage.py migrate

What does migration create?

Django writes its built-in tables to the tricycle_portal database. Those tables support authentication, sessions, and the administration site.

The migration output should list completed operations. That result proves Django can connect through tricycle_app.

Django cannot connect to MySQL?

  • Confirm that the MySQL service is running on Windows.
  • Confirm that the database name is tricycle_portal.
  • Confirm that the password matches your-strong-password in MySQL and settings.py.

For help with the connection output, ask why Django cannot reach the local database.

The administration site needs a local reviewer account. The next command asks for a username, an optional email address, and a password that stays hidden while you type.

  • Create the local Django superuser by running this command:
py manage.py createsuperuser

Where is the superuser stored?

Django stores the superuser in the authentication tables created by the migration. The account will review driver applications through Django admin.

  • Enter a local administrator username when prompted.
  • Enter an email address or continue without one.
  • Create a password for the local administrator.
  • Confirm the password when prompted.

Django confirms that the superuser was created successfully. Your MySQL-backed administration account is now ready.

  • Find the existing TEMPLATES block in tricycle_portal/settings.py.
  • Replace that block with the shared template configuration:
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

How will Django find shared templates?

The DIRS entry points Django at the top-level templates folder. Later pages can inherit one shared layout from that location.

  • Find STATIC_URL near the bottom of tricycle_portal/settings.py.
  • Replace everything from STATIC_URL to the end of the file with these static and media settings:
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]

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

What do these paths control?

The static settings point Django at the folder containing the compiled stylesheet. The media settings reserve a local folder for fake driver license uploads.

  • Save tricycle_portal/settings.py.
  • Start the Django development server in the activated Command Prompt terminal by running this command:
py manage.py runserver

What does the development server do?

Django loads the project through the active virtual environment. It connects to the configured MySQL database while serving local browser requests.

  • Open your browser.
  • Visit http://127.0.0.1:8000/.

You should see Django's default local success page. Keep this terminal running so Django can reload the project after the remaining file changes.

  • Create urls.py inside the accreditation folder using the Explorer sidebar.
  • Add an empty URL list to accreditation/urls.py using this code:
urlpatterns = [
]

Why create an empty URL list?

The root project can now include the accreditation app without a configuration error. The next step adds the first application route inside this list.

  • Open tricycle_portal/urls.py from the Explorer sidebar.
  • Replace the file with the root URL configuration below:
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("accreditation.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

How does the root URL configuration work?

  • The admin route exposes Django's local administration site.
  • The root include passes public portal routes to accreditation/urls.py.
  • The final helper serves fake uploaded files during local development.
  • Save accreditation/urls.py.
  • Save tricycle_portal/urls.py.
  • Refresh http://127.0.0.1:8000/ in your browser.

You should see a Django not-found response from the empty application URL list. This proves that the development server loaded the new root URL configuration.

  • Create input.css inside static/src using the Explorer sidebar.
  • Add the Tailwind source directive to static/src/input.css using this code:
@import "tailwindcss";

What does this stylesheet provide?

The import gives the standalone compiler its Tailwind source. The watcher converts that source into the browser-ready static/css/app.css file.

  • Save static/src/input.css.
  • Click Terminal in the Visual Studio Code top menu.
  • Click New Terminal.
  • Select Command Prompt as the terminal profile if another shell opens.

Before you start the watcher, what two signs would prove that Django and Tailwind are both running?

  • Compile the stylesheet and keep watching for future changes by running this command in the new terminal:
.\tailwindcss.exe -i .\static\src\input.css -o .\static\css\app.css --watch

What does the watcher do?

The executable reads static/src/input.css and writes compiled styles to static/css/app.css.

Watch mode keeps the terminal occupied. Future template changes trigger another Tailwind build.

The Tailwind terminal reports a completed build. The Explorer sidebar also shows the generated static/css/app.css file.

  • Refresh http://127.0.0.1:8000/ once more.

Your browser still receives a Django response while the other terminal reports a completed Tailwind build. Together, those results prove that the local stack is running.

One of the processes stopped?

  • Confirm that tailwindcss.exe sits beside manage.py.
  • Confirm that static/src/input.css contains the exact import shown above.
  • Confirm that the Django terminal begins with (.venv).

If either process still exits, ask for help diagnosing the local stack.

✔️ Awesome, I've got everything!

Great. Save every edited file and leave both Command Prompt terminals running.

ⓧ I'd like to double check the full code

Compare each hand-edited file with the cumulative project state below.

Django==6.1.1
mysqlclient==2.3.0
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = "your-django-secret-key-here"
DEBUG = True
ALLOWED_HOSTS = []

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

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "tricycle_portal.urls"

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

WSGI_APPLICATION = "tricycle_portal.wsgi.application"

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": "tricycle_portal",
        "USER": "tricycle_app",
        "PASSWORD": "your-strong-password",
        "HOST": "localhost",
    }
}

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True

STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]

MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("accreditation.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns = [
]
@import "tailwindcss";

The static/css/app.css file is generated by the running Tailwind watcher. It is not hand-edited.

Final checkpoint

  • The project root contains .venv with Django 6.1.1 and mysqlclient 2.3.0.
  • MySQL contains the tricycle_portal database with the utf8mb4 character set.
  • The local tricycle_app account uses your-strong-password and has all privileges on tricycle_portal.*.
  • The MySQL-backed Django authentication tables contain the local superuser.
  • The project root contains Tailwind CSS 4.3.3 as tailwindcss.exe.
  • The Tailwind watcher generates static/css/app.css from static/src/input.css.
  • The Django development server responds at http://127.0.0.1:8000/.

Your Windows development stack is ready. Next, you will turn this foundation into the first mobile-friendly driver application screen.

Build the Driver Application Screen

Your local Django site already responds through the MySQL-backed project. Now the portal needs a screen that a driver can use.

A visible form gives you the first usable slice of the workflow. Testing this screen reveals whether the interface can preserve an application.

In this step, get ready to:
  • Create a shared responsive layout.
  • Render a temporary driver application form.
  • Test the temporary form's submission behavior.
Create the shared Tailwind layout

A shared template gives every page the same navigation and content area. Tailwind CSS classes make that structure responsive without a separate handwritten stylesheet.

  • Create a templates folder inside tricycle-portal using the file tree in Visual Studio Code.
  • Create base.html inside the new templates folder using the file tree.
  • Build the shared page layout by adding this code to templates/base.html:
{% load static %}
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Tricycle Portal{% endblock %}</title>
    <link href="{% static 'css/app.css' %}" rel="stylesheet">
</head>
<body class="min-h-screen bg-slate-100 text-slate-900">
    <nav class="bg-emerald-800 text-white shadow">
        <div class="mx-auto flex max-w-5xl flex-wrap items-center gap-4 px-4 py-4">
            <a class="font-bold" href="{% url 'driver_apply' %}">Tricycle Portal</a>
            <a class="text-sm hover:underline" href="{% url 'driver_apply' %}">Apply</a>
            <a class="text-sm hover:underline" href="/drivers/">Approved drivers</a>
            <a class="text-sm hover:underline" href="/admin/">Admin</a>
        </div>
    </nav>
    <main class="mx-auto max-w-3xl px-4 py-8">
        {% block content %}{% endblock %}
    </main>
</body>
</html>

What does this layout do?

  • The {% load static %} tag lets the template load the compiled css/app.css file.
  • The navigation gives drivers access to the application page. It also points residents and administrators toward their separate areas.
  • The {% block content %} section gives each page a place to insert its own interface.
  • Save templates/base.html in Visual Studio Code.
  • Switch back to the Tailwind watcher terminal from earlier to confirm it reports a completed build.

The completed build confirms that Tailwind found the new layout classes and refreshed static/css/app.css.

Tailwind did not rebuild?

  • Confirm that base.html is saved inside the project-level templates folder.
  • Confirm that the Tailwind watcher terminal from the previous step is still running.

Ask for help with the watcher if the terminal remains idle: Why is my Tailwind watcher not rebuilding after I saved templates/base.html?

Render the temporary application form

A Django route connects the root browser address to a view. The view handles the GET request and renders the application template.

  • Create an accreditation folder inside templates using the Visual Studio Code file tree.
  • Create apply.html inside templates/accreditation using the file tree.
  • Build the temporary application form by adding this code to templates/accreditation/apply.html:
{% extends "base.html" %}

{% block title %}Driver Application{% endblock %}

{% block content %}
<section class="rounded-2xl bg-white p-6 shadow-sm sm:p-8">
    <p class="text-sm font-semibold uppercase tracking-wide text-emerald-700">Driver accreditation</p>
    <h1 class="mt-2 text-3xl font-bold">Submit a driver application</h1>
    <p class="mt-3 text-slate-600">Enter sample details and upload only a fake document while learning.</p>

    <form class="mt-8 space-y-5" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <div><label class="block text-sm font-medium" for="full_name">Full name</label><input class="mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2" id="full_name" name="full_name" type="text" required></div>
        <div><label class="block text-sm font-medium" for="contact_number">Contact number</label><input class="mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2" id="contact_number" name="contact_number" type="text" required></div>
        <div><label class="block text-sm font-medium" for="body_number">Body number</label><input class="mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2" id="body_number" name="body_number" type="text" required></div>
        <div><label class="block text-sm font-medium" for="affiliation">Affiliation</label><select class="mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2" id="affiliation" name="affiliation" required><option value="TODA">TODA member</option><option value="OPERATOR">Independent operator</option></select></div>
        <div><label class="block text-sm font-medium" for="toda_name">TODA name (optional)</label><input class="mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2" id="toda_name" name="toda_name" type="text"></div>
        <div><label class="block text-sm font-medium" for="license_file">Driver's license file</label><input class="mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2" id="license_file" name="license_file" type="file" accept=".jpg,.jpeg,.png,.pdf" required></div>
        <button class="w-full rounded-lg bg-emerald-700 px-4 py-3 font-semibold text-white hover:bg-emerald-800" type="submit">Submit application</button>
    </form>
</section>
{% endblock %}

What does this form include?

  • The template extends base.html so the form appears inside the shared navigation and content area.
  • The CSRF token protects the form submission using Django's built-in request protection.
  • The multipart/form-data encoding allows the browser to include the selected fake file in the request.
  • The input names match the accreditation details that the persistent workflow uses later.
  • Save templates/accreditation/apply.html in Visual Studio Code.
  • Switch back to the Tailwind watcher terminal to confirm it reports another completed build.

The new build adds the form's responsive spacing and input styles to the compiled stylesheet.

Tailwind ignored the form classes?

  • Confirm that the template path is exactly templates/accreditation/apply.html.
  • Confirm that the file is saved before checking the watcher terminal.

Get help checking the template path: Why is Tailwind not compiling the classes from my accreditation apply template?

  • Switch back to accreditation/views.py in the project from earlier.
  • Replace its contents with this temporary GET-only view:
from django.shortcuts import render


def apply(request):
    return render(request, "accreditation/apply.html")

What does this view do?

  • The apply() view receives the browser request for the application page.
  • The render() shortcut returns accreditation/apply.html as the response.
  • The view does not inspect submitted values or save a record.
  • Save accreditation/views.py in Visual Studio Code.
  • Switch back to the Django server terminal from earlier to confirm it reports reload activity without an error.

The reload confirms that Django can import the temporary view.

Django reports a view error?

  • Confirm that the import uses lowercase render.
  • Confirm that apply() is aligned at the start of its line.

Ask for help with the view syntax: Why can Django not import my temporary apply view?

  • Create urls.py inside the existing accreditation app using the Visual Studio Code file tree.
  • Connect the root path to the temporary view by adding this code to accreditation/urls.py:
from django.urls import path

from . import views

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

How does the route work?

  • The empty route matches the root address of the portal.
  • The route sends that request to views.apply.
  • The driver_apply name lets the base template create links back to the form.
  • Save accreditation/urls.py in Visual Studio Code.
  • Return to the browser tab from earlier.
  • Refresh http://127.0.0.1:8000/ to load the new screen.

You should see an emerald navigation bar above a white driver application card. The card contains six labeled fields and a full-width Submit application button.

The application page did not load?

  • Confirm that the Django development server from the previous step is still running.
  • Confirm that apply.html is inside templates/accreditation.
  • Confirm that accreditation/urls.py contains the route named driver_apply.

Get help tracing the request: Why does my Django root page fail to render the driver application template?

✔️ Awesome, I've got everything!

Your shared layout, temporary form, view, and root route are saved. The responsive application screen now loads in your browser.

ⓧ I'd like to double check the full code

Compare each file with the complete temporary screen below.

{% load static %}
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Tricycle Portal{% endblock %}</title>
    <link href="{% static 'css/app.css' %}" rel="stylesheet">
</head>
<body class="min-h-screen bg-slate-100 text-slate-900">
    <nav class="bg-emerald-800 text-white shadow">
        <div class="mx-auto flex max-w-5xl flex-wrap items-center gap-4 px-4 py-4">
            <a class="font-bold" href="{% url 'driver_apply' %}">Tricycle Portal</a>
            <a class="text-sm hover:underline" href="{% url 'driver_apply' %}">Apply</a>
            <a class="text-sm hover:underline" href="/drivers/">Approved drivers</a>
            <a class="text-sm hover:underline" href="/admin/">Admin</a>
        </div>
    </nav>
    <main class="mx-auto max-w-3xl px-4 py-8">
        {% block content %}{% endblock %}
    </main>
</body>
</html>
{% extends "base.html" %}

{% block title %}Driver Application{% endblock %}

{% block content %}
<section class="rounded-2xl bg-white p-6 shadow-sm sm:p-8">
    <p class="text-sm font-semibold uppercase tracking-wide text-emerald-700">Driver accreditation</p>
    <h1 class="mt-2 text-3xl font-bold">Submit a driver application</h1>
    <p class="mt-3 text-slate-600">Enter sample details and upload only a fake document while learning.</p>

    <form class="mt-8 space-y-5" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <div><label class="block text-sm font-medium" for="full_name">Full name</label><input class="mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2" id="full_name" name="full_name" type="text" required></div>
        <div><label class="block text-sm font-medium" for="contact_number">Contact number</label><input class="mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2" id="contact_number" name="contact_number" type="text" required></div>
        <div><label class="block text-sm font-medium" for="body_number">Body number</label><input class="mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2" id="body_number" name="body_number" type="text" required></div>
        <div><label class="block text-sm font-medium" for="affiliation">Affiliation</label><select class="mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2" id="affiliation" name="affiliation" required><option value="TODA">TODA member</option><option value="OPERATOR">Independent operator</option></select></div>
        <div><label class="block text-sm font-medium" for="toda_name">TODA name (optional)</label><input class="mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2" id="toda_name" name="toda_name" type="text"></div>
        <div><label class="block text-sm font-medium" for="license_file">Driver's license file</label><input class="mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2" id="license_file" name="license_file" type="file" accept=".jpg,.jpeg,.png,.pdf" required></div>
        <button class="w-full rounded-lg bg-emerald-700 px-4 py-3 font-semibold text-white hover:bg-emerald-800" type="submit">Submit application</button>
    </form>
</section>
{% endblock %}
from django.shortcuts import render


def apply(request):
    return render(request, "accreditation/apply.html")
from django.urls import path

from . import views

urlpatterns = [
    path("", views.apply, name="driver_apply"),
]
Test the temporary submission

The screen now looks ready to accept a driver application. This check tests what happens to the entered details after the browser sends them back to the view.

  • Enter Maria Santos in the Full name field.
  • Enter 09171234567 in the Contact number field.
  • Enter TRI-101 in the Body number field.
  • Select TODA member in the Affiliation field.
  • Enter Central TODA in the TODA name (optional) field.
  • Choose a fake .jpg, .jpeg, .png, or .pdf file in the Driver's license file field.

Before you submit, do you think the page can preserve these values with the current view?

  • Click Submit application.

The page reloads with empty fields. You receive no success page or reference number because the temporary view ignores the POST data.

That shortfall is intentional. You have proved that a polished interface still needs a data model and form workflow before it can preserve an application.

The browser did not submit the form?

  • Confirm that every required text field contains a value.
  • Confirm that the license field contains a fake sample file.
  • Confirm that the Django server terminal remains active.

Get help with the browser check: Why does my temporary driver application form stay on the page without submitting?

You now have a responsive driver application screen and clear evidence of the missing persistence layer. Next, you will save validated applications in MySQL and return a reference number to each driver.

Save Driver Applications and License Files

Your Tailwind CSS form looks ready for applicants. Its last submission proved that the browser loses every value because the portal has nowhere to store it.

This step gives Django a database-backed record and a ModelForm workflow. Valid applications move into MySQL. Fake license files move into local media storage.

In this step, get ready to:
  • Model the driver accreditation fields and migrate the new table.
  • Replace the temporary form with a ModelForm workflow.
  • Submit a fake application and confirm it persists in Django admin.
Model the accreditation record

A model defines the data that every application must preserve. Django uses this definition to create the corresponding table through its ORM.

  • In Visual Studio Code from earlier, replace the existing contents of accreditation/models.py with this first section:
from django.core.validators import FileExtensionValidator
from django.db import models


class DriverApplication(models.Model):
    AFFILIATION_CHOICES = [
        ("TODA", "TODA member"),
        ("OPERATOR", "Independent operator"),
    ]
    STATUS_CHOICES = [
        ("PENDING", "Pending"),
        ("APPROVED", "Approved"),
        ("REJECTED", "Rejected"),
    ]

    full_name = models.CharField(max_length=120)
    contact_number = models.CharField(max_length=20)
    body_number = models.CharField(max_length=30, unique=True)
    affiliation = models.CharField(max_length=20, choices=AFFILIATION_CHOICES)
    toda_name = models.CharField(max_length=120, blank=True)
    license_file = models.FileField(
        upload_to="driver_licenses/",
        validators=[FileExtensionValidator(["jpg", "jpeg", "png", "pdf"])],
    )

What does this model section do?

  • The choice lists store stable codes while showing readable labels in forms.
  • The unique body_number prevents two applications from using the same body number.
  • The license_file field stores uploaded files below media/driver_licenses/.
  • The validator accepts fake files with .jpg, .jpeg, .png, or .pdf extensions.
  • Add the remaining fields directly below the license_file field:
    status = models.CharField(
        max_length=10,
        choices=STATUS_CHOICES,
        default="PENDING",
    )
    submitted_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.full_name} ({self.body_number})"

How does the model track reviews?

  • The status field starts every application at PENDING.
  • The submitted_at field records when Django first saves the application.
  • The __str__() method gives each record a readable name inside Django admin.
  • Save accreditation/models.py using Visual Studio Code's file controls.

Before the migration runs, picture which table and columns you expect Django to create.

  • Create the model migration and apply it to MySQL by running these commands in the activated terminal:
py manage.py makemigrations accreditation
py manage.py migrate

What do these commands do?

  • The first command converts the model change into accreditation/migrations/0001_initial.py.
  • The second command applies that migration to the local tricycle_portal database.
  • Django generates the migration file from your model. You do not edit that generated file by hand.

The terminal reports that the initial accreditation migration was created and applied successfully. MySQL now contains the accreditation_driverapplication table.

Why extension checks are limited

An extension check confirms the filename ending. It does not prove what the file actually contains.

Use fake sample documents throughout this project. Production uploads need deeper content checks and stricter storage controls.

Migration not completing?

  • Confirm the terminal prompt shows the activated .venv environment.
  • Check that the MySQL service from earlier is still running.
  • Compare every bracket and indentation level in accreditation/models.py with the reference below.

Help me fix my DriverApplication migration.

Build the ModelForm workflow

The model provides storage. The ModelForm now turns those fields into validated inputs while keeping the Tailwind styling from your temporary screen.

  • Create forms.py inside the existing accreditation folder using Visual Studio Code's file controls.
  • Paste this first section into accreditation/forms.py to define the shared widget classes and form fields:
from django import forms

from .models import DriverApplication


INPUT_CLASSES = (
    "mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2 "
    "text-slate-900 shadow-sm outline-none focus:border-emerald-600 "
    "focus:ring-2 focus:ring-emerald-200"
)


class DriverApplicationForm(forms.ModelForm):
    class Meta:
        model = DriverApplication
        fields = [
            "full_name",
            "contact_number",
            "body_number",
            "affiliation",
            "toda_name",
            "license_file",
        ]

What does this form section do?

  • The INPUT_CLASSES value keeps one consistent Tailwind style across every widget.
  • The Meta class connects the form to DriverApplication.
  • The fields list controls which model values applicants can submit.
  • Add the labels and widget setup directly below the fields list:
        labels = {
            "toda_name": "TODA name (optional)",
            "license_file": "Driver's license file",
        }
        help_texts = {
            "license_file": "Use a fake JPG, JPEG, PNG, or PDF for this tutorial.",
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs.update({"class": INPUT_CLASSES})
        self.fields["license_file"].widget.attrs.update(
            {"accept": ".jpg,.jpeg,.png,.pdf"}
        )

How are the widgets configured?

  • The labels make the optional field and fake upload purpose clear.
  • The initializer applies INPUT_CLASSES to every generated widget.
  • The accept value helps the browser show the four tutorial file extensions in its file picker.
  • Save accreditation/forms.py using Visual Studio Code's file controls.
  • Switch back to the Django server terminal from earlier to confirm it reloads without a Python error.

The server reload confirms that Django can import the new form module.

Server showing a form import problem?

  • Confirm the file is named forms.py inside the accreditation folder.
  • Check that DriverApplication uses the same spelling in models.py and forms.py.
  • Compare the indentation below class Meta and def __init__.

Help me troubleshoot my ModelForm import.

  • Replace the existing contents of accreditation/views.py with the POST-aware views below:
from django.shortcuts import redirect, render

from .forms import DriverApplicationForm
from .models import DriverApplication


def apply(request):
    if request.method == "POST":
        form = DriverApplicationForm(request.POST, request.FILES)
        if form.is_valid():
            application = form.save()
            return redirect("application_success", reference=application.pk)
    else:
        form = DriverApplicationForm()

    return render(request, "accreditation/apply.html", {"form": form})


def application_success(request, reference):
    return render(
        request,
        "accreditation/application_success.html",
        {"reference": reference},
    )

How does the submission flow work?

  • A GET request creates an empty DriverApplicationForm for the page.
  • A POST request binds the typed values from request.POST.
  • The uploaded fake document comes from request.FILES.
  • A valid form saves the application before redirecting with its numeric primary key.
  • Save accreditation/views.py using Visual Studio Code's file controls.
  • Return to the Django server terminal from earlier to confirm it reloads without a Python error.
  • Replace the existing contents of accreditation/urls.py with the application and success routes below:
from django.urls import path

from . import views

urlpatterns = [
    path("", views.apply, name="driver_apply"),
    path(
        "success/<int:reference>/",
        views.application_success,
        name="application_success",
    ),
]

How does the success route work?

The root route still displays the application form. The second route captures a numeric reference before passing it to application_success.

  • Save accreditation/urls.py using Visual Studio Code's file controls.
  • Return to the Django server terminal from earlier to confirm the URL module reloads without a Python error.

The backend can now validate and save submissions. The temporary HTML fields still need to hand control to the ModelForm.

  • Replace the existing contents of templates/accreditation/apply.html with this opening section:
{% extends "base.html" %}

{% block title %}Driver Application{% endblock %}

{% block content %}
<section class="rounded-2xl bg-white p-6 shadow-sm sm:p-8">
    <p class="text-sm font-semibold uppercase tracking-wide text-emerald-700">Driver accreditation</p>
    <h1 class="mt-2 text-3xl font-bold">Submit a driver application</h1>
    <p class="mt-3 text-slate-600">
        Enter sample details and upload only a fake document while learning.
    </p>

    <form class="mt-8 space-y-5" method="post" enctype="multipart/form-data">
        {% csrf_token %}

What does this template section set up?

  • The template keeps the shared layout and responsive application card.
  • The POST method sends the entered values to the updated apply view.
  • The multipart encoding allows the fake license file to reach request.FILES.
  • The CSRF token protects the form submission.
  • Add the generated fields and submit button directly below the CSRF token:
        {% for error in form.non_field_errors %}
            <p class="rounded-lg bg-red-50 p-3 text-sm text-red-700">{{ error }}</p>
        {% endfor %}

        {% for field in form %}
            <div>
                <label class="block text-sm font-medium" for="{{ field.id_for_label }}">
                    {{ field.label }}
                </label>
                {{ field }}
                {% if field.help_text %}
                    <p class="mt-1 text-xs text-slate-500">{{ field.help_text }}</p>
                {% endif %}
                {% for error in field.errors %}
                    <p class="mt-1 text-sm text-red-700">{{ error }}</p>
                {% endfor %}
            </div>
        {% endfor %}

        <button class="w-full rounded-lg bg-emerald-700 px-4 py-3 font-semibold text-white hover:bg-emerald-800" type="submit">
            Submit application
        </button>
    </form>
</section>
{% endblock %}

How does the form render feedback?

  • The first loop displays errors that apply to the whole form.
  • The second loop renders each ModelForm label and widget.
  • Help text appears beside the fake file input.
  • Field-specific validation messages appear beside the input that needs attention.
  • Save templates/accreditation/apply.html using Visual Studio Code's file controls.
  • Refresh the existing form page at http://127.0.0.1:8000/.

You should see the same six inputs with consistent Tailwind styling. The fake license input now shows its tutorial help text.

Form missing its fields?

  • Confirm the view passes form to accreditation/apply.html.
  • Check that the template loop uses the same form name.
  • Confirm the Django server terminal is still running.

Help me restore the generated ModelForm fields.

  • Create application_success.html inside templates/accreditation using Visual Studio Code's file controls.
  • Paste the success screen into templates/accreditation/application_success.html below:
{% extends "base.html" %}

{% block title %}Application Received{% endblock %}

{% block content %}
<section class="rounded-2xl bg-white p-8 text-center shadow-sm">
    <p class="text-sm font-semibold uppercase tracking-wide text-emerald-700">Application received</p>
    <h1 class="mt-2 text-3xl font-bold">Your application is pending review</h1>
    <p class="mt-4 text-slate-600">Keep this reference number for the Secret Mission status tracker.</p>
    <p class="mt-6 text-4xl font-black text-emerald-800">{{ reference }}</p>
    <a class="mt-8 inline-block rounded-lg bg-slate-900 px-5 py-3 font-semibold text-white" href="{% url 'driver_apply' %}">
        Submit another sample
    </a>
</section>
{% endblock %}

What does the success screen prove?

  • The heading confirms that the application reached the saved state.
  • The reference value displays the new record's numeric primary key.
  • The link returns the learner to a fresh application form.
  • Save templates/accreditation/application_success.html using Visual Studio Code's file controls.
  • Return to the Django server terminal from earlier to confirm the new template causes no reload error.
Prove the submission persisted

A success page proves that the request reached the redirect. Django admin gives you a second view of the saved row and its uploaded fake document.

  • Replace the existing contents of accreditation/admin.py with the temporary model registration below:
from django.contrib import admin

from .models import DriverApplication


admin.site.register(DriverApplication)

What does this registration do?

The registration adds DriverApplication to the existing admin site. You can now inspect saved records without building a separate review screen.

  • Save accreditation/admin.py using Visual Studio Code's file controls.
  • Return to the application form at http://127.0.0.1:8000/.
  • Enter Jamie Cruz in the full name field.
  • Enter 09170000000 in the contact number field.
  • Enter TRI-001 in the body number field.
  • Select TODA member for the affiliation.
  • Enter Central TODA in the optional TODA name field.
  • Select a fake .jpg, .jpeg, .png, or .pdf sample from your device.

Before you submit, predict whether this version keeps the data after the redirect.

  • Click Submit application.

You should reach the application received screen. A numeric reference appears below the pending review message.

  • Record the displayed reference number here: your application reference number.
  • Select Admin in the navigation.
  • Use the Django superuser from earlier if the admin sign-in form appears.
  • Select the Driver applications model under the accreditation section.
  • Select the Jamie Cruz record.
  • Select the uploaded file link to confirm the fake sample opens.
  • Return to the driver application record in Django admin.

Before you refresh, predict whether the row and file link will still be there.

  • Refresh the driver application record in your browser.

You should still see the application with a Pending status and an uploaded file link. The refresh proves that MySQL preserved the record while local media storage preserved the fake file.

Submission not reaching the success page?

  • Check each field for a validation message beside its input.
  • Use a new body number if TRI-001 already belongs to an earlier test record.
  • Confirm the selected fake file uses one of the four accepted extensions.

Help me debug my saved application workflow.

Use this final comparison if you want to check every file changed in this step.

✔️ Awesome, I've got everything!

Your model, form workflow, success screen, and admin registration are ready for the next step.

ⓧ I'd like to double check the full code

Compare accreditation/models.py:

from django.core.validators import FileExtensionValidator
from django.db import models


class DriverApplication(models.Model):
    AFFILIATION_CHOICES = [
        ("TODA", "TODA member"),
        ("OPERATOR", "Independent operator"),
    ]
    STATUS_CHOICES = [
        ("PENDING", "Pending"),
        ("APPROVED", "Approved"),
        ("REJECTED", "Rejected"),
    ]

    full_name = models.CharField(max_length=120)
    contact_number = models.CharField(max_length=20)
    body_number = models.CharField(max_length=30, unique=True)
    affiliation = models.CharField(max_length=20, choices=AFFILIATION_CHOICES)
    toda_name = models.CharField(max_length=120, blank=True)
    license_file = models.FileField(
        upload_to="driver_licenses/",
        validators=[FileExtensionValidator(["jpg", "jpeg", "png", "pdf"])],
    )
    status = models.CharField(
        max_length=10,
        choices=STATUS_CHOICES,
        default="PENDING",
    )
    submitted_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.full_name} ({self.body_number})"

Compare accreditation/forms.py:

from django import forms

from .models import DriverApplication


INPUT_CLASSES = (
    "mt-1 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2 "
    "text-slate-900 shadow-sm outline-none focus:border-emerald-600 "
    "focus:ring-2 focus:ring-emerald-200"
)


class DriverApplicationForm(forms.ModelForm):
    class Meta:
        model = DriverApplication
        fields = [
            "full_name",
            "contact_number",
            "body_number",
            "affiliation",
            "toda_name",
            "license_file",
        ]
        labels = {
            "toda_name": "TODA name (optional)",
            "license_file": "Driver's license file",
        }
        help_texts = {
            "license_file": "Use a fake JPG, JPEG, PNG, or PDF for this tutorial.",
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs.update({"class": INPUT_CLASSES})
        self.fields["license_file"].widget.attrs.update(
            {"accept": ".jpg,.jpeg,.png,.pdf"}
        )

Compare accreditation/views.py:

from django.shortcuts import redirect, render

from .forms import DriverApplicationForm
from .models import DriverApplication


def apply(request):
    if request.method == "POST":
        form = DriverApplicationForm(request.POST, request.FILES)
        if form.is_valid():
            application = form.save()
            return redirect("application_success", reference=application.pk)
    else:
        form = DriverApplicationForm()

    return render(request, "accreditation/apply.html", {"form": form})


def application_success(request, reference):
    return render(
        request,
        "accreditation/application_success.html",
        {"reference": reference},
    )


def approved_drivers(request):
    drivers = DriverApplication.objects.filter(status="APPROVED").order_by(
        "body_number", "full_name"
    )
    return render(
        request,
        "accreditation/approved_drivers.html",
        {"drivers": drivers},
    )

Compare accreditation/urls.py:

from django.urls import path

from . import views

urlpatterns = [
    path("", views.apply, name="driver_apply"),
    path(
        "success/<int:reference>/",
        views.application_success,
        name="application_success",
    ),
    path("drivers/", views.approved_drivers, name="approved_drivers"),
]

Compare accreditation/admin.py:

from django.contrib import admin

from .models import DriverApplication


admin.site.register(DriverApplication)

Compare templates/accreditation/apply.html:

{% extends "base.html" %}

{% block title %}Driver Application{% endblock %}

{% block content %}
<section class="rounded-2xl bg-white p-6 shadow-sm sm:p-8">
    <p class="text-sm font-semibold uppercase tracking-wide text-emerald-700">Driver accreditation</p>
    <h1 class="mt-2 text-3xl font-bold">Submit a driver application</h1>
    <p class="mt-3 text-slate-600">
        Enter sample details and upload only a fake document while learning.
    </p>

    <form class="mt-8 space-y-5" method="post" enctype="multipart/form-data">
        {% csrf_token %}

        {% for error in form.non_field_errors %}
            <p class="rounded-lg bg-red-50 p-3 text-sm text-red-700">{{ error }}</p>
        {% endfor %}

        {% for field in form %}
            <div>
                <label class="block text-sm font-medium" for="{{ field.id_for_label }}">
                    {{ field.label }}
                </label>
                {{ field }}
                {% if field.help_text %}
                    <p class="mt-1 text-xs text-slate-500">{{ field.help_text }}</p>
                {% endif %}
                {% for error in field.errors %}
                    <p class="mt-1 text-sm text-red-700">{{ error }}</p>
                {% endfor %}
            </div>
        {% endfor %}

        <button class="w-full rounded-lg bg-emerald-700 px-4 py-3 font-semibold text-white hover:bg-emerald-800" type="submit">
            Submit application
        </button>
    </form>
</section>
{% endblock %}

Compare templates/accreditation/application_success.html:

{% extends "base.html" %}

{% block title %}Application Received{% endblock %}

{% block content %}
<section class="rounded-2xl bg-white p-8 text-center shadow-sm">
    <p class="text-sm font-semibold uppercase tracking-wide text-emerald-700">Application received</p>
    <h1 class="mt-2 text-3xl font-bold">Your application is pending review</h1>
    <p class="mt-4 text-slate-600">Keep this reference number for the Secret Mission status tracker.</p>
    <p class="mt-6 text-4xl font-black text-emerald-800">{{ reference }}</p>
    <a class="mt-8 inline-block rounded-lg bg-slate-900 px-5 py-3 font-semibold text-white" href="{% url 'driver_apply' %}">
        Submit another sample
    </a>
</section>
{% endblock %}

You have closed the persistence loop. Form values now survive the redirect while fake uploads remain available for review.

Next, you will turn the basic admin registration into a focused approval workspace.

Review Applications in Django Admin

Your first driver application now survives a refresh in MySQL. A saved application still needs a deliberate review before residents can trust it.

Django admin gives you a purpose-built review surface for that decision.

You will shape its application list into a queue for finding records. You will approve one sample while keeping another unpublished.

In this step, get ready to:
  • Replace the basic model registration with a custom application review list.
  • Use review controls to locate applications before inspecting their fake uploads.
  • Approve one sample while keeping another sample unpublished.
Build the admin review list

A custom ModelAdmin class controls how applications appear in the review queue. It also limits inline editing to the decision field.

  • Switch back to Visual Studio Code from earlier.
  • Select accreditation/admin.py in the Explorer sidebar.
  • Replace the contents of accreditation/admin.py with the code below:
from django.contrib import admin

from .models import DriverApplication


@admin.register(DriverApplication)
class DriverApplicationAdmin(admin.ModelAdmin):
    list_display = (
        "full_name",
        "body_number",
        "affiliation",
        "status",
        "submitted_at",
    )
    list_editable = ("status",)
    list_filter = ("status", "affiliation")
    search_fields = ("full_name", "body_number", "toda_name")

What does this admin class do?

  • The @admin.register(DriverApplication) decorator connects the model to its custom admin class.
  • The list_display setting places the applicant name, body number, affiliation, status, and submission time in the review list.
  • The list_editable setting makes only status editable from the list.
  • The list_filter setting narrows the queue by status or affiliation.
  • The search_fields setting finds records by full name, body number, or TODA name.
  • Save accreditation/admin.py by pressing Ctrl+S.
  • Return to the browser tab showing Django admin from earlier.
  • Refresh the Driver applications list.

You will see columns for full name, body number, affiliation, status, and submission time. Only the status column has editable dropdowns.

Good progress: your saved applications now sit in a review queue with a clear status control.

✔️ Awesome, I've got everything!

Your custom review list is saved. Confirm the five columns appear before moving on.

ⓧ I'd like to double check the full code

Compare your complete accreditation/admin.py file with this version:

from django.contrib import admin

from .models import DriverApplication


@admin.register(DriverApplication)
class DriverApplicationAdmin(admin.ModelAdmin):
    list_display = (
        "full_name",
        "body_number",
        "affiliation",
        "status",
        "submitted_at",
    )
    list_editable = ("status",)
    list_filter = ("status", "affiliation")
    search_fields = ("full_name", "body_number", "toda_name")

Admin list still looks basic?

  • Confirm you saved accreditation/admin.py inside the accreditation folder.
  • Confirm the old basic registration line is no longer in the file.
  • Check the Django server terminal from earlier for a Python traceback caused by mismatched brackets or indentation.

Help me troubleshoot my Django admin configuration.

Find and inspect an application

Filters narrow a busy review queue to one category. Search takes you directly to a known applicant.

  • Return to the Driver applications list in Django admin.
  • Select Pending under the Status filter in the right sidebar.
  • Enter the body number from your first fake sample in the search box.
  • Click Search.
  • Click the matching applicant name in the result row.
  • Review the stored applicant fields on the change page.
  • Select the linked fake license filename to inspect the uploaded sample.
  • Use your browser's Back button to return to the application record.

You will see the submitted applicant details alongside a link to the fake document. This proves the saved database record points to the corresponding file under local media storage.

Cannot find the sample application?

  • Clear the search box before trying the body number again.
  • Select All under the Status filter if the sample no longer has Pending status.
  • Compare the search value with the body number shown on the application success flow.

Help me find my saved application in Django admin.

Approve one sample and keep another unpublished

The status field acts as the publication decision for each application. The next step uses the approved value to decide which drivers residents can see.

  • Return to the Driver applications list.
  • Select Approved from the first sample's Status dropdown.
  • Click Save.

After the page refreshes, the first sample's status reads Approved.

  • Switch to the portal browser tab from earlier.
  • Select Apply in the portal navigation.
  • Complete the form with different fake application details.
  • Attach another fake JPG, JPEG, PNG, or PDF file.
  • Click Submit application.

You will see the success page with a new numeric reference number.

  • Return to the Django admin tab from earlier.
  • Refresh the Driver applications list.
  • Leave the second sample's status set to Pending.

Before you check the queue, which status do you expect to see beside each sample?

  • Enter the first sample's body number in the admin search box.
  • Click Search.

You will see the first sample as a single matching row with Approved in the status column.

  • Clear the admin search box.
  • Click Search.
  • Select Pending under the Status filter.

You will see the second sample in the filtered list. The approved sample stays outside this Pending view.

  • Select All under the Status filter.

You should now see both samples in the review queue with different statuses. That is the approval workflow working from submission to decision.

Status did not stay changed?

  • Return to the first sample's row.
  • Select Approved from its status dropdown again.
  • Click Save before refreshing the page.

Help me fix an application status that is not saving in Django admin.

Your admin workflow can now separate reviewed drivers from applications that remain unpublished. Next, you will turn that decision into a public directory for residents.

Publish the Approved Driver Directory

Your Django admin review workflow now gives administrators control over every application status. That decision still lives behind the private review screen.

Residents need a trustworthy public view that contains approved drivers only. A filtered Django ORM query applies that publication rule before the template receives any records.

In this step, get ready to:
  • Query approved applications in a consistent order.
  • Display approved drivers in a responsive public directory.
  • Change an application status to test the publication rule.
Query approved records

Filtering at the query layer keeps unapproved applications out of the template. Ordering by body number first makes the directory predictable after every refresh.

  • In the Explorer sidebar in Visual Studio Code, select accreditation/views.py.
  • Add this function below application_success():
def approved_drivers(request):
    drivers = DriverApplication.objects.filter(status="APPROVED").order_by(
        "body_number", "full_name"
    )
    return render(
        request,
        "accreditation/approved_drivers.html",
        {"drivers": drivers},
    )

What Does This View Do?

  • The filter(status="APPROVED") lookup returns applications whose saved status is approved.
  • The order_by("body_number", "full_name") call sorts matching records by body number. Full name resolves records with matching body numbers.
  • The render() call passes the filtered records to the directory template as drivers.
  • Save accreditation/views.py by pressing Ctrl+S.
  • Switch back to the Django server Command Prompt from earlier.

The development server reloads the project without reporting a Python exception. This confirms that Django can import the new view.

✔️ Awesome, I've got everything!

Your approved-driver query is saved. The Django server can load the updated view.

ⓧ I'd like to double check the full code

Compare your complete accreditation/views.py file with this version:

from django.shortcuts import redirect, render

from .forms import DriverApplicationForm
from .models import DriverApplication


def apply(request):
    if request.method == "POST":
        form = DriverApplicationForm(request.POST, request.FILES)
        if form.is_valid():
            application = form.save()
            return redirect("application_success", reference=application.pk)
    else:
        form = DriverApplicationForm()

    return render(request, "accreditation/apply.html", {"form": form})


def application_success(request, reference):
    return render(
        request,
        "accreditation/application_success.html",
        {"reference": reference},
    )


def approved_drivers(request):
    drivers = DriverApplication.objects.filter(status="APPROVED").order_by(
        "body_number", "full_name"
    )
    return render(
        request,
        "accreditation/approved_drivers.html",
        {"drivers": drivers},
    )

What Should Match?

The existing form and success views remain unchanged. The new approved_drivers() function sits at the bottom of the file.

Seeing a Reload Error?

  • Check that approved_drivers() starts at the left edge of accreditation/views.py.
  • Confirm that the existing DriverApplication import remains near the top of the file.

Help me fix the approved-driver view.

The view now defines the directory response. A URL pattern gives the browser a route that can call it.

  • Select accreditation/urls.py in the Visual Studio Code Explorer sidebar.
  • Replace the file contents with this code:
from django.urls import path

from . import views

urlpatterns = [
    path("", views.apply, name="driver_apply"),
    path(
        "success/<int:reference>/",
        views.application_success,
        name="application_success",
    ),
    path("drivers/", views.approved_drivers, name="approved_drivers"),
]

How Does the Route Work?

  • The drivers/ pattern sends matching requests to approved_drivers().
  • The approved_drivers route name matches the existing navigation link in templates/base.html.
  • Save accreditation/urls.py by pressing Ctrl+S.
  • Return to the Django server Command Prompt from earlier.

The server reloads without reporting a URL configuration exception. The directory route is now connected to its view.

✔️ Awesome, I've got everything!

Your application routes now include the public driver directory.

ⓧ I'd like to double check the full code

Compare your complete accreditation/urls.py file with this version:

from django.urls import path

from . import views

urlpatterns = [
    path("", views.apply, name="driver_apply"),
    path(
        "success/<int:reference>/",
        views.application_success,
        name="application_success",
    ),
    path("drivers/", views.approved_drivers, name="approved_drivers"),
]

What Should Match?

The root and success routes remain in place. The final pattern connects drivers/ to the approved-driver view.

Server Rejecting the URL Configuration?

  • Check that the new path() entry sits inside urlpatterns.
  • Confirm that the entry ends with a comma.
  • Match the view name to approved_drivers exactly.

Help me repair the directory route.

Build the Tailwind directory

The template turns each approved record into a responsive card styled with Tailwind CSS. Its limited field selection keeps contact numbers and uploaded files away from the public page.

  • Expand the templates/accreditation folder in the Visual Studio Code Explorer sidebar.
  • Click the New File icon beside the folder name.
  • Enter approved_drivers.html as the file name.
  • Press Enter.
  • Paste this template into templates/accreditation/approved_drivers.html:
{% extends "base.html" %}

{% block title %}Approved Drivers{% endblock %}

{% block content %}
<header>
    <p class="text-sm font-semibold uppercase tracking-wide text-emerald-700">Public directory</p>
    <h1 class="mt-2 text-3xl font-bold">Approved drivers</h1>
    <p class="mt-3 text-slate-600">Only applications approved by an administrator appear here.</p>
</header>

<div class="mt-8 grid gap-4 sm:grid-cols-2">
    {% for driver in drivers %}
        <article class="rounded-2xl bg-white p-5 shadow-sm">
            <p class="text-xs font-semibold uppercase tracking-wide text-emerald-700">Body {{ driver.body_number }}</p>
            <h2 class="mt-2 text-xl font-bold">{{ driver.full_name }}</h2>
            <p class="mt-2 text-sm text-slate-600">{{ driver.get_affiliation_display }}</p>
            {% if driver.toda_name %}
                <p class="mt-1 text-sm text-slate-600">{{ driver.toda_name }}</p>
            {% endif %}
        </article>
    {% empty %}
        <p class="rounded-2xl bg-white p-6 text-slate-600 shadow-sm">No drivers have been approved yet.</p>
    {% endfor %}
</div>
{% endblock %}

What Does This Template Show?

  • The for loop creates one card for every record supplied through drivers.
  • Each card displays the body number and full name.
  • The get_affiliation_display value shows the readable affiliation label.
  • The if block includes the TODA name only when the application contains one.
  • The empty block explains when the query returns no approved drivers.
  • Save templates/accreditation/approved_drivers.html by pressing Ctrl+S.
  • Switch back to the Tailwind watcher Command Prompt from earlier.

The Tailwind watcher reports a completed build after scanning the new template. Your compiled stylesheet now includes the directory classes.

Before you open the route, consider which application records you expect to become cards.

  • Open http://127.0.0.1:8000/drivers/ in your browser.

You should see a card for the approved driver. The pending or rejected sample stays absent from the grid.

Each visible card contains the driver's full name, body number, affiliation, and optional TODA name. Contact numbers and fake license links stay out of the directory.

✔️ Awesome, I've got everything!

Your directory shows approved drivers as responsive cards. Private application fields remain hidden.

ⓧ I'd like to double check the full code

Compare your complete templates/accreditation/approved_drivers.html file with this version:

{% extends "base.html" %}

{% block title %}Approved Drivers{% endblock %}

{% block content %}
<header>
    <p class="text-sm font-semibold uppercase tracking-wide text-emerald-700">Public directory</p>
    <h1 class="mt-2 text-3xl font-bold">Approved drivers</h1>
    <p class="mt-3 text-slate-600">Only applications approved by an administrator appear here.</p>
</header>

<div class="mt-8 grid gap-4 sm:grid-cols-2">
    {% for driver in drivers %}
        <article class="rounded-2xl bg-white p-5 shadow-sm">
            <p class="text-xs font-semibold uppercase tracking-wide text-emerald-700">Body {{ driver.body_number }}</p>
            <h2 class="mt-2 text-xl font-bold">{{ driver.full_name }}</h2>
            <p class="mt-2 text-sm text-slate-600">{{ driver.get_affiliation_display }}</p>
            {% if driver.toda_name %}
                <p class="mt-1 text-sm text-slate-600">{{ driver.toda_name }}</p>
            {% endif %}
        </article>
    {% empty %}
        <p class="rounded-2xl bg-white p-6 text-slate-600 shadow-sm">No drivers have been approved yet.</p>
    {% endfor %}
</div>
{% endblock %}

What Should Match?

The template extends base.html before rendering the heading and card grid. No template expression references contact_number or license_file.

Directory Page Not Loading?

  • Confirm that the template is saved at templates/accreditation/approved_drivers.html.
  • Check the Django server Command Prompt for a template syntax problem.
  • Confirm that the Tailwind watcher remains active if the page loads without its card styling.

Help me fix the approved-driver directory.

Test the publication rule

Status-based filtering links public visibility directly to the administrator's decision. Changing one saved status gives you immediate proof that the directory enforces this rule.

  • Switch back to the Django admin change list from earlier.
  • Set the approved sample's Status dropdown to Pending.
  • Click Save at the bottom of the change list.

Before you refresh the directory, consider whether that driver's card will remain visible.

  • Switch back to the public directory browser tab.
  • Refresh http://127.0.0.1:8000/drivers/.

The driver's card disappears from the directory. The saved record still exists in Django admin with its updated status.

  • Return to the Django admin change list.
  • Set the same sample's Status dropdown to Approved.
  • Click Save at the bottom of the change list.

Before the final refresh, consider whether the same driver's card will return.

  • Switch back to the public directory browser tab.
  • Refresh http://127.0.0.1:8000/drivers/.

The approved driver's card returns. Any pending or rejected sample remains hidden.

Status Change Not Reaching the Directory?

  • Confirm that you clicked Save after changing the admin status.
  • Check that the saved status reads Approved in the admin row.
  • Refresh the public directory after each saved change.

Help me trace the status visibility rule.

Strong work. Your approval-controlled directory is now public without exposing private application data.

Secret mission

Add a Private Application Status Tracker

Applicants have a reference number, but they still need a private way to check their decision. Build a status tracker that reveals an application only when its reference number matches its body number.

Clean Up Your Resources

Clean Up Your Resources

Everything you built stays on your Windows machine, so there are no ongoing service charges. Choose whether to keep your resources running, pause your work, or delete the project entirely.

Resources you used:

  • A local MySQL database named tricycle_portal containing the project records.
  • A local MySQL account named 'tricycle_app'@'localhost'.
  • A local Django development server.
  • A local Tailwind CSS watcher.
  • The complete tricycle-portal project folder.
  • A Python virtual environment stored in .venv.
  • The tailwindcss.exe executable with the generated static/css/app.css stylesheet.
  • Fake license uploads stored under media/driver_licenses/.

Keep everything running

No action needed. Choose this if you are still testing applications, approval decisions, or private status lookups.

The Django server continues using local memory while its terminal is active. The Tailwind watcher does the same.

Your saved data remains available. Keeping these local resources creates no service charge.

Pause - I'll come back to this later

Shut down the running processes to free local memory. Your database and project files stay in place.

  • Return to the Command Prompt terminal running the Tailwind watcher.
  • Press Ctrl+C to stop the Tailwind watcher.
  • Return to the Command Prompt terminal running the Django development server.
  • Press Ctrl+C to stop the Django development server.
  • Close Visual Studio Code.
  • Close the Command Prompt window used for the Tailwind watcher.
  • Close the Command Prompt window used for the Django server.

Your MySQL database remains available. The complete project stays inside the tricycle-portal folder.

Delete - I don't want to use this again

Remove all project resources and start fresh. Deletion is final, so the commands below target only the database, local account, and folder created for this project.

  • Return to the Command Prompt terminal running the Tailwind watcher.
  • Press Ctrl+C to stop the Tailwind watcher.
  • Return to the Command Prompt terminal running the Django development server.
  • Press Ctrl+C to stop the Django development server.

The next command asks for your MySQL administrator password after it starts. The password remains outside the command itself.

  • Sign in to MySQL as an administrator by running this command:
mysql -u root -p

What Does This Command Do?

This opens the existing MySQL administrator session used during setup. The following cleanup commands run inside that session.

  • Delete the tricycle_portal database by running this command:
DROP DATABASE IF EXISTS tricycle_portal;

What Gets Removed?

This removes every Django table stored in tricycle_portal. Your saved applications disappear with the local Django superuser record.

  • Delete the local MySQL account by running this command:
DROP USER IF EXISTS 'tricycle_app'@'localhost';

What Happens to MySQL?

This removes only the tricycle_app account at localhost. MySQL Community Server stays installed for other projects.

  • Close the Command Prompt window signed in to MySQL.
  • Close Visual Studio Code.
  • Return to the Command Prompt terminal that ran the Tailwind watcher.
  • Delete the tricycle-portal folder by running these commands:
cd ..
rmdir /s /q tricycle-portal

What Gets Deleted Locally?

The first line moves Command Prompt to the folder above your project. The second line permanently removes the project folder with its virtual environment, source files, compiled stylesheet, executable, migrations, and fake uploads.

  • Close the remaining Command Prompt window.

Cleanup complete. The project database, local account, source files, and fake uploads are gone.

Nice Work!

Nice Work!

You made it! Your local driver accreditation workflow now carries each sample application from submission to controlled publication.

You've learned how to:

  • Built a mobile-friendly application flow with Tailwind CSS. A ModelForm now stores sample driver details. Each accepted submission saves a fake license file. It also returns a numeric reference.
  • Connected the Django ORM to a dedicated MySQL database for persistent application records. You shaped Django admin into a searchable review surface with filters and inline status updates.
  • Published an approved-driver directory that hides Pending and Rejected applications. Residents can see useful driver details without seeing contact numbers or uploaded documents.
  • Completed the optional Secret Mission by adding a private status tracker. It reveals an applicant's name and status only when the reference number and body number match the same saved application.

Ready to quiz yourself?