Recruitment Finder: Data Models

Build a Django job board with automatic deadline classification and verification badges.

Introduction

30 Second Summary

Every year, hundreds of thousands of workers search for legitimate overseas jobs. Sorting through scattered listings with no way to tell which ones are expired, verified, or even real is overwhelming.

In this project, you will build a Django-powered recruitment intelligence platform that models overseas job data with enforced source traceability and automatic deadline classification. Every listing traces back to its original source, and color-coded badges show whether a job is open, closing soon, or expired.

What You'll Build

When you open the finished site, you see a list of overseas job opportunities with color-coded deadline badges (green for Open, orange for Closing Soon, red for Expired) and verification status indicators, each traceable back to its original government or employer announcement.

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

  • A customized Django admin dashboard where you can add job listings, search across your data, and filter by country and verification status.
  • A public-facing job listings page that displays every listing's employer, destination country, salary range, vacancies, deadline, and a link to the original announcement.
  • A deadline engine that automatically classifies every listing in real time. A job posted yesterday with a deadline next week shows "Closing Soon" in orange. One from last month with a passed deadline shows "Expired" in red.
  • Secret Mission: Add country-wise pages so users can browse jobs by destination country, with open listing counts and the same deadline and verification badges.

Are there any prerequisites?

You need Python 3.12 or higher and a code editor (like VS Code) installed on your machine. Django and all other tools are installed during the project.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Django and Scaffold the Project

Before you can model recruitment data or build any pages, you need a working Django project on your machine. Without it, there is no database, no admin panel, and no server to run.

In this step, you will create a project folder with its own Python virtual environment, install Django, scaffold the project skeleton, and confirm everything runs by visiting the Django welcome page in your browser.

In this step, get ready to:
  • Create a project folder with a Python virtual environment and install Django.
  • Scaffold the Django project and create the jobs app.
  • Register the app in settings and confirm the development server runs.
Create a project folder and install Django

Every Python project should use its own virtual environment. A virtual environment isolates your project's packages from the rest of your system, so installing Django here will not affect anything else on your machine.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop, create a project folder, and set up a virtual environment by running the commands for your operating system:

macOS

cd ~/Desktop
mkdir recruitment-finder
cd recruitment-finder
python3 -m venv venv
source venv/bin/activate

Windows

cd ~\Desktop
mkdir recruitment-finder
cd recruitment-finder
python -m venv venv
venv\Scripts\activate

What do these commands do?

  • cd ~/Desktop moves your terminal to the Desktop so your project folder is easy to find.
  • mkdir recruitment-finder creates a new folder for the project.
  • python3 -m venv venv creates a virtual environment inside a folder called venv.
  • source venv/bin/activate (or the Windows equivalent) activates the virtual environment so any packages you install stay inside this project.

Your terminal prompt now starts with (venv). This confirms the virtual environment is active.

Don't see (venv) in your prompt?

  • On macOS, make sure you typed source venv/bin/activate with a space after source.
  • On Windows, make sure you typed venv\Scripts\activate with backslashes, not forward slashes.
  • If PowerShell shows an execution policy error, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned and try the activation command again.

Still stuck? Help me activate my Python virtual environment.

  • Install Django by running this command:
pip install Django==5.2.17
  • Verify the installation by running:
django-admin --version

You should see 5.2.17 in the output.

Seeing command not found?

  • Make sure your virtual environment is active. Your prompt should start with (venv).
  • If you closed and reopened your terminal, you need to activate the virtual environment again before running Django commands.

Need help? Help me install Django in my virtual environment.

Scaffold the Django project and create the jobs app

Django's django-admin tool generates the project skeleton for you. The dot at the end of the command tells Django to create the project files in your current folder instead of making an extra nested folder.

  • Create the Django project and the jobs app by running these commands:
django-admin startproject recruitment_finder .
python manage.py startapp jobs

What did these commands create?

django-admin startproject recruitment_finder . created manage.py (a command-line utility for Django) and a recruitment_finder/ package containing your project settings, URL configuration, and server entry points.

python manage.py startapp jobs created a jobs/ directory with starter files like models.py, views.py, and admin.py. These are the files you will build on in the coming steps.

  • Confirm the files were created by running:
ls

You should see jobs, manage.py, recruitment_finder, and venv in the output.

Missing manage.py or recruitment_finder?

  • Make sure you included the dot at the end of the django-admin startproject recruitment_finder . command. Without it, Django creates an extra nested folder.
  • If you see recruitment_finder/recruitment_finder/ (double nesting), delete the outer folder and run the command again with the dot.

Need help? Help me fix my Django project structure.

Register the jobs app and start the server

Django does not automatically know about new apps you create. You need to register the jobs app in your project settings so Django includes it when running migrations, loading templates, and serving requests.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type the name of your code editor (e.g. VS Code) and press Enter to open it.
  • Open the recruitment-finder folder from your Desktop in your code editor.
  • Find and open recruitment_finder/settings.py.
  • Scroll down to find the INSTALLED_APPS list. It looks like this:
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
  • Add 'jobs', as the last item in the list, so it now looks like this:
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'jobs',
]
  • Save settings.py.

✔️ Awesome, I've got everything!

Great. Make sure you have saved settings.py before continuing.

ⓧ I'd like to double check the full code

Scroll to the INSTALLED_APPS list in recruitment_finder/settings.py and confirm 'jobs', appears as the last item, right after 'django.contrib.staticfiles',. This is the only change you need to make in this file.

Now it is time to start the development server. This command will take over your terminal, so you will not be able to type other commands in the same window while it is running. Press Ctrl+C at any time to stop the server.

Before you run the server, what do you think you will see when you open http://127.0.0.1:8000/ in your browser?

  • Start the development server by running this command in your terminal:
python manage.py runserver
  • Open your browser and go to http://127.0.0.1:8000/.

You see the Django welcome page with a green rocket icon. This confirms Django is installed correctly and your project is running.

What about the migration warning?

Your terminal shows a warning about unapplied migrations. This is completely normal. Django ships with built-in apps (like authentication and sessions) that need database tables. You will run migrations in the next step when you create your own models.

Not seeing the welcome page?

  • Make sure the server is still running in your terminal. You should see output ending with a line that mentions http://127.0.0.1:8000/.
  • Check that you typed the URL exactly as shown, including the port number 8000.
  • If you see an error about the port being in use, stop any other servers and try again.

Still stuck? Help me get the Django development server running.

Your Django project is scaffolded and running. Next up, you will build the data models that power the entire recruitment platform.

Build the Core Data Models

Your Django project is up and running. The welcome rocket confirms the framework is installed and the server works.

