Compare commits
3 Commits
fc6b3a7fa0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8aa8eda6ce | |||
| 2d56d06649 | |||
| 382e514d03 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
**/*.pyc
|
||||||
51
Dockerfile
Normal file
51
Dockerfile
Normal 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" \
|
||||||
|
]
|
||||||
21
Makefile
21
Makefile
@ -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)
|
||||||
|
|||||||
35
README.md
35
README.md
@ -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).
|
||||||
|
|||||||
6987
media/introduction.excalidraw
Normal file
6987
media/introduction.excalidraw
Normal file
File diff suppressed because it is too large
Load Diff
27
poetry.lock
generated
27
poetry.lock
generated
@ -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"
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user