init
This commit is contained in:
178
.gitignore
vendored
Normal file
178
.gitignore
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
.DS_Store
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
18
LICENSE
Normal file
18
LICENSE
Normal file
@@ -0,0 +1,18 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 WMT-dla-innych
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
53
README.md
Normal file
53
README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Przykładowa aplikacja CRUD FLASK
|
||||
|
||||
Flask to lekki framework webowy dla języka Python, zaliczany do kategorii mikro-frameworków. Oznacza to, że dostarcza jedynie podstawowej infrastruktury do budowy aplikacji internetowych, pozwalając jednocześnie na elastyczne poszerzanie funkcjonalności za pomocą zewnętrznych bibliotek.
|
||||
|
||||
Prosty przykład aplikacji CRUD zaprogramowany przy pomocy Flash i SQLAlchemy.
|
||||
|
||||
## Dlaczego ORM?
|
||||
|
||||
- Operujesz na obiektach (Contact) zamiast pisać SQL i ręcznie mapować kolumny.
|
||||
- Lepsza czytelność i mniejsza szansa na błędy typu literówki w zapytaniach.
|
||||
- Później łatwo dołożyć migracje (Alembic), relacje 1‑N itd.
|
||||
|
||||
|
||||
## Wirtualny katalog projektu
|
||||
|
||||
```
|
||||
W terminalu Visual Studio Code:
|
||||
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Zawartość requiremens.txt
|
||||
|
||||
```
|
||||
Flask>=3.0
|
||||
SQLAlchemy>=2.0
|
||||
Flask-SQLAlchemy>=3.1
|
||||
```
|
||||
|
||||
## Uruchomienie
|
||||
|
||||
python app.py
|
||||
|
||||
lub
|
||||
|
||||
python3 app.py
|
||||
|
||||
## CRUD_flask_example struktura projektu
|
||||
|
||||
```
|
||||
├─ app.py
|
||||
├─ requirements.txt
|
||||
├─ database.db
|
||||
├─ templates/
|
||||
│ ├─ base.html
|
||||
│ ├─ index.html
|
||||
│ └─ form.html
|
||||
└─ static/
|
||||
└─ style.css
|
||||
```
|
||||
|
||||
92
app.py
Normal file
92
app.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from flask import Flask, render_template, request, redirect, url_for
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.update(
|
||||
SQLALCHEMY_DATABASE_URI='sqlite:///' + str(Path(__file__).with_name('database.db')),
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS=False,
|
||||
)
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
class Contact(db.Model):
|
||||
__tablename__ = 'contacts'
|
||||
id: Mapped[int] = mapped_column(db.Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(db.String(255), nullable=False)
|
||||
email: Mapped[Optional[str]] = mapped_column(db.String(255), nullable=True)
|
||||
phone: Mapped[Optional[str]] = mapped_column(db.String(50), nullable=True)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Contact id={self.id} name={self.name!r}>"
|
||||
|
||||
# tworzymy tabelę przy starcie (w projekcie produkcyjnym użyj migracji)
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
@app.get('/')
|
||||
def index():
|
||||
rows = db.session.execute(select(Contact).order_by(Contact.id.desc())).scalars().all()
|
||||
return render_template('index.html', contacts=rows)
|
||||
|
||||
@app.get('/new')
|
||||
def new():
|
||||
return render_template('form.html', contact=None)
|
||||
|
||||
@app.post('/create')
|
||||
def create():
|
||||
name = (request.form.get('name') or '').strip()
|
||||
email = (request.form.get('email') or '').strip()
|
||||
phone = (request.form.get('phone') or '').strip()
|
||||
if not name:
|
||||
return render_template('form.html', contact={'name': name, 'email': email, 'phone': phone}, error='Imię i nazwisko jest wymagane.')
|
||||
c = Contact(name=name, email=email or None, phone=phone or None)
|
||||
db.session.add(c)
|
||||
db.session.commit()
|
||||
return redirect(url_for('index'), code=303)
|
||||
|
||||
@app.get('/edit/<int:cid>')
|
||||
def edit(cid: int):
|
||||
row = db.session.get(Contact, cid)
|
||||
if not row:
|
||||
return ('Nie znaleziono kontaktu', 404)
|
||||
return render_template('form.html', contact=row)
|
||||
|
||||
@app.post('/update')
|
||||
def update():
|
||||
try:
|
||||
cid = int(request.form.get('id'))
|
||||
except (TypeError, ValueError):
|
||||
return ('Błędny identyfikator', 400)
|
||||
row = db.session.get(Contact, cid)
|
||||
if not row:
|
||||
return ('Nie znaleziono kontaktu', 404)
|
||||
name = (request.form.get('name') or '').strip()
|
||||
email = (request.form.get('email') or '').strip()
|
||||
phone = (request.form.get('phone') or '').strip()
|
||||
if not name:
|
||||
return render_template('form.html', contact={'id': cid, 'name': name, 'email': email, 'phone': phone}, error='Imię i nazwisko jest wymagane.')
|
||||
row.name = name
|
||||
row.email = email or None
|
||||
row.phone = phone or None
|
||||
db.session.commit()
|
||||
return redirect(url_for('index'), code=303)
|
||||
|
||||
@app.post('/delete')
|
||||
def delete():
|
||||
try:
|
||||
cid = int(request.form.get('id'))
|
||||
except (TypeError, ValueError):
|
||||
return ('Błędny identyfikator', 400)
|
||||
row = db.session.get(Contact, cid)
|
||||
if not row:
|
||||
return ('Nie znaleziono kontaktu', 404)
|
||||
db.session.delete(row)
|
||||
db.session.commit()
|
||||
return redirect(url_for('index'), code=303)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
BIN
database.db
Normal file
BIN
database.db
Normal file
Binary file not shown.
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
blinker==1.9.0
|
||||
click==8.3.0
|
||||
Flask==3.1.2
|
||||
Flask-SQLAlchemy==3.1.1
|
||||
greenlet==3.2.4
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
MarkupSafe==3.0.3
|
||||
SQLAlchemy==2.0.43
|
||||
typing_extensions==4.15.0
|
||||
Werkzeug==3.1.3
|
||||
79
static/style.css
Normal file
79
static/style.css
Normal file
@@ -0,0 +1,79 @@
|
||||
body{
|
||||
font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;
|
||||
max-width:900px;
|
||||
margin:2rem auto;
|
||||
padding:0 1rem;
|
||||
background:#fff
|
||||
}
|
||||
|
||||
header{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
margin-bottom:1rem
|
||||
}
|
||||
|
||||
nav a{margin-right:.5rem}
|
||||
|
||||
table{
|
||||
border-collapse:collapse;
|
||||
width:100%
|
||||
}
|
||||
|
||||
th,td{
|
||||
border:1px solid #ddd;
|
||||
padding:.5rem
|
||||
}
|
||||
|
||||
th{
|
||||
background:#f6f8fa;
|
||||
text-align:left
|
||||
}
|
||||
tr:nth-child(even){
|
||||
background:#fafbfc
|
||||
}
|
||||
|
||||
.btn{
|
||||
display:inline-block;
|
||||
padding:.35rem .6rem;
|
||||
border:1px solid #888;
|
||||
border-radius:.4rem;
|
||||
text-decoration:none
|
||||
}
|
||||
.btn.primary{
|
||||
background:#0a7;
|
||||
color:#fff;
|
||||
border-color:#0a7
|
||||
}
|
||||
.btn.warn{
|
||||
background:#e33;
|
||||
color:#fff;
|
||||
border-color:#e33
|
||||
}
|
||||
input[type=text], input[type=email]{
|
||||
width:100%;
|
||||
padding:.4rem;
|
||||
border:1px solid #bbb;
|
||||
border-radius:.35rem
|
||||
}
|
||||
|
||||
form .row{
|
||||
display:grid;
|
||||
grid-template-columns:150px 1fr;
|
||||
gap:.5rem;
|
||||
margin:.4rem 0
|
||||
}
|
||||
footer{
|
||||
margin-top:2rem;
|
||||
color:#666;
|
||||
font-size:.9rem
|
||||
}
|
||||
.alert{
|
||||
padding:.5rem .6rem;
|
||||
background:#eef9ff;
|
||||
border:1px solid #b5def7;
|
||||
border-radius:.35rem;
|
||||
margin:.6rem 0
|
||||
}
|
||||
.actions{display:flex;gap:.4rem}
|
||||
.actions form{display:inline}
|
||||
27
templates/base.html
Normal file
27
templates/base.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Książka adresowa{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="{{ url_for('index') }}">Książka adresowa</a></h1>
|
||||
<nav>
|
||||
<a class="btn primary" href="{{ url_for('new') }}">+ Dodaj kontakt</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer>WMT - CRUD - Flask - SQLAlchemy ORM - SQLite</footer>
|
||||
</body>
|
||||
</html>
|
||||
26
templates/form.html
Normal file
26
templates/form.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ 'Edycja' if contact and (contact.id if contact is not mapping else contact.get('id')) else 'Nowy' }} kontakt{% endblock %}
|
||||
{% block content %}
|
||||
{% set cid = (contact.id if contact and contact is not mapping else (contact.get('id') if contact else None)) %}
|
||||
<form method="post" action="{{ url_for('update' if cid else 'create') }}">
|
||||
{% if cid %}
|
||||
<input type="hidden" name="id" value="{{ cid }}">
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<label>Imię i nazwisko</label>
|
||||
<input type="text" name="name" value="{{ (contact.name if contact and contact is not mapping else (contact['name'] if contact else ''))|e }}" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Email</label>
|
||||
<input type="email" name="email" value="{{ (contact.email if contact and contact is not mapping else (contact['email'] if contact else ''))|e }}">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Telefon</label>
|
||||
<input type="text" name="phone" value="{{ (contact.phone if contact and contact is not mapping else (contact['phone'] if contact else ''))|e }}">
|
||||
</div>
|
||||
<p>
|
||||
<button class="btn primary" type="submit">Zapisz</button>
|
||||
<a class="btn" href="{{ url_for('index') }}">Anuluj</a>
|
||||
</p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
26
templates/index.html
Normal file
26
templates/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Lista kontaktów{% endblock %}
|
||||
{% block content %}
|
||||
<table>
|
||||
<tr><th>ID</th><th>Imię i nazwisko</th><th>Email</th><th>Telefon</th><th>Akcje</th></tr>
|
||||
{% if contacts %}
|
||||
{% for c in contacts %}
|
||||
<tr>
|
||||
<td>{{ c.id }}</td>
|
||||
<td>{{ c.name }}</td>
|
||||
<td>{{ c.email or '' }}</td>
|
||||
<td>{{ c.phone or '' }}</td>
|
||||
<td class="actions">
|
||||
<a class="btn" href="{{ url_for('edit', cid=c.id) }}">Edytuj</a>
|
||||
<form method="post" action="{{ url_for('delete') }}" onsubmit="return confirm('Usunąć?');">
|
||||
<input type="hidden" name="id" value="{{ c.id }}">
|
||||
<button class="btn warn" type="submit">Usuń</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr><td colspan="5"><i>Brak adresów</i></td></tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user