Every overseas job listing traces back to an original source. A government announcement from the Department of Foreign Employment, an employer's own posting, or a licensed agency's notice. In this step, you will build the data models that enforce this traceability at the database level.

In this step, get ready to:
  • Define five interconnected models in jobs/models.py for countries, employers, agencies, sources, and job listings.
  • Use TextChoices enums to define controlled vocabularies for agency status and source type.
  • Run migrations to create the database tables in SQLite.
Create the Country and Employer models

The recruitment platform needs to track which countries have job openings and which employers operate in those countries. These two models form the foundation that every job listing connects to.

  • Open jobs/models.py in VS Code. Replace its contents with this import block and the Country model:
from datetime import date

from django.db import models


# Destination country for job listings
class Country(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)
    code = models.CharField(
        max_length=3, blank=True, help_text="ISO 3166-1 alpha-3 country code"
    )

    class Meta:
        verbose_name_plural = "countries"
        ordering = ["name"]

    def __str__(self):
        return self.name

What does this code do?

Country stores each destination country. The slug field creates a URL-friendly version of the name (like japan or south-korea), and unique=True prevents duplicate slugs.

The Meta class sets verbose_name_plural so Django displays "countries" instead of "countrys" in the admin. The __str__ method controls how a Country appears in dropdown menus and lists.

  • Add the Employer model directly below the Country class:
# Company that hires overseas workers
class Employer(models.Model):
    name = models.CharField(max_length=200)
    country = models.ForeignKey(
        Country, on_delete=models.CASCADE, related_name="employers"
    )
    website = models.URLField(blank=True)
    is_verified = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["name"]

    def __str__(self):
        return self.name

What is a ForeignKey?

A ForeignKey creates a link between two models. Here, every Employer belongs to exactly one Country. If a country is deleted, on_delete=models.CASCADE automatically deletes all employers in that country too.

The related_name="employers" lets you go in the other direction. Given a country object, you can call country.employers.all() to get every employer in that country.

  • Save jobs/models.py.
  • Verify both models are valid by running this command in your terminal:
python manage.py check

You should see System check identified no issues (0 silenced).

Seeing a syntax error?

  • Check that the Employer class is at the same indentation level as Country. Both should start at column 0, not inside another class.
  • Make sure the closing parenthesis on models.ForeignKey( lines is present. A missing ) causes confusing errors on the lines below.
  • Confirm you still have "jobs" in INSTALLED_APPS inside recruitment_finder/settings.py.

Help me fix a Django model syntax error in jobs/models.py

Add the RecruitmentAgency and Source models

Nepal's Department of Foreign Employment licenses recruitment agencies that connect workers with overseas employers. Each agency has a status that can change over time. Sources track where each job listing was originally published.

  • In jobs/models.py, add the RecruitmentAgency model below the Employer class:
# Licensed agency that connects workers with employers
class RecruitmentAgency(models.Model):
    class Status(models.TextChoices):
        ACTIVE = "ACTIVE", "Active"
        INACTIVE = "INACTIVE", "Inactive"
        BLOCKED = "BLOCKED", "Blocked"
        SUSPENDED = "SUSPENDED", "Suspended"

    name = models.CharField(max_length=200)
    license_number = models.CharField(max_length=50, blank=True)
    status = models.CharField(
        max_length=20, choices=Status, default=Status.ACTIVE
    )
    website = models.URLField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name_plural = "recruitment agencies"
        ordering = ["name"]

    def __str__(self):
        return self.name

What is TextChoices?

TextChoices is a Django enum type that limits a field to a fixed set of values. Instead of storing any arbitrary string in the status field, only ACTIVE, INACTIVE, BLOCKED, or SUSPENDED are allowed.

Each entry has two parts: the stored value ("ACTIVE") and the human-readable label ("Active"). The database stores the value. The admin displays the label.

  • Add the Source model below the RecruitmentAgency class:
# Where a job listing was originally published
class Source(models.Model):
    class SourceType(models.TextChoices):
        GOVERNMENT = "GOVERNMENT", "Government"
        EMPLOYER = "EMPLOYER", "Official Employer"
        AGENCY = "AGENCY", "Licensed Agency"
        NEWS = "NEWS", "Reputable News"

    name = models.CharField(max_length=200)
    url = models.URLField()
    source_type = models.CharField(max_length=20, choices=SourceType)
    reliability_priority = models.PositiveIntegerField(
        help_text=(
            "Lower number = higher priority. "
            "Government=1, Employer=2, Agency=3, News=4"
        )
    )
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["reliability_priority", "name"]

    def __str__(self):
        return f"{self.name} ({self.get_source_type_display()})"

Why rank sources by priority?

The reliability_priority field lets you rank how trustworthy each source is. A government announcement (priority 1) is more reliable than a news article (priority 4). The ordering in Meta sorts sources by this priority automatically.

  • Save jobs/models.py.
  • Run the system check again to make sure all four models are valid:
python manage.py check

You should see System check identified no issues (0 silenced). again. Four models are defined and Django is happy with all of them.

Seeing an error about TextChoices?

  • Make sure the Status class and SourceType class are defined inside their respective model classes, indented one level in.
  • Check that each TextChoices entry uses the format NAME = "VALUE", "Label" with a comma between the value and label strings.

Help me debug my Django TextChoices definition

Create the JobListing model

The JobListing model is the heart of the platform. It stores every detail about an overseas job and links back to the employer, country, and source that published it. You will add it in three chunks because it has many fields.

  • In jobs/models.py, add the beginning of the JobListing class below the Source model. Start with the status enums and core job detail fields:
class JobListing(models.Model):
    # Controlled vocabulary for verification status
    class VerificationStatus(models.TextChoices):
        VERIFIED = "VERIFIED", "Verified"
        SOURCE_FOUND = "SOURCE_FOUND", "Source Found"
        UNVERIFIED = "UNVERIFIED", "Unverified"
        EXPIRED = "EXPIRED", "Expired"

    # Controlled vocabulary for deadline status
    class DeadlineStatus(models.TextChoices):
        OPEN = "OPEN", "Open"
        OPENING_SOON = "OPENING_SOON", "Opening Soon"
        CLOSING_SOON = "CLOSING_SOON", "Closing Soon"
        EXPIRED = "EXPIRED", "Expired"
        CLOSED = "CLOSED", "Closed"

    # Core job details
    title = models.CharField(max_length=300)
    employer = models.ForeignKey(
        Employer, on_delete=models.CASCADE, related_name="jobs"
    )
    country = models.ForeignKey(
        Country, on_delete=models.CASCADE, related_name="jobs"
    )
    location = models.CharField(max_length=200, blank=True)
    vacancies = models.PositiveIntegerField(default=0)
    salary_min = models.DecimalField(
        max_digits=12, decimal_places=2, null=True, blank=True
    )
    salary_max = models.DecimalField(
        max_digits=12, decimal_places=2, null=True, blank=True
    )

