3 Commits

Author SHA1 Message Date
8aa8eda6ce Tweaks 2024-05-23 06:54:48 +02:00
2d56d06649 Dockerize 2024-05-18 19:19:16 +02:00
382e514d03 City as select 2024-05-16 21:40:28 +02:00
12 changed files with 7201 additions and 70 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
**/*.pyc

51
Dockerfile Normal file
View File

@ -0,0 +1,51 @@
FROM python:3.11-slim-bookworm AS env-builder
WORKDIR /app
COPY pyproject.toml .
COPY poetry.lock .
# create virtual environment
RUN python -m venv /venv
# set python thingies, set environment variables and activate virtual environment
ENV \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/venv/bin:$PATH"
RUN \
pip install poetry && \
# dump python dependencies into requirements file
poetry export --without-hashes --format=requirements.txt > requirements.txt && \
# install python libs
pip install -r requirements.txt --no-cache-dir --prefer-binary --no-deps --no-compile
FROM python:3.11-slim-bookworm
WORKDIR /app
COPY --from=env-builder /venv /venv
# set python thingies and activate virtual environment
ENV \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/venv/bin:$PATH"
COPY manage.py .
COPY pyproject.toml .
COPY poetry.lock .
COPY db.template.sqlite3 db.sqlite3
COPY ./project ./project
# run as user www-data
USER 33
ENTRYPOINT [ "/venv/bin/gunicorn" ]
CMD [ \
"--bind", "0.0.0.0:8000", \
"--workers", "4", \
"project.wsgi" \
]

View File

@ -1,3 +1,7 @@
CONTAINER_NAME=django-htmx
IMAGE_NAME=django-htmx
ifeq ($(VIRTUAL_ENV),) ifeq ($(VIRTUAL_ENV),)
RUN_IN_ENV=poetry run RUN_IN_ENV=poetry run
else else
@ -19,3 +23,20 @@ migrations:
migrate: migrate:
@ $(RUN_IN_ENV) python manage.py migrate @ $(RUN_IN_ENV) python manage.py migrate
docker-build:
- @docker image rm $(IMAGE_NAME) --force
@docker \
build . \
-t $(IMAGE_NAME)
@docker \
build . \
-t $(IMAGE_NAME)
docker-run:
@docker run \
--publish 8000:8000 \
--name $(CONTAINER_NAME) \
$(IMAGE_NAME)

View File

@ -1 +1,36 @@
# Django-htmx demo # Django-htmx demo
## Run demo
### As docker container
```
make docker-build
make docker-run
```
Browse to [localhost:8000](http://localhost:8000).
Later, start and stop docker container using:
```
docker start django-htmx
```
and
```
docker stop django-htmx
```
### As standard local Django app
- requirements: Python 3.10 or 3.11
- [poetry](https://python-poetry.org)
```
poetry install
make run
```
Browse to [localhost:8000](http://localhost:8000).

File diff suppressed because it is too large Load Diff

27
poetry.lock generated
View File

@ -19,13 +19,13 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
[[package]] [[package]]
name = "django" name = "django"
version = "5.0.4" version = "5.0.6"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
files = [ files = [
{file = "Django-5.0.4-py3-none-any.whl", hash = "sha256:916423499d75d62da7aa038d19aef23d23498d8df229775eb0a6309ee1013775"}, {file = "Django-5.0.6-py3-none-any.whl", hash = "sha256:8363ac062bb4ef7c3f12d078f6fa5d154031d129a15170a1066412af49d30905"},
{file = "Django-5.0.4.tar.gz", hash = "sha256:4bd01a8c830bb77a8a3b0e7d8b25b887e536ad17a81ba2dce5476135c73312bd"}, {file = "Django-5.0.6.tar.gz", hash = "sha256:ff1b61005004e476e0aeea47c7f79b85864c70124030e95146315396f1e7951f"},
] ]
[package.dependencies] [package.dependencies]
@ -74,13 +74,13 @@ tornado = ["tornado (>=0.2)"]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.3" version = "3.1.4"
description = "A very fast and expressive template engine." description = "A very fast and expressive template engine."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
] ]
[package.dependencies] [package.dependencies]
@ -171,19 +171,18 @@ files = [
[[package]] [[package]]
name = "sqlparse" name = "sqlparse"
version = "0.4.4" version = "0.5.0"
description = "A non-validating SQL parser." description = "A non-validating SQL parser."
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.8"
files = [ files = [
{file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"},
{file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"},
] ]
[package.extras] [package.extras]
dev = ["build", "flake8"] dev = ["build", "hatch"]
doc = ["sphinx"] doc = ["sphinx"]
test = ["pytest", "pytest-cov"]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
@ -209,5 +208,5 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = ">= 3.10, < 3.12"
content-hash = "6c86d5721314c92afa919983780e664a1573ebb7c25a8b54622393b97990e509" content-hash = "26984438b0e835c052014186db2a76cbe044e19fd5b56ba3fc3d642b2ed530dc"

View File

@ -1,4 +1,22 @@
{% macro inline_table_row(person, is_editing, errors={}) %} {% macro inline_table_row(person) %}
<tr hx-target="this" hx-swap="outerHTML">
<td>{{ person.name }}</td>
<td>{{ person.address }}</td>
<td>{{ person.city }}</td>
<td>
<button
class="btn btn-outline-primary"
hx-get="{{ url("table-inline-edit-row", pk=person.pk) }}"
>
<i class="bi bi-pencil-square"></i>
</button>
</td>
</tr>
{% endmacro %}
{% macro inline_table_row_edit(person, cities, errors={}) %}
{% macro render_input(field_name, value) %} {% macro render_input(field_name, value) %}
{% set has_error = field_name in errors %} {% set has_error = field_name in errors %}
<input <input
@ -9,48 +27,52 @@
> >
{% endmacro %} {% endmacro %}
{% if is_editing %} {% macro render_select(field_name, value, options) %}
<tr id="person-row-{{ person.pk }}" hx-target="this" hx-swap="outerHTML"> {% set has_error = field_name in errors %}
<td> <select
{{ render_input(field_name="name", value=person.name) }} class="form-select {% if has_error %}is-invalid{% endif %}"
</td> name="{{ field_name }}"
<td> {% if has_error %}title="{{ errors[field_name] }}"{% endif %}
{{ render_input(field_name="address", value=person.address) }} >
</td> {% for option in options %}
<td> {% set selected = value == option %}
{{ render_input(field_name="city", value=person.city) }} <option value="{{ option }}" {% if selected %}selected{% endif %}>
</td> {{ option }}
<td> </option>
<button {% endfor %}
class="btn btn-outline-success" </select>
hx-post="{{ url("table-inline-edit-row", pk=person.pk) }}" {% endmacro %}
hx-include="#person-row-{{ person.pk }} input"
>
<i class="bi bi-check-circle-fill"></i>
</button>
<button
class="btn btn-outline-danger"
hx-get="{{ url("table-inline-edit-row", pk=person.pk) }}"
hx-vals='{"action": "cancel"}'
>
<i class="bi bi-x-circle-fill"></i>
</button>
</td>
</tr>
{% else %}
<tr hx-target="this" hx-swap="outerHTML">
<td>{{ person.name }}</td>
<td>{{ person.address }}</td>
<td>{{ person.city }}</td>
<td>
<button
class="btn btn-outline-primary"
hx-get="{{ url("table-inline-edit-row", pk=person.pk) }}"
> <tr
<i class="bi bi-pencil-square"></i> id="person-row-{{ person.pk }}"
</button> hx-target="this"
</td> hx-swap="outerHTML"
</tr> >
{% endif %} <td>
{{ render_input(field_name="name", value=person.name) }}
</td>
<td>
{{ render_input(field_name="address", value=person.address) }}
</td>
<td>
{# {{ render_input(field_name="city", value=person.city) }}#}
{{ render_select(field_name="city", value=person.city, options=cities) }}
</td>
<td>
<button
class="btn btn-outline-success"
hx-post="{{ url("table-inline-edit-row", pk=person.pk) }}"
hx-include="#person-row-{{ person.pk }} input, #person-row-{{ person.pk }} select"
>
<i class="bi bi-check-circle-fill"></i>
</button>
<button
class="btn btn-outline-danger"
hx-get="{{ url("table-inline-edit-row", pk=person.pk) }}"
hx-vals='{"action": "cancel"}'
>
<i class="bi bi-x-circle-fill"></i>
</button>
</td>
</tr>
{% endmacro %} {% endmacro %}

View File

@ -17,7 +17,7 @@
</thead> </thead>
<tbody> <tbody>
{% for person in persons %} {% for person in persons %}
{{ inline_table_row(person, is_editing=False) }} {{ inline_table_row(person) }}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@ -1,3 +1,7 @@
{% from "main/components/inline_table_row.html" import inline_table_row %} {% from "main/components/inline_table_row.html" import inline_table_row, inline_table_row_edit %}
{{ inline_table_row(person, is_editing=is_editing, errors=errors) }} {% if is_editing %}
{{ inline_table_row_edit(person, cities=cities, errors=errors) }}
{% else %}
{{ inline_table_row(person) }}
{% endif %}

View File

@ -1,4 +1,4 @@
from typing import Any, Optional from typing import Any, Optional, Iterator
from django.db.models import Count, Q from django.db.models import Count, Q
@ -6,18 +6,19 @@ from project.main.models import CatBreed
from project.main.views.demo_view_base import DemoViewBase from project.main.views.demo_view_base import DemoViewBase
def get_countries() -> list[str]: def get_countries() -> Iterator[str]:
ann = ( ann = (
CatBreed.objects.values("country") CatBreed.objects.values("country")
.annotate(Count("country")) .annotate(Count("country"))
.order_by("country") .order_by("country")
) )
return [a["country"] for a in ann] for a in ann:
yield a["country"]
def filter_cat_breeds( def filter_cat_breeds(
breed_filter: Optional[str] = None, country_filter: Optional[str] = None breed_filter: Optional[str] = None, country_filter: Optional[str] = None
) -> list[CatBreed]: ) -> Iterator[CatBreed]:
q = Q() q = Q()
if breed_filter: if breed_filter:
@ -25,7 +26,8 @@ def filter_cat_breeds(
if country_filter: if country_filter:
q &= Q(country=country_filter) q &= Q(country=country_filter)
return CatBreed.objects.filter(q).order_by("name") for c in CatBreed.objects.filter(q).order_by("name"):
yield c
class FilterListView(DemoViewBase): class FilterListView(DemoViewBase):

View File

@ -8,6 +8,15 @@ from django.shortcuts import render
from project.main.models import Person from project.main.models import Person
from project.main.views.demo_view_base import DemoViewBase from project.main.views.demo_view_base import DemoViewBase
CITIES: list[str] = [
"",
"Zagreb",
"Split",
"Pula",
"Rijeka",
"Kozari bok",
]
def get_person(pk: int) -> Person: def get_person(pk: int) -> Person:
try: try:
@ -46,6 +55,7 @@ class TableInlineEditRowView(DemoViewBase):
context_data.update( context_data.update(
{ {
"person": person, "person": person,
"cities": CITIES,
"is_editing": action == "edit", "is_editing": action == "edit",
} }
) )
@ -66,12 +76,11 @@ class TableInlineEditRowView(DemoViewBase):
else: else:
person.save() person.save()
print(errors)
return render( return render(
context={ context={
"person": person, "person": person,
"errors": errors, "errors": errors,
"cities": CITIES,
"is_editing": errors is not None, "is_editing": errors is not None,
}, },
template_name=self.template_name, template_name=self.template_name,

View File

@ -6,7 +6,7 @@ authors = ["Eden Kirin <eden@ekirin.com>"]
readme = "README.md" readme = "README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = ">= 3.10, < 3.12"
django = "^5.0.4" django = "^5.0.4"
django-jinja = "^2.11.0" django-jinja = "^2.11.0"
gunicorn = "^21.2.0" gunicorn = "^21.2.0"