Mini projekt: Knjižnica¶
Zakaj mini projekt¶
Mini projekt je točka, kjer se posamezna poglavja končno združijo. Cilj ni zgraditi kompleksnega informacijskega sistema, ampak eno majhno, čisto in razumljivo spletno aplikacijo, ki pokaže celoten tok:
- HTML struktura,
- CSS videz,
- obrazec,
- Flask backend,
- SQLite baza,
- prikaz podatkov.
Funkcionalni cilj projekta¶
Projekt “Knjižnica” naj omogoča:
- prikaz seznama knjig,
- dodajanje nove knjige,
- osnovno validacijo,
- shranjevanje v SQLite,
- prikaz napak in uspešen povratek na seznam.
Struktura projekta¶
knjiznica/
├─ app.py
├─ schema.sql
├─ templates/
│ ├─ base.html
│ ├─ index.html
│ └─ dodaj.html
└─ static/
└─ style.css
schema.sql¶
DROP TABLE IF EXISTS knjige;
CREATE TABLE knjige (
id INTEGER PRIMARY KEY AUTOINCREMENT,
naslov TEXT NOT NULL,
avtor TEXT NOT NULL,
leto INTEGER
);
app.py¶
import sqlite3
from pathlib import Path
from flask import Flask, g, redirect, render_template, request, url_for
BASE_DIR = Path(__file__).resolve().parent
DATABASE = BASE_DIR / "knjiznica.db"
app = Flask(__name__)
def get_db():
if "db" not in g:
g.db = sqlite3.connect(DATABASE)
g.db.row_factory = sqlite3.Row
return g.db
@app.teardown_appcontext
def close_db(exception):
db = g.pop("db", None)
if db is not None:
db.close()
def init_db():
db = sqlite3.connect(DATABASE)
schema = (BASE_DIR / "schema.sql").read_text(encoding="utf-8")
db.executescript(schema)
db.commit()
db.close()
@app.route("/")
def index():
db = get_db()
knjige = db.execute(
"SELECT id, naslov, avtor, leto FROM knjige ORDER BY naslov"
).fetchall()
return render_template("index.html", knjige=knjige)
@app.route("/dodaj", methods=["GET", "POST"])
def dodaj():
if request.method == "POST":
naslov = request.form.get("naslov", "").strip()
avtor = request.form.get("avtor", "").strip()
leto_raw = request.form.get("leto", "").strip()
if not naslov or not avtor:
return render_template(
"dodaj.html",
napaka="Naslov in avtor sta obvezna.",
vrednosti={"naslov": naslov, "avtor": avtor, "leto": leto_raw},
)
leto = int(leto_raw) if leto_raw else None
db = get_db()
db.execute(
"INSERT INTO knjige (naslov, avtor, leto) VALUES (?, ?, ?)",
(naslov, avtor, leto),
)
db.commit()
return redirect(url_for("index"))
return render_template("dodaj.html", vrednosti={"naslov": "", "avtor": "", "leto": ""})
if __name__ == "__main__":
if not DATABASE.exists():
init_db()
app.run(debug=True)
Predloge¶
=== "templates/base.html"
```html
<!doctype html>
<html lang="sl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Knjižnica{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header class="site-header">
<div class="container">
<h1>Moja knjižnica</h1>
<nav aria-label="Glavna navigacija">
<a href="{{ url_for('index') }}">Domov</a>
<a href="{{ url_for('dodaj') }}">Dodaj knjigo</a>
</nav>
</div>
</header>
<main class="container">
{% block content %}{% endblock %}
</main>
</body>
</html>
```
=== "templates/index.html"
```html
{% extends "base.html" %}
{% block title %}Seznam knjig{% endblock %}
{% block content %}
<section class="panel">
<div class="panel-head">
<h2>Seznam knjig</h2>
<a class="button-link" href="{{ url_for('dodaj') }}">Dodaj novo knjigo</a>
</div>
{% if knjige %}
<table class="data-table">
<thead>
<tr>
<th>Naslov</th>
<th>Avtor</th>
<th>Leto</th>
</tr>
</thead>
<tbody>
{% for knjiga in knjige %}
<tr>
<td>{{ knjiga["naslov"] }}</td>
<td>{{ knjiga["avtor"] }}</td>
<td>{{ knjiga["leto"] or "—" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>V bazi še ni knjig.</p>
{% endif %}
</section>
{% endblock %}
```
=== "templates/dodaj.html"
```html
{% extends "base.html" %}
{% block title %}Dodaj knjigo{% endblock %}
{% block content %}
<section class="panel">
<h2>Dodaj knjigo</h2>
{% if napaka %}
<p class="error">{{ napaka }}</p>
{% endif %}
<form class="book-form" action="{{ url_for('dodaj') }}" method="post">
<div>
<label for="naslov">Naslov</label>
<input
id="naslov"
name="naslov"
type="text"
required
value="{{ vrednosti.naslov }}">
</div>
<div>
<label for="avtor">Avtor</label>
<input
id="avtor"
name="avtor"
type="text"
required
value="{{ vrednosti.avtor }}">
</div>
<div>
<label for="leto">Leto izida</label>
<input
id="leto"
name="leto"
type="number"
min="0"
value="{{ vrednosti.leto }}">
</div>
<button type="submit">Shrani knjigo</button>
</form>
</section>
{% endblock %}
```
static/style.css¶
* {
box-sizing: border-box;
}
body {
font-family: system-ui, sans-serif;
line-height: 1.6;
margin: 0;
background: #f7f7f8;
color: #222;
}
.container {
width: min(100% - 2rem, 70rem);
margin: 0 auto;
}
.site-header {
background: #1f2937;
color: white;
padding: 1rem 0;
}
.site-header .container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.site-header a {
color: white;
text-decoration: none;
margin-right: 1rem;
}
main.container {
padding: 1.5rem 0;
}
.panel {
background: white;
padding: 1rem;
border-radius: 0.75rem;
box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.06);
}
.panel-head {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1rem;
}
.button-link,
button {
display: inline-block;
border: 0;
border-radius: 0.5rem;
padding: 0.75rem 1rem;
text-decoration: none;
cursor: pointer;
}
button {
width: fit-content;
}
.book-form {
display: grid;
gap: 1rem;
max-width: 32rem;
}
.book-form label {
display: block;
font-weight: 600;
margin-bottom: 0.35rem;
}
.book-form input {
width: 100%;
padding: 0.7rem;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
text-align: left;
padding: 0.75rem;
border-bottom: 1px solid #ddd;
}
.error {
padding: 0.75rem;
border-radius: 0.5rem;
background: #fee2e2;
margin-bottom: 1rem;
}
@media (min-width: 768px) {
.site-header .container,
.panel-head {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
Kako projekt razložiti po korakih¶
Namesto da dijakom takoj pokažeš vse datoteke, je boljša naslednja pot:
- najprej pokaži
schema.sql, - nato najpreprostejši
app.py, - nato
index.html, - nato
dodaj.html, - nato CSS,
- nato validacijo in preusmeritev.
S tem jim zmanjšaš občutek, da je projekt “prevelik”.
Kaj se dijak nauči iz tega projekta¶
S tem projektom se dijak nauči:
- organizirati majhen projekt po mapah,
- napisati osnovno podatkovno shemo,
- prebrati in prikazati podatke iz baze,
- oddati obrazec,
- validirati osnovna polja,
- shraniti podatke v SQLite,
- uporabiti dedovanje predlog,
- uporabiti osnovno odzivno postavitev.
Možne razširitve¶
Če ostane čas, lahko projekt razširiš z:
- iskanjem po naslovu,
- filtriranjem po avtorju,
- urejanjem zapisa,
- brisanjem zapisa,
- ločeno tabelo
avtorji, - preprosto statistiko števila knjig.
Tipične napake pri projektu¶
- napačna imena polj v HTML,
- pozabljen
commit(), - napačna pot do CSS,
- manjkajoča mapa
templates, - pretirana panika ob prvi
500napaki.
Minimalen kriterij uspeha¶
Projekt je za osnovni nivo uspešen, če:
- se aplikacija zažene,
- seznam knjig deluje,
- dodajanje deluje,
- podatki ostanejo v bazi,
- obrazec vrne razumljivo napako pri praznih poljih.
Realno merilo
Za 16-urni sklop je to že zelo lep in pošten rezultat. Ni treba graditi “Amazon za knjige”, da bi bil predmet uspešen.