What does this code do?

VerificationStatus tracks how trustworthy a listing is: VERIFIED means confirmed from the official source, SOURCE_FOUND means the original posting was located, and UNVERIFIED means nobody has checked yet.

DeadlineStatus defines the possible states for a job's timeline. You will write the logic that computes these automatically in a later step. For now, you are just defining the labels.

DecimalField handles salary values with exact precision. Using null=True, blank=True makes salary optional because not every listing publishes compensation.

  • Continue building the JobListing class by adding the contract, benefits, and timeline fields directly below salary_max (still inside the class, at the same indentation level):
    salary_currency = models.CharField(max_length=10, default="NPR")
    # Contract and benefits
    contract_duration = models.CharField(max_length=100, blank=True)
    working_hours = models.CharField(max_length=100, blank=True)
    accommodation_provided = models.BooleanField(default=False)
    food_provided = models.BooleanField(default=False)
    transportation_provided = models.BooleanField(default=False)
    visa_sponsorship = models.BooleanField(default=False)
    # Requirements
    requirements = models.TextField(blank=True)
    education_level = models.CharField(max_length=100, blank=True)
    experience_years = models.PositiveIntegerField(default=0)
    # Timeline
    opening_date = models.DateField(null=True, blank=True)
    deadline = models.DateField(null=True, blank=True)
    application_url = models.URLField(
        blank=True, help_text="Official application link"
    )

What does this code do?

The boolean fields (accommodation_provided, food_provided, etc.) store yes/no values about what the employer offers. These default to False so listings start with no benefits claimed.

The opening_date and deadline fields will power the automatic deadline classification you add later. The deadline engine compares these dates against today to decide if a job is OPEN, CLOSING SOON, or EXPIRED.

  • Save jobs/models.py and run the system check to make sure everything parses correctly so far:
python manage.py check

You should see System check identified no issues (0 silenced). Django has validated the partial JobListing model alongside the four models you built earlier.

  • Finish the JobListing class by adding the source traceability fields, verification tracking, and the Meta class directly below application_url:
    # Recruitment agency (optional)
    agency = models.ForeignKey(
        RecruitmentAgency,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="jobs",
    )
    fees = models.CharField(max_length=200, blank=True)
    # Source traceability (required)
    source = models.ForeignKey(
        Source, on_delete=models.CASCADE, related_name="jobs"
    )
    source_url = models.URLField(
        help_text="Direct URL to the original announcement"
    )
    publication_date = models.DateField(null=True, blank=True)
    last_verified_date = models.DateField(null=True, blank=True)
    verification_status = models.CharField(
        max_length=20,
        choices=VerificationStatus,
        default=VerificationStatus.UNVERIFIED,
    )
    original_announcement = models.TextField(
        blank=True, help_text="Original text of the announcement"
    )
    manually_closed = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

What does this code do?

The source ForeignKey is the traceability rule in action. It uses on_delete=models.CASCADE and has no null=True, so every job listing must link to a Source record. Django enforces this at the database level.

The agency ForeignKey is optional (null=True, blank=True) because not every job goes through a recruitment agency. Its on_delete=models.SET_NULL keeps the job listing even if the agency record is deleted.

The manually_closed field lets admins override the deadline engine. If a recruitment is cancelled before the deadline, an admin sets this to True and the listing shows as CLOSED.

  • Add the Meta class and __str__ method at the bottom of the JobListing class, below updated_at:
    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return f"{self.title} - {self.employer.name} ({self.country.name})"

What does this code do?

The ordering = ["-created_at"] sorts job listings with the newest first. The minus sign reverses the default ascending order.

The __str__ method shows the job title, employer name, and country together. When you see a listing in the admin, it reads something like "Factory Worker - Toyota Motors (Japan)".

  • Save jobs/models.py.

✔️ Awesome, I've got everything!

Double check you have saved jobs/models.py. Your file should contain five model classes in this order: Country, Employer, RecruitmentAgency, Source, JobListing.

ⓧ I'd like to double check the full code

Your jobs/models.py is too long to show in one block. Scroll up and confirm each section is present, in order:

  • Imports at the top: from datetime import date and from django.db import models.
  • Country class with name, slug, code fields plus Meta and __str__.
  • Employer class with a ForeignKey to Country.
  • RecruitmentAgency class with Status TextChoices (ACTIVE, INACTIVE, BLOCKED, SUSPENDED).
  • Source class with SourceType TextChoices (GOVERNMENT, EMPLOYER, AGENCY, NEWS).
  • JobListing class with VerificationStatus and DeadlineStatus enums, all field groups (core details, benefits, timeline, agency, source traceability), and Meta with __str__.
Run migrations to create the database tables

Django models describe the shape of your data, but they do not create database tables on their own. You need to run two commands: one to generate a migration file (the instructions for the database) and one to apply it.

Before you run the next command, what do you think Django will output? It knows about five models that have no database tables yet.

  • Generate the migration file by running:
python manage.py makemigrations

You should see output listing each model being created:

Migrations for 'jobs':
  jobs/migrations/0001_initial.py
    - Create model Country
    - Create model Employer
    - Create model Source
    - Create model RecruitmentAgency
    - Create model JobListing

What is a migration?

A migration is a Python file that describes changes to your database. Django compares your current models against what the database already has and generates instructions to bring the database up to date.

The file jobs/migrations/0001_initial.py contains the SQL needed to create all five tables. You never need to write SQL yourself.

  • Apply the migration to create the actual database tables by running:
python manage.py migrate

You should see Applying jobs.0001_initial... OK at the end of the output, confirming your five tables now exist in the db.sqlite3 database.

Seeing an error during makemigrations?

  • If you see a NameError, check that your models appear in the right order in the file. Country must come before Employer (which references it), and Source must come before JobListing.
  • If you see No changes detected, make sure "jobs" is listed in INSTALLED_APPS in recruitment_finder/settings.py.
  • If you see an IndentationError, open the double-check tab above and compare your code against the model checklist. Python is strict about consistent indentation.

Help me fix my Django makemigrations error

Your database now has tables for countries, employers, recruitment agencies, sources, and job listings. Every job listing is required to link to a source, enforcing the traceability rule at the database level. Next up, you will build a custom admin dashboard to manage this data.

Build the Admin Dashboard

