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