diff --git a/demo/core/__init__.py b/demo/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/demo/core/apps.py b/demo/core/apps.py new file mode 100644 index 00000000..c0ce093b --- /dev/null +++ b/demo/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "core" diff --git a/demo/core/utils.py b/demo/core/utils.py new file mode 100644 index 00000000..e991b225 --- /dev/null +++ b/demo/core/utils.py @@ -0,0 +1,29 @@ +from django.core.paginator import InvalidPage +from django.core.paginator import Paginator +from django.db.models import QuerySet +from django.http import Http404 +from django.http import HttpRequest +from django.utils.translation import gettext_lazy as _ + + +def paginate_queryset(request: HttpRequest, queryset: QuerySet, page_size: int = 20): + """ + Paginates a queryset, and returns a page object. + copied from https://github.com/carltongibson/neapolitan/blob/main/src/neapolitan/views.py + """ + paginator = Paginator(queryset, page_size) + page_number = request.GET.get("page") or 1 + try: + page_number = int(page_number) + except ValueError: + if page_number == "last": + page_number = paginator.num_pages + else: + msg = "Page is not 'last', nor can it be converted to an int." + raise Http404(_(msg)) + + try: + return paginator.page(page_number) + except InvalidPage as exc: + msg = "Invalid page (%s): %s" + raise Http404(_(msg) % (page_number, str(exc))) diff --git a/demo/demo/settings.py b/demo/demo/settings.py index 6d8f48d5..944569eb 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -36,6 +36,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "core", "products", ] diff --git a/demo/demo/urls.py b/demo/demo/urls.py index 54592e2b..d5b2cd9a 100644 --- a/demo/demo/urls.py +++ b/demo/demo/urls.py @@ -17,10 +17,11 @@ from django.contrib import admin from django.urls import include from django.urls import path +from django.urls import reverse_lazy from django.views.generic.base import RedirectView urlpatterns = [ path("admin/", admin.site.urls), - path("", RedirectView.as_view(url="/products/")), + path("", RedirectView.as_view(url=reverse_lazy("products:product_list"))), path("products/", include("products.urls")), ] diff --git a/demo/products/forms.py b/demo/products/forms.py new file mode 100644 index 00000000..57193198 --- /dev/null +++ b/demo/products/forms.py @@ -0,0 +1,9 @@ +from django.forms import ModelForm + +from .models import Product + + +class ProductForm(ModelForm): + class Meta: + model = Product + fields = ("id", "name", "description", "price", "slug", "sku") diff --git a/demo/products/urls.py b/demo/products/urls.py new file mode 100644 index 00000000..baa4a089 --- /dev/null +++ b/demo/products/urls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from . import views + +app_name = "products" + +urlpatterns = [ + path("products/", views.product_list, name="product_list"), + path("products/create/", views.product_create, name="product_create"), + path("products//", views.product_detail, name="product_detail"), + path("products//update/", views.product_update, name="product_update"), + path("products//delete/", views.product_delete, name="product_delete"), +] diff --git a/demo/products/views.py b/demo/products/views.py new file mode 100644 index 00000000..38cd351b --- /dev/null +++ b/demo/products/views.py @@ -0,0 +1,59 @@ +from core.utils import paginate_queryset +from django.http import HttpRequest +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.template.response import TemplateResponse +from django.views.decorators.http import require_http_methods + +from .forms import ProductForm +from .models import Product + + +def product_list(request: HttpRequest): + products = Product.objects.all() + return TemplateResponse( + request, + "products/product_list.html", + context={"products": paginate_queryset(request, products)}, + ) + + +def product_detail(request: HttpRequest, pk: int): + product = get_object_or_404(Product.objects, pk=pk) + return TemplateResponse( + request, + "products/product_detail.html", + context={"product": product}, + ) + + +def product_create(request: HttpRequest): + form = ProductForm(request.POST or None) + if request.method == "POST" and form.is_valid(): + form.save() + return redirect("products:product_list") + return TemplateResponse( + request, + "products/product_create.html", + context={"form": form}, + ) + + +def product_update(request: HttpRequest, pk: int): + product = get_object_or_404(Product.objects, pk=pk) + form = ProductForm(request.POST or None, instance=product) + if request.method == "POST" and form.is_valid(): + form.save() + return redirect("products:product_detail", pk=pk) + return TemplateResponse( + request, + "products/product_update.html", + context={"product": product, "form": form}, + ) + + +@require_http_methods(["DELETE"]) +def product_delete(request: HttpRequest, pk: int): + Product.objects.filter(pk=pk).delete() + return HttpResponse("OK") diff --git a/demo/templates/base.html b/demo/templates/base.html new file mode 100644 index 00000000..5ac56dbc --- /dev/null +++ b/demo/templates/base.html @@ -0,0 +1,15 @@ + + + + + + My Website + + + +
+ {% block content %} + {% endblock %} +
+ + diff --git a/demo/templates/products/product_list.html b/demo/templates/products/product_list.html index e69de29b..021dd3ea 100644 --- a/demo/templates/products/product_list.html +++ b/demo/templates/products/product_list.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block content %} + + + + + + + + + {% for product in products %} + + + + + {% endfor %} + +
Product NamePrice
{{ product.name }}{{ product.price }}
+ + +{% endblock %} diff --git a/demo/templates/products/product_update.html b/demo/templates/products/product_update.html new file mode 100644 index 00000000..e69de29b diff --git a/docs/the_cli/crud.rst b/docs/the_cli/crud.rst index 5a6e5bec..18e594d8 100644 --- a/docs/the_cli/crud.rst +++ b/docs/the_cli/crud.rst @@ -3,6 +3,7 @@ CRUD for your model .. figure:: ../images/crud.svg + This command generates htmx-powered create, read, update, and delete views for your model. It follows a similar idea as `neapolitan `_ but with a completely different approach. To use **neapolitan**, you'll inherit from its base class view, and for customization, get familiar with its API (which is fairly easy). I prefer function-based views, so this command generates basic and simple function-based views with some basic HTML templates. @@ -10,11 +11,11 @@ I prefer function-based views, so this command generates basic and simple functi .. admonition:: Why function based views? :class: hint dropdown - Read this `django views the right way `_ article. + I think class-based views get complex faster than function-based views. Both have their use cases, but function-based views + stay simpler to manage longer in my experience. There is an excellent document on the topic, read this `django views the right way `_. This command depends on your ``manage.py`` to work, so you must run it from your project root directory. - **Examples** .. code:: bash diff --git a/src/falco/commands/work.py b/src/falco/commands/work.py index d82e253e..c8890ae3 100644 --- a/src/falco/commands/work.py +++ b/src/falco/commands/work.py @@ -38,9 +38,7 @@ def __call__(self) -> None: with suppress(FileNotFoundError): pyproject_config = read_toml(Path("pyproject.toml")) - user_commands = ( - pyproject_config.get("tool", {}).get("falco", {}).get("work", {}) - ) + user_commands = pyproject_config.get("tool", {}).get("falco", {}).get("work", {}) commands = commands | user_commands manager = HonchoManager()