Your five models are defined and the database tables exist, but there is no way to add or inspect data yet. You could write raw SQL or use the Django shell, but both are slow and error-prone for everyday data entry.

Django ships with a built-in admin interface that generates a full CRUD dashboard from your models for free. In this step you will register every model, create an admin account, and populate the database with sample job listings.

In this step, get ready to:
  • Register all five models in the Django admin with customized list displays, filters, and search.
  • Create a superuser account and log in to the admin dashboard.
  • Add sample countries, employers, sources, and job listings with different deadline scenarios.
Register your models in the admin

Django's admin reads your model definitions and builds forms, list views, and search automatically. You control what columns, filters, and search fields appear by writing a short ModelAdmin class for each model.

  • Open jobs/admin.py in your editor.
  • Replace everything in the file with the following code to register the Country, Employer, and RecruitmentAgency models:
from django.contrib import admin

from .models import Country, Employer, JobListing, RecruitmentAgency, Source


@admin.register(Country)
class CountryAdmin(admin.ModelAdmin):
    # Show name, country code, and slug in the list view
    list_display = ["name", "code", "slug"]
    # Auto-fill the slug field when the admin types a country name
    prepopulated_fields = {"slug": ("name",)}
    search_fields = ["name"]


@admin.register(Employer)
class EmployerAdmin(admin.ModelAdmin):
    list_display = ["name", "country", "is_verified"]
    # Sidebar filters for verified status and country
    list_filter = ["is_verified", "country"]
    search_fields = ["name"]


@admin.register(RecruitmentAgency)
class RecruitmentAgencyAdmin(admin.ModelAdmin):
    list_display = ["name", "license_number", "status"]
    list_filter = ["status"]
    search_fields = ["name", "license_number"]

What does this code do?

The @admin.register decorator tells Django to display a model in the admin dashboard and use your custom ModelAdmin class to control the layout.

list_display sets which columns appear on the model's list page. list_filter adds sidebar filters. search_fields adds a search box that matches against those fields.

prepopulated_fields on CountryAdmin auto-generates the slug from the name as you type, so "Japan" becomes japan automatically.

  • Save jobs/admin.py.
  • Now add the Source and JobListing admin classes below your RecruitmentAgencyAdmin class:
@admin.register(Source)
class SourceAdmin(admin.ModelAdmin):
    list_display = ["name", "source_type", "reliability_priority"]
    list_filter = ["source_type"]
    search_fields = ["name"]


@admin.register(JobListing)
class JobListingAdmin(admin.ModelAdmin):
    list_display = [
        "title",
        "employer",
        "country",
        "vacancies",
        "deadline",
        "verification_status",
    ]
    list_filter = ["country", "verification_status", "manually_closed"]
    search_fields = ["title", "employer__name", "country__name"]
    date_hierarchy = "created_at"

What does this code do?

SourceAdmin lists each source's name, type, and priority so you can see at a glance which sources are most reliable.

JobListingAdmin is the most customized class. search_fields uses double-underscore syntax (employer__name) to search across related models. date_hierarchy adds a date-based drill-down bar at the top of the list, so you can browse listings by year, month, and day.

  • Save jobs/admin.py.

Seeing an import error?

Make sure the first import line reads from .models import Country, Employer, JobListing, RecruitmentAgency, Source. A missing model name in that import causes an ImportError when Django starts.

ask me to help debug my admin.py import error

✔️ Awesome, I've got everything!

Great. Double check you have saved jobs/admin.py and you are ready to move on.

ⓧ I'd like to double check the full code

Scroll up and confirm your jobs/admin.py has these sections in order:

  • The two import lines at the top.
  • CountryAdmin with list_display, prepopulated_fields, and search_fields.
  • EmployerAdmin with list_display, list_filter, and search_fields.
  • RecruitmentAgencyAdmin with list_display, list_filter, and search_fields.
  • SourceAdmin with list_display, list_filter, and search_fields.
  • JobListingAdmin with list_display, list_filter, search_fields, and date_hierarchy.
Create a superuser and log in to the admin

Django's admin is password-protected. You need a superuser account to log in.

  • Create a superuser account by running this command:
python manage.py createsuperuser

What will this command ask me?

Django will prompt you for four things, one at a time:

  • Username: type any username you like (e.g. admin).
  • Email address: type any email (e.g. admin@example.com).
  • Password: type a password. The characters will not appear on screen.
  • Password (again): re-enter the same password to confirm.

After both passwords match, you will see Superuser created successfully. in the terminal.

  • Enter a username when prompted, then an email address, then a password twice.

Password too short or too common?

Django validates your password. If it is shorter than 8 characters or too similar to your username, Django will warn you and ask if you want to proceed anyway. Type y and press Enter to continue with a simple password for local development.

ask me to help with Django superuser creation issues

  • Start the development server by running:
python manage.py runserver
  • Open your browser and go to http://127.0.0.1:8000/admin/.
  • Enter the username and password you just created, then click Log in.

You should see the Django administration dashboard with five model groups under the JOBS heading: Countrys, Employers, Job listings, Recruitment agencys, and Sources.

Why does it say Countrys instead of Countries?

Django auto-generates plural names by adding an "s". Your Country model has verbose_name_plural = "countries" in its Meta class, but the admin sidebar uses the verbose name. You will see Countries displayed correctly because of that setting.

Add sample data through the admin

Your admin is live but the database is empty. You need sample data to test the job listings page you will build in the next step.

Why does the order of data entry matter?

Your models use ForeignKey relationships. A JobListing requires an Employer, an Employer requires a Country, and a JobListing requires a Source. You need to create the dependent records first.

  • In the admin dashboard, click Add next to Countries.
  • Enter Japan in the Name field. The Slug field auto-fills to japan.
  • Enter JPN in the Code field.
  • Click Save and add another.
  • Enter Qatar as the name (slug auto-fills to qatar), code QAT, then click Save.

You should see both countries listed on the Countries page.

  • Click Home in the breadcrumb at the top to return to the dashboard.
  • Click Add next to Sources.
  • Enter Department of Foreign Employment as the name.
  • Enter https://dofe.gov.np/ as the URL.
  • Select Government for Source type, enter 1 for Reliability priority, then click Save.

Now add one employer in each country.

  • Click Home, then click Add next to Employers.
  • Enter Tokyo Manufacturing Co. as the name, select Japan for Country, then click Save and add another.
  • Enter Doha Construction LLC as the name, select Qatar for Country, then click Save.

