commit 66eab0aeb5d415669f359448fc56892404204889 Author: Leszek Klich Date: Thu Nov 27 16:25:46 2025 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd00da9 --- /dev/null +++ b/.gitignore @@ -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 + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9f7a735 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a18eabf --- /dev/null +++ b/README.md @@ -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 +``` + diff --git a/app.py b/app.py new file mode 100644 index 0000000..5283184 --- /dev/null +++ b/app.py @@ -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"" + +# 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/') +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) diff --git a/database.db b/database.db new file mode 100644 index 0000000..5dcb00e Binary files /dev/null and b/database.db differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8d1a635 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..d3b7de3 --- /dev/null +++ b/static/style.css @@ -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} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..d512e9c --- /dev/null +++ b/templates/base.html @@ -0,0 +1,27 @@ + + + + + +{% block title %}Książka adresowa{% endblock %} + + + +
+

Książka adresowa

+ +
+ +{% if error %} +
{{ error }}
+{% endif %} + +
+{% block content %}{% endblock %} +
+ +
WMT - CRUD - Flask - SQLAlchemy ORM - SQLite
+ + \ No newline at end of file diff --git a/templates/form.html b/templates/form.html new file mode 100644 index 0000000..62cec97 --- /dev/null +++ b/templates/form.html @@ -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)) %} +
+{% if cid %} + +{% endif %} +
+ + +
+
+ + +
+
+ + +
+

+ +Anuluj +

+
+{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..e2fbe75 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% block title %}Lista kontaktów{% endblock %} +{% block content %} + + +{% if contacts %} +{% for c in contacts %} + + + + + + + +{% endfor %} +{% else %} + +{% endif %} +
IDImię i nazwiskoEmailTelefonAkcje
{{ c.id }}{{ c.name }}{{ c.email or '' }}{{ c.phone or '' }} +Edytuj +
+ + +
+
Brak adresów
+{% endblock %} \ No newline at end of file