Build a Lead Generation App
Build a Flask app that captures leads, scores them, and shows an admin dashboard.
Introduction
30 Second Summary
Every business needs a way to capture potential customers who visit their website. A simple contact form is fine, but knowing which inquiries are worth prioritizing saves hours of follow-up time.
In this project, you will build a lead generation web app with Python and Flask that captures visitor inquiries through a form, automatically scores each one by priority, and displays them all in a color-coded admin dashboard.
What You'll Build
A working lead generation app where you fill out a form, watch the app score the lead instantly, and then open the admin dashboard to see it appear with a color-coded priority badge.
By the end of this project, you'll have:
- A professional landing page with a form that captures company name, email, budget, timeline, and project details.
- Automatic lead scoring that classifies each submission as hot, warm, or cold based on budget and urgency.
- An admin dashboard showing all leads in a filterable table with color-coded score badges.
- Secret Mission: A CSV export feature that downloads all your leads as a spreadsheet.
Are there any prerequisites?
You need Python 3.9 or higher installed on your machine. Everything else (Flask, SQLite, Bootstrap) is set up during the project.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters. This is your moment to commit to the project goal before any code or setup begins.
Set Up Your Flask Project
You know what you're building. Now you need the foundation to build it on.
Every Flask app needs a clean project folder, an isolated Python environment, and Flask itself installed. In this step, you'll set all of that up and confirm everything works with a hello-world page in your browser.
In this step, get ready to:
- Create a project folder and Python virtual environment.
- Install Flask and create the app skeleton.
- Run the app and see it live in your browser.
Create a project folder and virtual environment
A virtual environment keeps your project's packages separate from your system Python. This prevents version conflicts between projects.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir leadgen-app
cd leadgen-app
- Create a virtual environment by running:
python -m venv venv
- Activate the virtual environment. The command differs by operating system:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
Your terminal prompt now shows (venv) at the beginning. This confirms you are working inside the virtual environment.
Don't see (venv) in your prompt?
Make sure you ran the activation command for your operating system. On macOS, the command starts with source. On Windows, it uses backslashes with no source prefix.
help me activate my Python virtual environment
- Install Flask by running:
pip install Flask==3.1.3
You should see pip download and install Flask along with its dependencies.
- Confirm Flask is installed by running:
flask --version
You should see Flask 3.1.3 printed along with your Python and Werkzeug versions.
Seeing 'command not found'?
This usually means the virtual environment is not active. Run the activation command again for your operating system, then retry flask --version.
help me fix flask command not found after pip install
Create the Flask app skeleton
Now that Flask is installed, you need an application file for it to serve. You will create a minimal app with a single route that returns a greeting.
- Open your leadgen-app folder (the one you just created on your Desktop) in your code editor.
- Create a new file called app.py in the root of leadgen-app and add this code:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello, World!'
What does this code do?
- from flask import Flask imports the Flask class from the package you just installed.
- app = Flask(__name__) creates an instance of your web application.
- @app.route('/') tells Flask which URL triggers the function below it.
- def index() is the function that runs when someone visits the home page. It returns plain text for now.
- Save app.py.
- Create the templates folder (Flask looks here for HTML files) by running this in your terminal:
mkdir templates
- Confirm both exist by running:
ls
You should see app.py, templates, and venv listed in the output.
Missing app.py in the listing?
Make sure you saved the file inside the leadgen-app folder on your Desktop, not in a subfolder or your home directory. The file should sit at the same level as venv.
help me find where my app.py file was saved
Run the app and confirm it works
Before you run the server, what do you think will happen when you open http://127.0.0.1:5000 in your browser?
- Start the Flask development server by running:
flask --app app run --debug
What does --debug do?
The --debug flag enables auto-reload. Every time you save a code change, Flask restarts the server automatically so you can see updates without stopping and restarting manually.
Your terminal shows a message that the server is running. It will stay occupied serving your app.
- Open your web browser and navigate to http://127.0.0.1:5000.
You should see "Hello, World!" displayed as plain text on the page. That is your Flask app responding to the browser's request.
Seeing 'Address already in use'?
Another process is using port 5000. Stop it with Ctrl+C in any other terminal running Flask, then try again.
help me fix Flask address already in use error
✔️ Awesome, I've got everything!
Great. Make sure app.py is saved and the server is still running in your terminal.
ⓧ I'd like to double check the full code
Here is the complete app.py at this point:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello, World!'
Your Flask foundation is solid. Next up, you'll replace that plain text greeting with a professional lead capture form styled with Bootstrap.
Build the Lead Capture Form
Your Flask app is running and serving a plain "Hello, World!" message. That proves the server works, but visitors have no way to interact with it.
In this step, you will build a professional-looking landing page with a lead capture form. The form collects company name, email, budget range, timeline, and a project description.
In this step, get ready to:
- Create a base HTML template with Bootstrap styling and navigation.
- Build a landing page with a lead capture form.
- Connect the form to your Flask app so it renders in the browser.
Create the base HTML template with Bootstrap CDN
Flask uses Jinja2 template inheritance so you can define a shared page layout once and reuse it across every page. Your base template will include the Bootstrap CSS framework, a navigation bar, and a content block that child templates fill in.
- Create a new file called base.html inside your templates folder.
- Paste the following code into base.html to set up the page head and navigation bar:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LeadGen App</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container">
<a class="navbar-brand" href="/">LeadGen</a>
<div class="navbar-nav">
<a class="nav-link" href="/">Capture Form</a>
<a class="nav-link" href="/admin">Admin Dashboard</a>
</div>
</div>
</nav>
What does this code do?
- The <link> tag pulls in Bootstrap 5.3.8 from a CDN, giving you professional styling without writing any CSS.
- The <nav> creates a dark navigation bar with links to the capture form and an admin dashboard you will build later.
- Directly below the nav code you just pasted, add the page container and closing tags:
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
</body>
</html>
What does this section do?
- get_flashed_messages() is a Jinja2 function that displays one-time notifications (like error or success messages) at the top of the page.
- {% block content %}{% endblock %} is the placeholder where child templates inject their own HTML.
- The <script> tag loads Bootstrap's JavaScript bundle for interactive components like dismissible alerts.
- Save templates/base.html.
Seeing a syntax error in your editor?
Make sure the {% with %} and {% endwith %} tags are properly paired. The Jinja2 template syntax uses {% for logic and {{ for output.
help me fix a Jinja2 syntax error in my base.html template
✔️ Awesome, I've got everything!
Great. Make sure you have saved base.html before moving on.
ⓧ I'd like to double check the full code
Your base.html should contain both chunks above in order. Scroll up and confirm:
- Chunk 1: The <!DOCTYPE html> declaration through the closing </nav> tag.
- Chunk 2: The <div class="container"> through the closing </html> tag.
Create the landing page template with the lead form
The landing page extends your base template and fills the content block with a lead capture form. Each form field maps to a piece of information you need from potential leads.
- Create a new file called index.html inside your templates folder.
- Paste the following code into index.html to set up the card container and the first two form fields:
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-body p-4">
<h2 class="card-title text-center mb-4">Get a Free Quote</h2>
<p class="text-center text-muted mb-4">Tell us about your project and we will get back to you within 24 hours.</p>
<form action="/submit" method="POST">
<div class="mb-3">
<label for="company_name" class="form-label">Company Name *</label>
<input type="text" class="form-control" id="company_name" name="company_name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email Address *</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
What does this code do?
- {% extends "base.html" %} tells Jinja2 to wrap this page inside your base template layout.
- The form uses method="POST" and submits to /submit. You will create that route in the next step.
- Each name attribute (like company_name) is the key Flask uses to read the submitted value on the server.
- Directly below the email field, add the budget and timeline dropdowns, the message textarea, and the submit button:
<div class="mb-3">
<label for="budget" class="form-label">Budget Range *</label>
<select class="form-select" id="budget" name="budget" required>
<option value="" disabled selected>Select your budget</option>
<option value="Under $1,000">Under $1,000</option>
<option value="$1,000-$5,000">$1,000-$5,000</option>
<option value="$5,000-$10,000">$5,000-$10,000</option>
<option value="$10,000+">$10,000+</option>
</select>
</div>
<div class="mb-3">
<label for="timeline" class="form-label">Project Timeline *</label>
<select class="form-select" id="timeline" name="timeline" required>
<option value="" disabled selected>When do you need this?</option>
<option value="This week">This week</option>
<option value="This month">This month</option>
<option value="In 1-3 months">In 1-3 months</option>
<option value="Just exploring">Just exploring</option>
</select>
</div>
<div class="mb-3">
<label for="message" class="form-label">Project Description</label>
<textarea class="form-control" id="message" name="message" rows="4" placeholder="Tell us briefly about your project..."></textarea>
</div>
<button type="submit" class="btn btn-primary w-100 btn-lg">Submit Inquiry</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
What do these fields capture?
- Budget and timeline use <select> dropdowns so the learner picks from predefined options. This keeps data consistent for scoring later.
- The message field is optional (no required attribute) because not every lead wants to describe their project upfront.
- The submit button uses btn-primary w-100 btn-lg Bootstrap classes to make it full-width and prominent.
- Save templates/index.html.
- Confirm both template files exist by running:
ls templates/
You should see base.html and index.html listed in the output.
Only see one file?
Make sure you created both files inside the templates folder (not in the project root). The folder path should be leadgen-app/templates/base.html and leadgen-app/templates/index.html.
help me find where my template files were saved
✔️ Awesome, I've got everything!
Great. Make sure you have saved index.html before moving on.
ⓧ I'd like to double check the full code
Your index.html should contain both chunks above in order. Scroll up and confirm:
- Chunk 1: The {% extends "base.html" %} line through the closing </div> of the email field.
- Chunk 2: The budget <select> field through {% endblock %}.
Update app.py to serve the landing page
Your templates are ready, but Flask still returns the plain "Hello, World!" string. You need to tell Flask to render your new template instead.
- Open app.py and replace its entire contents with the following code:
from flask import Flask, render_template
app = Flask(__name__)
app.secret_key = 'leadgen-secret-key-change-in-production'
@app.route('/')
def index():
return render_template('index.html')
What changed?
- render_template is imported from Flask. It loads an HTML file from the templates folder and returns it as a response.
- app.secret_key is required for Flask's flash message system to work. Flash messages are stored in the session, which needs a secret key for encryption.
- The index() function now returns render_template('index.html') instead of a plain string.
- Save app.py.
Before you check the browser, what do you think you will see at http://127.0.0.1:5000 now?
- If your Flask server is still running, it will auto-reload thanks to debug mode. If you stopped it, start it again by running:
flask --app app run --debug
- Open http://127.0.0.1:5000 in your browser.
You should see a dark navigation bar at the top with "LeadGen" branding, and a centered card containing a "Get a Free Quote" form with fields for company name, email, budget, timeline, and project description.
Seeing a TemplateNotFound error?
This means Flask cannot find index.html in the templates folder. Double check that the folder is named exactly templates (not "template") and that it sits in the same directory as app.py.
help me fix a TemplateNotFound error in Flask
✔️ Awesome, I've got everything!
Great. Your app.py is only 8 lines. Double check you have saved it.
ⓧ I'd like to double check the full code
from flask import Flask, render_template
app = Flask(__name__)
app.secret_key = 'leadgen-secret-key-change-in-production'
@app.route('/')
def index():
return render_template('index.html')
Your landing page is live and looking sharp. Next up, you will make the form actually do something by saving submissions to a database.
Store Leads in the Database
Your form looks professional, but try clicking Submit Inquiry right now. You get a 404 error because the /submit route does not exist yet. The data vanishes into thin air.
You need a database to catch and store every form submission so leads are never lost. In this step, you will set up SQLite to persist leads, build the submission handler, and show a confirmation page.
In this step, get ready to:
- Set up a SQLite database with a leads table.
- Handle form submissions with validation and store them in the database.
- Show a thank-you confirmation page after a successful submission.
Set up the SQLite database with a leads table
SQLite is a lightweight database built into Python. It stores everything in a single file, so there is nothing extra to install or configure.
- In app.py, find the first line:
from flask import Flask, render_template
- Replace that line with these three imports:
from flask import Flask, render_template, request, redirect, url_for, flash
import sqlite3
from datetime import datetime
What are these new imports?
- request lets you read data from submitted forms.
- redirect and url_for send the user to a different page.
- flash displays one-time error or success messages.
- sqlite3 is Python's built-in database module.
- datetime stamps each lead with the current time.
Now you need two helper functions: one to connect to the database, and one to create the table that stores leads.
- In app.py, find this line:
app.secret_key = 'leadgen-secret-key-change-in-production'
- Add the following code below that line (leave a blank line between them):
def get_db():
# Connect to the SQLite database file
conn = sqlite3.connect('leads.db')
# Allow accessing columns by name instead of index
conn.row_factory = sqlite3.Row
return conn
def init_db():
# Create the leads table if it doesn't exist yet
conn = get_db()
conn.execute('''
CREATE TABLE IF NOT EXISTS leads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_name TEXT NOT NULL,
email TEXT NOT NULL,
budget TEXT NOT NULL,
timeline TEXT NOT NULL,
message TEXT,
score TEXT NOT NULL,
created_at TEXT NOT NULL
)
''')
conn.commit()
conn.close()
# Create the database table when the app starts
init_db()
What does this code do?
get_db() opens a connection to a file called leads.db. If the file does not exist yet, SQLite creates it automatically.
init_db() creates the leads table with columns for every piece of form data, plus a score and timestamp. Calling init_db() at the module level means the table is created the moment your app starts.
- Save app.py.
- Your dev server auto-reloads. Confirm the database file was created by running this in a new terminal:
ls leads.db
You should see leads.db listed. SQLite created the file the moment your app restarted.
Don't see leads.db?
- Make sure your terminal is inside the leadgen-app folder where app.py lives.
- Check the terminal running your Flask server for errors. A syntax mistake in your new code will prevent the app from reloading.
- Verify that init_db() is called at the module level (not indented inside a function).
Help me debug why leads.db is not appearing in my project folder.
Handle form submission with a POST route
The database is ready to receive data. Now you need a route that catches the form submission, checks the data is complete, and inserts it into the table.
- In app.py, add the following route below your existing index() function:
@app.route('/submit', methods=['POST'])
def submit():
# Read form data and strip whitespace
company_name = request.form.get('company_name', '').strip()
email = request.form.get('email', '').strip()
budget = request.form.get('budget', '').strip()
timeline = request.form.get('timeline', '').strip()
message = request.form.get('message', '').strip()
# Validate required fields
if not company_name or not email or not budget or not timeline:
flash('Please fill in all required fields.', 'error')
return redirect(url_for('index'))
# Assign a placeholder score and timestamp
score = 'cold'
created_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Insert the lead into the database
conn = get_db()
conn.execute(
'INSERT INTO leads (company_name, email, budget, timeline, message, score, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
(company_name, email, budget, timeline, message, score, created_at)
)
conn.commit()
conn.close()
return render_template('thankyou.html', score=score)
What does this route do?
- request.form.get() reads each field the visitor typed. The .strip() removes accidental leading/trailing spaces.
- The validation check ensures the four required fields are present. If anything is missing, it flashes an error and sends the user back to the form.
- The ? placeholders in the SQL query prevent SQL injection attacks. Never paste user input directly into a query string.
- Every lead gets a hardcoded score of cold for now. You will add real scoring logic in the next step.
- Save app.py.
- Check the terminal running your Flask server. It should reload without errors.
Seeing an error on reload?
- Make sure the @app.route('/submit', methods=['POST']) decorator is directly above def submit(): with no blank line between them.
- Confirm you have exactly seven ? placeholders matching the seven values in the tuple.
Help me fix a syntax error in my Flask /submit route.
Create the thank-you template
When a submission succeeds, the route renders thankyou.html. That template does not exist yet, so you need to create it.
- Create a new file called templates/thankyou.html and add this code:
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6 text-center">
<div class="card shadow p-5">
<h2 class="text-success mb-3">Thank You!</h2>
<p class="lead">Your inquiry has been received. We will get back to you within 24 hours.</p>
<a href="/" class="btn btn-outline-primary mt-3">Submit Another</a>
</div>
</div>
</div>
{% endblock %}
What does this template do?
It extends your base layout (so the navbar and Bootstrap still appear), shows a green "Thank You!" heading, and gives the visitor a link to submit another inquiry.
- Save templates/thankyou.html.
Before you test, what do you think will happen when you fill out the form and click Submit Inquiry?
- Return to your browser at http://127.0.0.1:5000.
- Fill in the form with any test data. For example: company name Acme Corp, email test@example.com, budget $10,000+, timeline This week.
- Click Submit Inquiry.
You should see the green "Thank You!" card with a confirmation message. Your lead is now saved in the database.
- Confirm the data was stored by checking that leads.db still exists in your project folder (it should have grown in size).
Still seeing a 404 or error page?
- Verify your form's action="/submit" matches the route path @app.route('/submit') exactly.
- Check that methods=['POST'] is present in the route decorator. Without it, Flask only accepts GET requests.
- Make sure thankyou.html is inside the templates folder, not in the project root.
Help me debug why my Flask form submission is not working.
Now test the validation. Try submitting the form with the company name field left empty.
- Clear the company name field and click Submit Inquiry.
- You should see a red alert banner at the top of the page saying "Please fill in all required fields." The form reappears so the visitor can fix their input.
Why does the browser also block empty fields?
The required attribute on your HTML inputs provides client-side validation. Your server-side check with flash() is a safety net for requests that bypass the browser (like API tools or bots).
✔️ Awesome, I've got everything!
Great. Double check you have saved both files.
ⓧ I'd like to double check the full code
Scroll up and confirm each piece is present in your app.py in this order:
- The three import lines at the top (flask imports, sqlite3, datetime).
- app = Flask(__name__) and the secret key line.
- The get_db() function.
- The init_db() function.
- The init_db() call at the module level.
- The / route with index().
- The /submit POST route with submit().
Here is the full templates/thankyou.html for reference:
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6 text-center">
<div class="card shadow p-5">
<h2 class="text-success mb-3">Thank You!</h2>
<p class="lead">Your inquiry has been received. We will get back to you within 24 hours.</p>
<a href="/" class="btn btn-outline-primary mt-3">Submit Another</a>
</div>
</div>
</div>
{% endblock %}
Your app now captures and stores leads. Every submission is safely persisted in leads.db. The one problem? Every lead is labelled "cold" no matter what. Next up, you will add scoring logic that automatically classifies leads as hot, warm, or cold based on their budget and timeline.
Add Lead Scoring Logic
In the last step, you built a form that captures leads and stores them in your SQLite database. Every submission works, but there is a gap.
Every lead gets stamped as "cold" regardless of what the visitor selected. A $10,000 rush job looks identical to a casual browser. The scoring function you write next fixes this by automatically classifying leads based on their budget and timeline.
In this step, get ready to:
- Write a scoring function that classifies leads as hot, warm, or cold.
- Integrate the scoring function into the form submission handler.
- Test that different form inputs produce different scores in the database.
Write the scoring function
The scoring logic uses two signals from the form: budget and timeline. A lead with a big budget AND an urgent timeline is "hot." If only one of those conditions is true, the lead is "warm." Everything else is "cold."
- In app.py, find the init_db() function definition (the one that ends with conn.close()).
- Directly below that function, add the scoring function by pasting this code:
def score_lead(budget, timeline):
# Define which values count as high-priority
high_budgets = ['$10,000+', '$5,000-$10,000']
urgent_timelines = ['This week', 'This month']
# Both conditions met = hot lead
if budget in high_budgets and timeline in urgent_timelines:
return 'hot'
# Only one condition met = warm lead
elif budget in high_budgets or timeline in urgent_timelines:
return 'warm'
# Neither condition met = cold lead
else:
return 'cold'
What does this code do?
This function takes two arguments (the budget and timeline strings from the form) and returns a score.
- high_budgets lists the two budget options that signal a serious buyer.
- urgent_timelines lists timelines that suggest the visitor needs help soon.
- The if/elif/else chain checks whether both, one, or neither condition is true.
- Save app.py.
- Check the terminal where Flask is running. You should see a message like Restarting with stat confirming Flask detected your change and reloaded.
Seeing a syntax error in the terminal?
- Make sure the function is placed between the closing of init_db() and the init_db() call line. It should not be inside another function.
- Check that all lines inside the function are indented with 4 spaces consistently.
Help me fix a syntax error in my Flask app.
Integrate scoring into the submission handler
The function exists, but nothing calls it yet. You need to swap the hardcoded "cold" value in your /submit route for a call to score_lead().
- In app.py, scroll down to the submit() function.
- Find this line:
score = 'cold'
- Replace it with this line:
score = score_lead(budget, timeline)
- Save app.py. Flask will auto-reload again.
Test scoring by submitting leads
Time to prove that different form inputs produce different scores. You will submit two leads with very different profiles and check the database to confirm they were scored correctly.
- Open http://127.0.0.1:5000 in your browser.
- Fill out the form with these values:
- Company Name: Acme Corp
- Email: test@acme.com
- Budget Range: $10,000+
- Project Timeline: This week
Before you hit submit, think about what score this lead should get. Big budget plus urgent timeline. What does that combination produce?
- Click **Submit Inquiry**.
- You should see the Thank You page confirming the submission went through.
Now submit a second lead that should score differently.
- Click Submit Another on the Thank You page.
- Fill out the form with these values:
- Company Name: Tiny Startup
- Email: hello@tiny.io
- Budget Range: Under $1,000
- Project Timeline: Just exploring
- Click **Submit Inquiry**.
Both leads are now in your database. To confirm the scores were assigned correctly, you need to query the database directly.
- Open a new terminal window or tab (keep the Flask server running in the original one).
- Navigate to your project folder and check the database by running this command:
cd ~/Desktop/leadgen-app
python -c "import sqlite3; conn = sqlite3.connect('leads.db'); [print(r) for r in conn.execute('SELECT company_name, score FROM leads').fetchall()]"
You should see output showing Acme Corp with a score of hot and Tiny Startup with a score of cold. The scoring logic is working.
Why check the database directly?
The thank-you page does not display the score yet. In the next step, you will build an admin dashboard that shows all leads with color-coded score badges. For now, querying the database proves the scoring function is working behind the scenes.
All leads still showing as cold?
- Make sure you replaced score = 'cold' with score = score_lead(budget, timeline) in the submit() function.
- Check that score_lead is defined ABOVE the init_db() call line. Python reads top to bottom, so the function must be defined before it is used.
- Old leads submitted before you added the scoring function will still show "cold." Only new submissions get scored.
Help me debug my lead scoring function.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py and that Flask reloaded without errors.
ⓧ I'd like to double check the full code
Here is the complete app.py at this point. Compare yours against it:
from flask import Flask, render_template, request, redirect, url_for, flash
import sqlite3
from datetime import datetime
app = Flask(__name__)
app.secret_key = 'leadgen-secret-key-change-in-production'
def get_db():
conn = sqlite3.connect('leads.db')
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db()
conn.execute('''
CREATE TABLE IF NOT EXISTS leads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_name TEXT NOT NULL,
email TEXT NOT NULL,
budget TEXT NOT NULL,
timeline TEXT NOT NULL,
message TEXT,
score TEXT NOT NULL,
created_at TEXT NOT NULL
)
''')
conn.commit()
conn.close()
def score_lead(budget, timeline):
high_budgets = ['$10,000+', '$5,000-$10,000']
urgent_timelines = ['This week', 'This month']
if budget in high_budgets and timeline in urgent_timelines:
return 'hot'
elif budget in high_budgets or timeline in urgent_timelines:
return 'warm'
else:
return 'cold'
init_db()
@app.route('/')
def index():
return render_template('index.html')
@app.route('/submit', methods=['POST'])
def submit():
company_name = request.form.get('company_name', '').strip()
email = request.form.get('email', '').strip()
budget = request.form.get('budget', '').strip()
timeline = request.form.get('timeline', '').strip()
message = request.form.get('message', '').strip()
if not company_name or not email or not budget or not timeline:
flash('Please fill in all required fields.', 'error')
return redirect(url_for('index'))
score = score_lead(budget, timeline)
created_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
conn = get_db()
conn.execute(
'INSERT INTO leads (company_name, email, budget, timeline, message, score, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
(company_name, email, budget, timeline, message, score, created_at)
)
conn.commit()
conn.close()
return render_template('thankyou.html', score=score)
Your app now automatically scores every lead that comes in. Next up, you will build an admin dashboard where all these scored leads are displayed in a color-coded table so you know exactly who to follow up with first.
Build the Admin Dashboard
Your leads are being captured and scored automatically. But right now there is no way to actually see them without opening the database file directly.
The admin dashboard is where this app becomes genuinely useful. It shows all leads in one place, color-coded by priority, so you know exactly who to follow up with first.
In this step, get ready to:
- Create an admin route that queries all leads from the database.
- Build a dashboard template with a color-coded leads table.
- Add filter buttons so you can view only hot, warm, or cold leads.
Create the admin route that queries all leads
The dashboard needs a route that pulls leads from the database and passes them to a template. This route will also support filtering by score using a URL query parameter.
- In app.py, find the import line at the very top of the file:
from flask import Flask, render_template, request, redirect, url_for, flash
- Replace that line with the following updated imports:
from flask import Flask, render_template, request, redirect, url_for, flash, Response
import sqlite3
import csv
import io
from datetime import datetime
What did we add here?
Response is added to the Flask imports for a CSV export feature you can build later. The csv and io modules are Python built-ins that support that same feature.
- Now scroll to the bottom of app.py (below the submit route) and add the admin route by pasting this code:
@app.route('/admin')
def admin():
score_filter = request.args.get('score', '')
conn = get_db()
if score_filter in ['hot', 'warm', 'cold']:
leads = conn.execute(
'SELECT * FROM leads WHERE score = ? ORDER BY created_at DESC',
(score_filter,)
).fetchall()
else:
leads = conn.execute(
'SELECT * FROM leads ORDER BY created_at DESC'
).fetchall()
conn.close()
return render_template('admin.html', leads=leads, current_filter=score_filter)
What does this route do?
- request.args.get('score', '') reads an optional ?score=hot query parameter from the URL. If none is provided, it defaults to an empty string.
- The if block filters the database query when a valid score filter is present. Otherwise it returns all leads.
- ORDER BY created_at DESC puts the newest leads at the top of the list.
- current_filter is passed to the template so the active filter button can be highlighted.
- Save app.py.
The route is ready, but it references a template that does not exist yet. You will build that next.
Create the admin dashboard template with score badges
The dashboard template renders leads as a table. Each lead's score gets a color-coded badge so you can spot hot leads at a glance.
- Create a new file called admin.html inside the templates folder.
- Start by pasting this code to set up the page structure and table header:
{% extends "base.html" %}
{% block content %}
<h2 class="mb-4">Lead Dashboard</h2>
{% if leads %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-dark">
<tr>
<th>Score</th>
<th>Company</th>
<th>Email</th>
<th>Budget</th>
<th>Timeline</th>
<th>Date</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">No leads captured yet. Go to the <a href="/">capture form</a> to submit one.</div>
{% endif %}
{% endblock %}
What does this template do?
- {% if leads %} shows the table only when leads exist. The {% else %} block shows a friendly empty-state message with a link back to the form.
- table-hover highlights rows when you hover over them. table-responsive makes the table scroll horizontally on small screens.
- Save templates/admin.html and open http://127.0.0.1:5000/admin in your browser.
You should see a "Lead Dashboard" heading with an empty table header. The leads exist in the database but the table body has no content yet.
Now you need to fill in the table body with a row for each lead, including the color-coded score badge.
- In templates/admin.html, find the empty <tbody> and </tbody> tags.
- Paste the following code between them:
{% for lead in leads %}
<tr>
<td>
{% if lead.score == 'hot' %}
<span class="badge bg-danger">Hot</span>
{% elif lead.score == 'warm' %}
<span class="badge bg-warning text-dark">Warm</span>
{% else %}
<span class="badge bg-primary">Cold</span>
{% endif %}
</td>
<td>{{ lead.company_name }}</td>
<td>{{ lead.email }}</td>
<td>{{ lead.budget }}</td>
<td>{{ lead.timeline }}</td>
<td>{{ lead.created_at }}</td>
</tr>
{% endfor %}
How do the score badges work?
The {% if %} block checks each lead's score and applies a different Bootstrap badge class.
- bg-danger (red) for hot leads.
- bg-warning (orange) for warm leads.
- bg-primary (blue) for cold leads.
- Save templates/admin.html and refresh http://127.0.0.1:5000/admin in your browser.
You should see a table listing all the leads you submitted earlier. Each row has a colored badge in the Score column: red for hot, orange for warm, blue for cold.
Don't see any leads in the table?
- If you see the "No leads captured yet" message, go to http://127.0.0.1:5000 and submit a couple of test leads first.
- Check that the {% for lead in leads %} code is between <tbody> and </tbody>, not outside the table.
- help me debug why my admin dashboard shows no leads
Add filter links to show only hot, warm, or cold leads
The admin route already supports filtering via the ?score= query parameter. Now you need filter buttons in the template so you can click to filter without editing the URL manually.
- In templates/admin.html, find this line:
<h2 class="mb-4">Lead Dashboard</h2>
{% if leads %}
- Add the following filter buttons between the <h2> and the {% if leads %} line:
<div class="mb-3">
<a href="/admin" class="btn btn-sm {% if not current_filter %}btn-dark{% else %}btn-outline-dark{% endif %}">All</a>
<a href="/admin?score=hot" class="btn btn-sm {% if current_filter == 'hot' %}btn-danger{% else %}btn-outline-danger{% endif %}">Hot</a>
<a href="/admin?score=warm" class="btn btn-sm {% if current_filter == 'warm' %}btn-warning{% else %}btn-outline-warning{% endif %}">Warm</a>
<a href="/admin?score=cold" class="btn btn-sm {% if current_filter == 'cold' %}btn-primary{% else %}btn-outline-primary{% endif %}">Cold</a>
<a href="/export" class="btn btn-sm btn-outline-success float-end">Export CSV</a>
</div>
What do these buttons do?
Each button links to /admin?score=hot (or warm, or cold). The Jinja2 {% if %} checks make the active filter button solid-colored while inactive buttons stay outlined.
The "Export CSV" button links to /export which does not exist yet. That is the Secret Mission challenge.
- Save templates/admin.html and refresh http://127.0.0.1:5000/admin in your browser.
Before you click the filter buttons, what do you think will happen when you click "Hot"? Will the table show fewer leads?
- Click the Hot button.
The table now shows only leads with a red "Hot" badge. The URL changes to /admin?score=hot and the Hot button appears solid red while the others are outlined.
- Click All to return to the unfiltered view.
Filter buttons not working?
- Make sure current_filter is passed in your route's render_template() call. Without it, the template cannot highlight the active button.
- Check that the href values in your filter links match exactly: /admin?score=hot, /admin?score=warm, /admin?score=cold.
- help me debug my dashboard filter buttons
✔️ Awesome, I've got everything!
Great. Double check you have saved both app.py and templates/admin.html.
ⓧ I'd like to double check the full code
Here is the complete templates/admin.html file. Confirm yours matches:
{% extends "base.html" %}
{% block content %}
<h2 class="mb-4">Lead Dashboard</h2>
<div class="mb-3">
<a href="/admin" class="btn btn-sm {% if not current_filter %}btn-dark{% else %}btn-outline-dark{% endif %}">All</a>
<a href="/admin?score=hot" class="btn btn-sm {% if current_filter == 'hot' %}btn-danger{% else %}btn-outline-danger{% endif %}">Hot</a>
<a href="/admin?score=warm" class="btn btn-sm {% if current_filter == 'warm' %}btn-warning{% else %}btn-outline-warning{% endif %}">Warm</a>
<a href="/admin?score=cold" class="btn btn-sm {% if current_filter == 'cold' %}btn-primary{% else %}btn-outline-primary{% endif %}">Cold</a>
<a href="/export" class="btn btn-sm btn-outline-success float-end">Export CSV</a>
</div>
And the table and else block that follows:
{% if leads %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-dark">
<tr>
<th>Score</th>
<th>Company</th>
<th>Email</th>
<th>Budget</th>
<th>Timeline</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{% for lead in leads %}
<tr>
<td>
{% if lead.score == 'hot' %}
<span class="badge bg-danger">Hot</span>
{% elif lead.score == 'warm' %}
<span class="badge bg-warning text-dark">Warm</span>
{% else %}
<span class="badge bg-primary">Cold</span>
{% endif %}
</td>
<td>{{ lead.company_name }}</td>
<td>{{ lead.email }}</td>
<td>{{ lead.budget }}</td>
<td>{{ lead.timeline }}</td>
<td>{{ lead.created_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">No leads captured yet. Go to the <a href="/">capture form</a> to submit one.</div>
{% endif %}
{% endblock %}
And here is the admin route at the bottom of app.py:
@app.route('/admin')
def admin():
score_filter = request.args.get('score', '')
conn = get_db()
if score_filter in ['hot', 'warm', 'cold']:
leads = conn.execute(
'SELECT * FROM leads WHERE score = ? ORDER BY created_at DESC',
(score_filter,)
).fetchall()
else:
leads = conn.execute(
'SELECT * FROM leads ORDER BY created_at DESC'
).fetchall()
conn.close()
return render_template('admin.html', leads=leads, current_filter=score_filter)
Your lead generation app is fully functional. You can capture leads, score them automatically, and view them on a color-coded dashboard with filtering. Next up is a bonus challenge: exporting your leads as a CSV spreadsheet.
Secret mission
Add CSV Export
Your admin dashboard is great for a quick glance, but what if a stakeholder wants the lead data in a spreadsheet? Add a CSV export feature so anyone can download all leads with one click.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally on your machine, so there are no ongoing costs.
Resources you used:
- leadgen-app project folder on your Desktop (contains all code, templates, and database).
- leads.db SQLite database file (stores all captured lead data).
- venv/ Python virtual environment folder (contains Flask and dependencies).
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your lead generation app.
- Your project folder stays on your Desktop with all files intact.
- To start the app again later, open a terminal, navigate to your leadgen-app folder, activate the virtual environment, and run flask --app app run --debug.
- Your leads.db database retains all previously submitted leads.
Pause - I'll come back to this later
Shut down the running Flask server to free up your terminal. All files remain on disk for when you return.
- In the terminal where Flask is running, press Ctrl+C to stop the server.
- Deactivate your virtual environment by running:
deactivate
- Your project folder and database stay on your Desktop. When you come back, reactivate the environment and start the server again.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your code, templates, database, and virtual environment permanently.
- In the terminal where Flask is running, press Ctrl+C to stop the server.
- Deactivate your virtual environment by running:
deactivate
- Move up one directory and delete the entire project folder.
macOS
cd ~/Desktop
rm -rf leadgen-app
Windows
cd ~\Desktop
rmdir /s /q leadgen-app
- Confirm the folder is gone by running ls (macOS) or dir (Windows) on your Desktop. You should no longer see leadgen-app listed.
Nice Work!
Nice Work!
Nice work! You just built a complete lead generation web application with Python Flask, from an empty folder to a working app that captures, scores, and displays business leads.
You've learned how to:
- Build a Flask web application from scratch with routes, Jinja2 templates, and SQLite database operations.
- Implement server-side lead scoring logic that automatically classifies leads as hot, warm, or cold based on budget and timeline.
- Create an admin dashboard with color-coded badges and query-parameter filtering to manage captured leads at a glance.
- Secret Mission: Added a CSV export feature so you can download all leads as a spreadsheet for reporting or CRM imports.
Ready to quiz yourself?