Time for the main event. You will create three job listings with different deadline scenarios to test the deadline engine later.

  • Click Home, then click Add next to Job listings.
  • For the first listing, enter Warehouse Worker - Tokyo as the title.
  • Select Tokyo Manufacturing Co. as the employer and Japan as the country.
  • Enter 10 for vacancies.
  • Set Deadline to a date that has already passed, such as one week ago.
  • Select Department of Foreign Employment (Government) as the source.
  • Enter https://dofe.gov.np/example-listing as the source URL.
  • Click Save and add another.

What about all the other fields?

Fields like salary, contract duration, and requirements are optional (marked blank=True in your models). You only need to fill in the required fields right now. You can come back and add more details later.

  • For the second listing, enter Construction Worker - Doha as the title.
  • Select Doha Construction LLC as the employer and Qatar as the country.
  • Enter 25 for vacancies.
  • Set Deadline to 5 days from today.
  • Select the same source, enter https://dofe.gov.np/example-listing-2 as the source URL, then click Save and add another.
  • For the third listing, enter Factory Operator - Osaka as the title.
  • Select Tokyo Manufacturing Co. as the employer and Japan as the country.
  • Enter 5 for vacancies.
  • Set Opening date to a date about one month from now. Leave Deadline empty.
  • Select the same source, enter https://dofe.gov.np/example-listing-3 as the source URL, then click Save.

Before you check the final list, what do you expect to see in the job listings table? Think about which columns you configured in list_display.

  • In the admin sidebar, click Job listings to view all three entries.

You should see your 3 job listings displayed with columns for title, employer, country, vacancies, deadline, and verification status.

  • In the right sidebar, click Japan under the By country filter.

The list narrows to show only the two Japan listings. This is the list_filter you configured on JobListingAdmin in action.

Don't see any listings or filters?

Make sure you clicked Save (not just Save and continue editing) on each listing. Unsaved listings will not appear in the list view.

If the country filter is missing from the sidebar, open jobs/admin.py and check that list_filter includes "country" in JobListingAdmin.

ask me to help troubleshoot my Django admin list view

Your admin dashboard is fully functional and loaded with test data. Next up, you will build a public-facing job listings page so job seekers can browse these listings without an admin account.

Create the Public Job Listings Page

Your admin dashboard is ready. You can add job listings, filter by country, and search by employer name through Django's built-in admin interface.

The admin is built for data managers, not job seekers. Someone looking for overseas work needs a public page they can visit without an admin account. Django's views and templates system lets you build exactly that.

In this step, get ready to:
  • Create a view function that queries all job listings from the database.
  • Set up URL routing so visitors can reach the page at the site root.
  • Build HTML templates to display job details for each listing.
Create the view and URL routing

A Django view is a Python function that receives a web request and returns a response. Your view will query the database for all job listings and hand the results to an HTML template for display.

  • Open jobs/views.py in your editor and replace its contents with this code:
from django.shortcuts import render

from .models import JobListing


def job_list(request):
    # Query all job listings, joining related tables in one SQL query
    jobs = JobListing.objects.select_related(
        "employer", "country", "source"
    ).all()
    # Send the results to the job_list template
    return render(request, "jobs/job_list.html", {"jobs": jobs})

What does this code do?

select_related("employer", "country", "source") tells Django to fetch each listing's employer, country, and source data in a single database query instead of making a separate query for each listing. This prevents the N+1 query problem where displaying 10 listings would otherwise trigger 30+ queries.

render() combines the query results with an HTML template and returns the finished page to the browser.

  • Save jobs/views.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

from django.shortcuts import render

from .models import JobListing


def job_list(request):
    # Query all job listings, joining related tables in one SQL query
    jobs = JobListing.objects.select_related(
        "employer", "country", "source"
    ).all()
    # Send the results to the job_list template
    return render(request, "jobs/job_list.html", {"jobs": jobs})

The view function exists, but Django has no idea when to call it. You need a URL pattern that maps a web address to this function.

  • Create a new file called urls.py inside the jobs folder and add this code:
from django.urls import path

from . import views

# Namespace for reverse URL lookups
app_name = "jobs"

urlpatterns = [
    # Map the site root to the job_list view
    path("", views.job_list, name="job_list"),
]

What does this code do?

app_name = "jobs" gives this app a namespace so you can refer to its URLs unambiguously in templates and code.

path("", views.job_list, name="job_list") maps the root URL (empty string) to your job_list view function. The name argument lets you reference this URL by name instead of hardcoding paths.

  • Save jobs/urls.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

from django.urls import path

from . import views

# Namespace for reverse URL lookups
app_name = "jobs"

urlpatterns = [
    # Map the site root to the job_list view
    path("", views.job_list, name="job_list"),
]
  • Verify both new files are valid by running this command:
python manage.py check

You should see System check identified no issues (0 silenced). in the output.

Your app has its own URL patterns, but Django only checks routes registered in the project's main urls.py file. You need to connect them.

  • Open recruitment_finder/urls.py and find this code:
from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]
  • Replace the entire contents with this updated version:
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    # Send all non-admin requests to the jobs app
    path("", include("jobs.urls")),
]

What changed?

include was added to the import line. This function delegates URL matching to another file.

path("", include("jobs.urls")) tells Django to forward any request that does not start with admin/ to the URL patterns you defined in jobs/urls.py.

  • Save recruitment_finder/urls.py.
  • Start the development server by running:
python manage.py runserver

You should see a Django error page with a TemplateDoesNotExist heading. This is good news. It means Django found your view function and tried to render the template, but the template file does not exist yet.

Seeing a different error?

  • If you see an ImportError, double-check that jobs/urls.py exists and uses from . import views (note the dot before import).
  • If you see a plain Page not found (404) page, check that you added path("", include("jobs.urls")) to recruitment_finder/urls.py.
  • help me debug my URL routing error.
Set up the template directory and base layout

Django needs to know where your HTML templates live. You will create a project-level templates/ directory and tell Django to look there.

  • Stop the dev server by pressing Ctrl+C in your terminal.
  • Create the template directories by running these commands:
mkdir templates
mkdir templates/jobs
  • Confirm the directories were created by running:
ls templates

You should see jobs listed in the output.

  • Open recruitment_finder/settings.py and find the TEMPLATES setting.
  • Find this line inside the TEMPLATES list:
"DIRS": [],
  • Replace it with:
"DIRS": [BASE_DIR / "templates"],
  • Save recruitment_finder/settings.py.

What does this change?

BASE_DIR / "templates" tells Django to look for templates in the templates/ folder at the root of your project. Without this, Django would only search inside each app's own directories.

