Web Input, Uploads, Databases, and Testing
You can read this note directly if you know basic Flask routes and HTML forms. This note connects user input to validation, processing, storage, retrieval, and displayed output.
Beginner Mental Model
A complete small web application often follows this path:
HTML form
-> Flask request
-> Python validation and processing
-> SQLite storage or retrieval
-> Jinja template
-> browser displayFor an uploaded image, the file itself is normally saved in a folder while useful metadata, such as its title and filename, is stored in the database.
When something fails, identify the first broken layer rather than changing every part at once.
Form to Flask Flow
Caption: Form values travel to a Flask route in a request; Flask processes them and returns rendered HTML for the browser to display.
HTML:
<form method="post" action="/add">
<label for="title">Title</label>
<input id="title" name="title" type="text">
<button type="submit">Add</button>
</form>Flask:
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route("/add", methods=["POST"])
def add():
title = request.form["title"].strip()
if title == "":
return render_template(
"form.html",
error="Enter a title."
)
return render_template("result.html", title=title)The server must validate input even if the HTML form also uses client-side restrictions. A user can bypass or alter browser-side checks.
Input Validation
Useful checks include:
| Input | Possible check |
|---|---|
| required text | not empty after removing surrounding spaces |
| score | integer and within an allowed range |
| filename | non-empty and acceptable extension |
| category | one of the allowed values |
| identifier | expected format and length |
Validation should happen before database insertion or file saving where possible.
Example:
score_text = request.form["score"]
try:
score = int(score_text)
except ValueError:
return render_template("form.html", error="Enter a whole number.")
if not 0 <= score <= 100:
return render_template("form.html", error="Score must be from 0 to 100.")Image Upload Flow
Caption: A complete upload flow validates the input, saves the file, stores its metadata in SQL, retrieves the record, and displays the result.
HTML:
<form method="post"
action="/upload"
enctype="multipart/form-data">
<label for="photo">Image</label>
<input id="photo" type="file" name="photo">
<button type="submit">Upload image</button>
</form>Flask route:
from pathlib import Path
from flask import Flask, request
app = Flask(__name__)
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
@app.route("/upload", methods=["POST"])
def upload():
photo = request.files["photo"]
if photo.filename == "":
return "No file selected", 400
save_path = UPLOAD_DIR / photo.filename
photo.save(save_path)
return "Uploaded"Beginner trace:
| Step | What happens |
|---|---|
| 1 | browser sends the file using multipart/form-data |
| 2 | Flask reads it from request.files["photo"] |
| 3 | Python checks whether the submission is acceptable |
| 4 | server saves the file |
| 5 | database stores metadata if required |
| 6 | a template displays the result |
For real systems, filenames must be handled carefully because they come from users. The exam-core point is to validate input and avoid blindly trusting submitted data.
SQLite Table
Example table for uploaded-image metadata:
CREATE TABLE IF NOT EXISTS Photo (
photo_id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
filename TEXT NOT NULL
);Python setup:
import sqlite3
def open_database():
connection = sqlite3.connect("gallery.db")
connection.row_factory = sqlite3.Row
return connectionCreate the table:
def create_table(connection):
connection.execute("""
CREATE TABLE IF NOT EXISTS Photo (
photo_id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
filename TEXT NOT NULL
)
""")
connection.commit()Parameterised SQL
Insert values using parameters:
def add_photo(connection, title, filename):
connection.execute(
"INSERT INTO Photo (title, filename) VALUES (?, ?)",
(title, filename)
)
connection.commit()Do not build SQL by concatenating form input:
# Avoid this
sql = "INSERT INTO Photo (title) VALUES ('" + title + "')"Parameters keep data values separate from SQL structure and reduce SQL injection risk.
Retrieval
def get_photos(connection):
return connection.execute(
"""
SELECT photo_id, title, filename
FROM Photo
ORDER BY title
"""
).fetchall()A route can pass the rows to a template:
@app.route("/photos")
def photos():
with open_database() as connection:
rows = get_photos(connection)
return render_template("photos.html", photos=rows)Displaying Formatted Output
<table>
<tr>
<th>Title</th>
<th>File</th>
</tr>
{% for photo in photos %}
<tr>
<td>{{ photo["title"] }}</td>
<td>{{ photo["filename"] }}</td>
</tr>
{% endfor %}
</table>The route variable and template variable must agree:
render_template("photos.html", photos=rows){% for photo in photos %}If uploaded images are to be displayed, Flask must also provide a route or suitable static location from which the browser can request each saved file.
One Complete Processing Sequence
1. user enters title and chooses an image
2. browser submits text fields and file
3. Flask reads request.form and request.files
4. Python validates both inputs
5. image is saved to an upload folder
6. title and filename are inserted into SQLite
7. transaction is committed
8. rows are selected from SQLite
9. Jinja renders a table or image gallery
10. browser displays the responseThis sequence separates three different things:
- file content stored in the file system;
- metadata stored in the database;
- HTML presentation generated by the template.
Local Server Testing
The syllabus expects testing on a local server.
Run the application and visit its local address, commonly:
http://127.0.0.1:5000Test cases should cover more than the happy path.
| Test category | Example | Expected result |
|---|---|---|
| normal | valid title and image | record saved and displayed |
| extreme | shortest allowed title | accepted if within rule |
| invalid | unsupported file type | clear rejection message |
| empty | no title or no file | form redisplayed with guidance |
| persistence | restart app after insert | stored database row remains |
| route | visit expected URL | correct page and status |
| method | submit form using intended method | route accepts request |
| display | retrieve several rows | complete table shown |
Debugging Map
| Symptom | Likely place to check |
|---|---|
| 404 response | requested URL and @app.route path |
| 405 response | form method and route methods |
| form value missing | input name and request.form[...] key |
| upload missing | enctype and request.files |
| file not saved | upload path and save() call |
| row not stored | SQL, parameters, and commit() |
| table blank | query result and template variable |
| image broken | file-serving route or generated image URL |
| CSS missing | static/ folder and stylesheet URL |
A useful debugging question is:
What evidence proves that this layer produced the expected value?Common Mistakes
- Forgetting
enctype="multipart/form-data"for uploads. - Reading uploaded files from
request.form. - Trusting a user-supplied filename without checks.
- Saving a file but not recording its filename when metadata is required.
- Recording a filename but never saving the file.
- Forgetting to commit an INSERT or UPDATE.
- Concatenating form data into SQL.
- Passing one template variable name but looping over another.
- Testing only valid input.
Paper 1 and Paper 2 Emphasis
Paper 1-style reasoning: trace the movement of data, identify missing form attributes, explain parameterised SQL, and propose normal, extreme, invalid, and empty tests.
Paper 2-style practical work: complete form handling, validate values, save an upload, insert and retrieve SQLite rows, render a table, and verify the application locally.
Check Your Understanding
- Where does Flask find ordinary text fields and uploaded files?
- Why should validation occur on the server?
- Why use SQL parameters?
- What is the difference between saving an image and storing its metadata?
- Give one normal and one invalid local-server test.
Answers:
- Text fields are in
request.form; uploaded files are inrequest.files. - Browser-side checks can be bypassed or changed.
- They separate values from SQL structure and reduce injection risk.
- The image bytes are saved as a file; descriptive fields such as title and filename can be stored as database rows.
- Normal: valid title and image are stored and displayed. Invalid: unsupported file type is rejected with a clear message.