Now you will create the base template. This file defines the shared page structure (header, styles, layout) that every page on the site inherits.

  • Create a new file called base.html inside the templates folder and paste this code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Global Recruitment Finder{% endblock %}</title>
    <style>
        /* Global reset and layout styles */
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #f5f5f5;
            color: #333;
            line-height: 1.6;
        }
        header {
            background: #1a237e;
            color: white;
            padding: 1rem 2rem;
        }
        header h1 { font-size: 1.5rem; }
        header p { font-size: 0.9rem; opacity: 0.8; }
        main { max-width: 1000px; margin: 2rem auto; padding: 0 1rem; }
        /* Badge base style */
        .badge {
            display: inline-block;
            padding: 0.2rem 0.6rem;
            border-radius: 4px;
            font-size: 0.75rem;
            font-weight: 600;
            text-transform: uppercase;
        }
  • Directly below the closing brace, add these badge and card styles:
        /* Color variants for deadline and verification badges */
        .badge-open { background: #c8e6c9; color: #2e7d32; }
        .badge-opening-soon { background: #bbdefb; color: #1565c0; }
        .badge-closing-soon { background: #ffe0b2; color: #e65100; }
        .badge-expired { background: #ffcdd2; color: #c62828; }
        .badge-closed { background: #e0e0e0; color: #616161; }
        .badge-verified { background: #c8e6c9; color: #2e7d32; }
        .badge-source-found { background: #bbdefb; color: #1565c0; }
        .badge-unverified { background: #fff9c4; color: #f57f17; }
        /* Job listing card styles */
        .job-card {
            background: white;
            border-radius: 8px;
            padding: 1.5rem;
            margin-bottom: 1rem;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
        }
        .job-card h2 { font-size: 1.2rem; margin-bottom: 0.5rem; }
        .job-meta { color: #666; font-size: 0.9rem; margin-bottom: 0.5rem; }
        .job-badges { margin-bottom: 0.5rem; }
        .job-details { font-size: 0.9rem; color: #555; }
        .job-details span { margin-right: 1rem; }
        .source-link {
            font-size: 0.8rem;
            color: #1565c0;
            text-decoration: none;
            margin-top: 0.5rem;
            display: inline-block;
        }
        .source-link:hover { text-decoration: underline; }
        a { color: #1a237e; }
  • Below the last CSS line, close the style block and add the HTML body:
    </style>
</head>
<body>
    <header>
        <h1>Global Recruitment Finder</h1>
        <p>Nepal's trusted overseas job intelligence platform</p>
    </header>
    <main>
        {% block content %}{% endblock %}
    </main>
</body>
</html>

What does base.html do?

This file defines the shared page structure for the entire site. Every page inherits the dark blue header, the centered layout, and all the CSS styles.

{% block content %}{% endblock %} is a placeholder. Each page template fills this block with its own content while keeping everything else the same.

  • Save templates/base.html.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

Since base.html was built across three chunks, scroll up and confirm each one is present in order:

  • Chunk 1: The <!DOCTYPE html> declaration through the global layout CSS and the .badge { } base style block.
  • Chunk 2: The badge color variants (.badge-open through .badge-unverified), job card styles, and the a { color: #1a237e; } line.
  • Chunk 3: The closing </style> tag, the <body> with the header, and the {% block content %} placeholder inside <main>.

Seeing a syntax error in base.html?

  • Make sure the three chunks connect without gaps. The CSS from chunk 2 should sit directly below chunk 1's closing brace.
  • Check that the </style> tag from chunk 3 appears right after chunk 2's last CSS line.
  • help me check my base.html template.
Build the job listing template and verify

The base template defines the page shell. Now you need the page-specific template that actually displays the job listings.

  • Create a new file called job_list.html inside templates/jobs/ and paste this code:
<!-- Inherit the base layout and styles -->
{% extends "base.html" %}

{% block title %}Job Listings - Global Recruitment Finder{% endblock %}

{% block content %}
<h2 style="margin-bottom: 1rem;">Overseas Job Listings</h2>
<p style="margin-bottom: 1.5rem; color: #666;">
    {{ jobs|length }} listing{{ jobs|length|pluralize }} found
</p>

{% for job in jobs %}
<div class="job-card">
    <h2>{{ job.title }}</h2>
    <p class="job-meta">
        {{ job.employer.name }} &middot; {{ job.country.name }}
        {% if job.location %} &middot; {{ job.location }}{% endif %}
    </p>
  • Below the closing </p> tag for the job meta paragraph, add the job details and source section:
    <!-- Job details: vacancies, salary range, deadline -->
    <div class="job-details">
        {% if job.vacancies %}
            <span>Vacancies: {{ job.vacancies }}</span>
        {% endif %}
        {% if job.salary_min or job.salary_max %}
            <span>Salary:
                {% if job.salary_min and job.salary_max %}
                    {{ job.salary_min }} - {{ job.salary_max }} {{ job.salary_currency }}
                {% elif job.salary_min %}
                    From {{ job.salary_min }} {{ job.salary_currency }}
                {% elif job.salary_max %}
                    Up to {{ job.salary_max }} {{ job.salary_currency }}
                {% endif %}
            </span>
        {% endif %}
        {% if job.deadline %}
            <span>Deadline: {{ job.deadline }}</span>
        {% endif %}
    </div>
    <!-- Source traceability: link back to the original announcement -->
    <div style="margin-top: 0.5rem; font-size: 0.85rem; color: #777;">
        Source: {{ job.source.name }} ({{ job.source.get_source_type_display }})
        {% if job.source_url %}
            &middot; <a class="source-link" href="{{ job.source_url }}" target="_blank" rel="noopener">View original announcement</a>
        {% endif %}
    </div>
</div>
{% empty %}
<p>No job listings found.</p>
{% endfor %}
{% endblock %}

What does this template do?

{% extends "base.html" %} tells Django this page inherits the header, styles, and layout from your base template. It only needs to fill in the content block.

The {% for job in jobs %} loop iterates over every listing your view passed to the template. Each listing renders as a card with its title, employer, country, salary range, vacancies, deadline, and a link back to the original source.

{% empty %} is a fallback that displays a message when there are no listings in the database.

  • Save templates/jobs/job_list.html.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

Since job_list.html was built across two chunks, scroll up and confirm both are present in order:

  • Chunk 1: The {% extends %} declaration, the title block, the heading and listing count, the {% for %} loop opening, the job card with title and employer meta.
  • Chunk 2: The job-details div with vacancies, salary, and deadline. The source div with the original announcement link. The closing </div>, {% empty %}, {% endfor %}, and {% endblock %}.

Before you open the page, think about this: you added three job listings with different deadlines. One is expired, one is closing in a few days, and one has not opened yet. Do you think you will be able to tell which is which just by looking at the page?

  • Start the development server by running:
python manage.py runserver

You should see all 3 job listings displayed as white cards. Each card shows the job title, employer, country, salary range, vacancies, deadline date, and a source link.

Now look carefully. The listing with a deadline that already passed looks exactly the same as the one closing in five days. There is no visual difference between open, expired, or upcoming jobs. A job seeker scanning this page would have no idea which listings are still worth applying to.

Why does this matter?

The data is all there. Deadlines, opening dates, and verification statuses are stored in the database. The problem is that the template treats every listing identically. It just dumps the raw date without telling the user what it means.

This is the gap you will close in the next step by adding a deadline engine that automatically classifies each listing and displays color-coded badges.

Don't see your job listings?

  • Check that TEMPLATES in settings.py has BASE_DIR / "templates" inside the DIRS list.
  • Verify that job_list.html is inside templates/jobs/ (the jobs subfolder is required, not just templates/).
  • If you see 0 listings, make sure you added sample data through the admin in the previous step.
  • help me debug my Django templates.

Your public listings page is live. The data is there, but every listing looks identical. Next up, you will build a deadline engine that automatically classifies each listing and adds color-coded badges so users can spot open opportunities at a glance.

Add the Deadline Engine and Verification Badges

Your public job listings page is live, but there is a glaring problem. Every listing looks identical regardless of whether the deadline passed last month or the job closes tomorrow.

A job seeker scanning this page has no way to tell which opportunities are still worth pursuing. In this step, you will build a deadline engine that automatically classifies each listing based on today's date. Then you will add color-coded badges so every listing communicates its real-time status at a glance.

In this step, get ready to:
  • Add computed properties to JobListing for real-time deadline classification.
  • Display the computed deadline status in the Django admin dashboard.
  • Show color-coded deadline and verification badges on the public page.
Add the deadline engine to your model

Your JobListing model stores raw dates like opening_date and deadline, but it does nothing with them. You need logic that compares today's date against those fields and decides the listing's status automatically.

Why a property instead of a database field?

A deadline status changes based on the current date. A stored database field would go stale the moment midnight passes.

A Python @property recalculates every time you access it. The status is always accurate with zero maintenance.

  • In jobs/models.py, scroll to the bottom of the JobListing class.
  • Find the __str__ method. Below it, add these two properties:
    @property
    def deadline_status(self):
        # Compare today's date against the listing's dates
        today = date.today()
        if self.manually_closed:
            return self.DeadlineStatus.CLOSED
        if self.opening_date and self.opening_date > today:
            return self.DeadlineStatus.OPENING_SOON
        if self.deadline:
            if self.deadline < today:
                return self.DeadlineStatus.EXPIRED
            days_remaining = (self.deadline - today).days
            if days_remaining <= 7:
                return self.DeadlineStatus.CLOSING_SOON
        return self.DeadlineStatus.OPEN

    @property
    def days_until_deadline(self):
        # Return the number of days remaining, or None if no deadline
        if self.deadline is None:
            return None
        return (self.deadline - date.today()).days

What does this code do?

The deadline_status property checks conditions in a specific priority order:

  • If manually_closed is True, the listing is CLOSED. An admin chose to shut it down.
  • If opening_date is in the future, it is OPENING_SOON.
  • If the deadline has passed, it is EXPIRED. If the deadline is within 7 days, it is CLOSING_SOON.
  • If none of those match, the listing is OPEN.

The days_until_deadline property returns the integer number of days remaining. The template uses this to show "Closing Soon (3 days)" on urgent listings.

  • Save jobs/models.py.
  • Open the Django interactive shell to test your new properties by running:
python manage.py shell
  • In the shell, test the deadline engine on one of your sample listings by running these lines one at a time:
from jobs.models import JobListing
job = JobListing.objects.first()
print(job.title, job.deadline_status, job.days_until_deadline)

You should see the listing's title followed by a status like EXPIRED or CLOSING_SOON depending on which listing comes first, plus an integer for days remaining.

  • Exit the shell by typing:
exit()

Seeing an AttributeError?

  • Make sure you saved jobs/models.py before opening the shell. The shell loads code from disk at startup.
  • Check that the @property decorator sits on the line directly above def deadline_status(self):. Missing it turns the property into a regular method.
  • Confirm the properties are indented inside the JobListing class, at the same level as the __str__ method.

Still stuck? Ask for help.

✔️ Awesome, I've got everything!

Great. Make sure you have saved jobs/models.py.

ⓧ I'd like to double check the full code

The full jobs/models.py file is long, so scroll through and confirm these sections are present in order:

  • The Country, Employer, RecruitmentAgency, and Source model classes from earlier steps (unchanged).
  • The JobListing class with its VerificationStatus and DeadlineStatus enums, all the fields, and the __str__ method (from Step 2).
  • The new deadline_status property and days_until_deadline property at the very bottom of the JobListing class (added in this step).
Display the deadline status in the admin

The deadline engine works in Python, but the admin dashboard does not show it yet. You need to tell Django's admin to call the deadline_status property and display the result as a column.

  • In jobs/admin.py, find the list_display list inside JobListingAdmin.
  • Find this line:
        "deadline",
        "verification_status",
  • Add "get_deadline_status", between those two lines so it looks like this:
        "deadline",
        "get_deadline_status",
        "verification_status",
  • Now scroll to the bottom of the JobListingAdmin class. Below the date_hierarchy line, add this method:
    @admin.display(description="Deadline Status")
    def get_deadline_status(self, obj):
        # Call the model's computed property
        return obj.deadline_status

What does this code do?

  • The @admin.display decorator tells Django to show this method as a column in the admin list. The description parameter sets the column header text.
  • The method calls obj.deadline_status, which triggers the @property you just wrote. Django cannot put a property directly in list_display, so this wrapper method bridges the gap.
  • Save jobs/admin.py.
  • Start the dev server by running:
python manage.py runserver
  • Open http://127.0.0.1:8000/admin/jobs/joblisting/ in your browser and log in with your superuser credentials.

You should see a new Deadline Status column in the job listing table. Each row displays its computed status like EXPIRED, CLOSING_SOON, or OPENING_SOON based on the dates you entered earlier.

Don't see the Deadline Status column?

  • Make sure "get_deadline_status" appears in the list_display list. It must match the method name exactly.
  • Check that the get_deadline_status method is indented inside the JobListingAdmin class, not outside it.

Need help troubleshooting?

✔️ Awesome, I've got everything!

Great. Make sure you have saved jobs/admin.py.

ⓧ I'd like to double check the full code

Scroll through jobs/admin.py and confirm these sections are present in order:

  • The CountryAdmin, EmployerAdmin, RecruitmentAgencyAdmin, and SourceAdmin classes (unchanged from Step 3).
  • The JobListingAdmin class with "get_deadline_status" now in list_display (between "deadline" and "verification_status").
  • The new get_deadline_status method with the @admin.display decorator at the bottom of the JobListingAdmin class.
Add color-coded badges to the template

The deadline engine is working in Python and visible in the admin. Now you need the public page to show it. The base.html template already has CSS classes for badges (like .badge-open and .badge-expired). You just need the template logic to pick the right class for each listing.

  • Open templates/jobs/job_list.html.
  • Inside the {% for job in jobs %} loop, find this line:
<div class="job-card">
    <h2>{{ job.title }}</h2>
  • Add the following badge section between <div class="job-card"> and the <h2> tag. Copy this code and paste it right after the opening <div class="job-card"> line:
    <div class="job-badges">
        {% if job.deadline_status == "OPEN" %}
            <span class="badge badge-open">Open</span>
        {% elif job.deadline_status == "OPENING_SOON" %}
            <span class="badge badge-opening-soon">Opening Soon</span>
        {% elif job.deadline_status == "CLOSING_SOON" %}
            <span class="badge badge-closing-soon">Closing Soon{% if job.days_until_deadline is not None %} ({{ job.days_until_deadline }} days){% endif %}</span>
        {% elif job.deadline_status == "EXPIRED" %}
            <span class="badge badge-expired">Expired</span>
        {% elif job.deadline_status == "CLOSED" %}
            <span class="badge badge-closed">Closed</span>
        {% endif %}

        {% if job.verification_status == "VERIFIED" %}
            <span class="badge badge-verified">Verified</span>
        {% elif job.verification_status == "SOURCE_FOUND" %}
            <span class="badge badge-source-found">Source Found</span>
        {% elif job.verification_status == "UNVERIFIED" %}
            <span class="badge badge-unverified">Unverified</span>
        {% elif job.verification_status == "EXPIRED" %}
            <span class="badge badge-expired">Expired</span>
        {% endif %}
    </div>

What does this template code do?

The first {% if %} block reads job.deadline_status (the property you just built) and picks a color-coded CSS class. Green for OPEN, blue for OPENING SOON, orange for CLOSING SOON, red for EXPIRED, gray for CLOSED.

The CLOSING_SOON badge also shows the number of days remaining using job.days_until_deadline. The second {% if %} block does the same for the verification status field that is already stored in the database.

  • Save templates/jobs/job_list.html.

Before you refresh the page, think about what you expect to see. You have three sample listings: one with a past deadline, one due in about 5 days, and one with an opening date in the future. What color badge should each one display?

  • Open http://127.0.0.1:8000/ in your browser (or refresh if it is already open).

Your three listings now display distinct color-coded badges. The past-deadline listing shows a red "Expired" badge. The one due in about 5 days shows an orange "Closing Soon" badge with the days remaining. The future-opening listing shows a blue "Opening Soon" badge. Each listing also shows its verification status badge.

Badges not showing up?

  • Make sure the job-badges div is inside the {% for job in jobs %} loop, not outside it.
  • Check that the comparison values in the template use uppercase strings like "OPEN" and "CLOSING_SOON". These must match the values defined in your DeadlineStatus enum exactly.
  • Confirm you saved both jobs/models.py (with the properties) and templates/jobs/job_list.html (with the badge HTML).

Still stuck? Ask for help.

✔️ Awesome, I've got everything!

Great. Make sure you have saved templates/jobs/job_list.html.

ⓧ I'd like to double check the full code

Scroll through templates/jobs/job_list.html and confirm these sections are present in order:

  • The {% extends "base.html" %} and {% block title %} tags at the top.
  • The heading and listing count paragraph.
  • Inside each job-card div: the new job-badges div (with deadline badges and verification badges) comes BEFORE the <h2> title.
  • The existing job details, salary info, and source link sections (unchanged from Step 4).

Your job listings page now tells a real story. A job seeker can instantly spot which opportunities are worth pursuing and which have already passed. The deadline engine recalculates every time the page loads, so you never need to manually update a status. Next up, take on the Secret Mission and add country-wise pages so users can browse jobs by destination.

Secret mission

Add Country-Wise Pages

Your job listings page shows every listing in one big list. Can you add country-wise browsing so users filter by destination? You will create new views, URL patterns with slug parameters, and templates that let users click a country and see only the jobs for that destination.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally using Python, Django, and SQLite. There are no cloud resources or ongoing costs to worry about.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Your project folder on the Desktop containing all Django source files, templates, and the db.sqlite3 database.
  • The Python virtual environment directory inside your project folder.

Keep everything running

No action needed. Choose this if you are still actively building or plan to continue with follow-on projects in this series.

  • Your project folder, virtual environment, and SQLite database take minimal disk space.
  • You can restart the development server anytime by activating the virtual environment and running python manage.py runserver.

Pause - I'll come back to this later

Shut down running processes to free up your terminal. Your project files and database remain intact for when you return.

  • If the development server is still running, press Ctrl+C in the terminal to stop it.
  • Deactivate the virtual environment by running:
  • Your project files, database, and virtual environment stay on disk. When you come back, reactivate the virtual environment and start the server again.
deactivate

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your project files, virtual environment, and the SQLite database permanently.

  • If the development server is still running, press Ctrl+C in the terminal to stop it.
  • Deactivate the virtual environment by running:
deactivate
  • Navigate to your Desktop and delete the project folder. On macOS or Linux, run:
cd ~/Desktop
rm -rf global-recruitment-finder
  • On Windows, run:
cd ~\Desktop
rmdir /s /q global-recruitment-finder
  • Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows) on your Desktop. Your project folder should no longer appear.

Nice Work!

Nice Work!

You just built a full Django recruitment intelligence platform from scratch. You can demo a public job board with real-time deadline badges, browse listings by country, and manage everything through a customized admin dashboard.

You've learned how to:

  • Build a production-grade data model with 5 interconnected Django models (Country, Employer, RecruitmentAgency, Source, JobListing) that enforce source traceability through ForeignKey relationships and TextChoices enums.
  • Customize the Django admin dashboard with search, filters, date hierarchy, and computed display fields using the @admin.display decorator for managing recruitment data.
  • Create a public job listings page with a deadline engine that automatically classifies every listing as OPEN, OPENING SOON, CLOSING SOON, EXPIRED, or CLOSED, with color-coded verification badges computed in real time.
  • Secret Mission: Created country-wise pages so users can browse jobs by destination country, with open listing counts and filtered detail views.

Ready to quiz yourself?