From 4444d27c9f104079e08da47bbfb026372c430ce7 Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Sun, 21 Apr 2024 17:05:36 +0100 Subject: [PATCH 1/2] feat: Added HTMX Boosting to improve site performance (#345) * enhance: Added HTMX Boosting for increased performance * fix: Fixed incompatible TypeHints --- backend/context_processors.py | 9 +++-- backend/decorators.py | 17 +++++++++ backend/middleware.py | 13 +++++++ backend/types/htmx.py | 6 +++ backend/views/core/invoices/dashboard.py | 25 ++++++------ backend/views/core/invoices/overview.py | 10 +++-- backend/views/core/quotas/view.py | 3 +- backend/views/core/settings/teams.py | 12 +++--- frontend/templates/base/+left_drawer.html | 7 +++- frontend/templates/base/base.html | 2 +- frontend/templates/base/breadcrumbs.html | 5 ++- frontend/templates/base/htmx.html | 10 +++++ .../templates/base/topbar/+icon_dropdown.html | 28 ++++++++++++++ frontend/templates/base/topbar/_topbar.html | 38 ++++--------------- .../pages/clients/create/create.html | 3 +- .../pages/clients/dashboard/dashboard.html | 6 ++- .../pages/currency_converter/dashboard.html | 2 +- frontend/templates/pages/dashboard.html | 2 +- .../templates/pages/emails/dashboard.html | 2 +- .../pages/invoices/create/create.html | 2 +- .../pages/invoices/dashboard/_fetch_body.html | 2 +- .../pages/invoices/dashboard/dashboard.html | 6 ++- .../pages/invoices/dashboard/manage.html | 4 +- .../templates/pages/invoices/edit/edit.html | 2 +- .../invoices/manage_access/manage_access.html | 2 +- .../pages/invoices/schedules/view.html | 2 +- .../templates/pages/quotas/dashboard.html | 5 ++- frontend/templates/pages/quotas/list.html | 4 +- .../templates/pages/quotas/view_requests.html | 10 ++++- .../templates/pages/receipts/dashboard.html | 2 +- frontend/templates/pages/settings/main.html | 3 +- .../templates/pages/settings/teams/leave.html | 2 +- .../templates/pages/settings/teams/main.html | 2 +- .../pages/settings/teams/permissions.html | 2 +- settings/settings.py | 1 + 35 files changed, 162 insertions(+), 89 deletions(-) create mode 100644 frontend/templates/base/htmx.html create mode 100644 frontend/templates/base/topbar/+icon_dropdown.html diff --git a/backend/context_processors.py b/backend/context_processors.py index d9af432e..98c497c4 100644 --- a/backend/context_processors.py +++ b/backend/context_processors.py @@ -30,6 +30,9 @@ def extras(request: HttpRequest): data["import_method"] = get_var("IMPORT_METHOD", default="webpack") data["analytics"] = get_var("ANALYTICS_STRING") + if hasattr(request, "htmx") and request.htmx.boosted: + data["base"] = "base/htmx.html" + return data @@ -52,7 +55,7 @@ def get_item(name: str, url_name: Optional[str] = None, icon: Optional[str] = No "icon": icon, } - def generate_breadcrumbs(*breadcrumb_list: str) -> List[dict[Any, Any] | None]: + def generate_breadcrumbs(*breadcrumb_list: str) -> list[dict[Any, Any] | None]: """ Generate a list of breadcrumb items based on the provided list of breadcrumb names. @@ -66,7 +69,7 @@ def generate_breadcrumbs(*breadcrumb_list: str) -> List[dict[Any, Any] | None]: current_url_name: str | Any = request.resolver_match.url_name # type: ignore[union-attr] - all_items: Dict[str, dict] = { + all_items: dict[str, dict] = { "dashboard": get_item("Dashboard", "dashboard", "house"), "invoices:dashboard": get_item("Invoices", "invoices:dashboard", "file-invoice"), "invoices:create": get_item("Create", "invoices:create"), @@ -78,7 +81,7 @@ def generate_breadcrumbs(*breadcrumb_list: str) -> List[dict[Any, Any] | None]: "clients create": get_item("Create", "clients create"), } - all_breadcrumbs: Dict[str | None, list] = { + all_breadcrumbs: dict[str | None, list] = { "dashboard": generate_breadcrumbs("dashboard"), "user settings teams": generate_breadcrumbs("dashboard", "user settings teams"), "receipts dashboard": generate_breadcrumbs("dashboard", "receipts dashboard"), diff --git a/backend/decorators.py b/backend/decorators.py index 04183211..ea27389a 100644 --- a/backend/decorators.py +++ b/backend/decorators.py @@ -60,6 +60,23 @@ def wrapper_func(request, *args, **kwargs): return decorator +def hx_boost(view): + """ + Decorator for HTMX requests. + + used by wrapping FBV in @hx_boost and adding **kwargs to param + then you can use context = kwargs.get("context", {}) to continue and then it will handle HTMX boosts + """ + + @wraps(view) + def wrapper(request, *args, **kwargs): + if request.htmx.boosted: + kwargs["context"] = kwargs.get("context", {}) | {"base": "base/htmx.html"} + return view(request, *args, **kwargs) + + return wrapper + + def feature_flag_check(flag, status=True, api=False, htmx=False): def decorator(view_func): @wraps(view_func) diff --git a/backend/middleware.py b/backend/middleware.py index deb698e5..1c3b013b 100644 --- a/backend/middleware.py +++ b/backend/middleware.py @@ -1,6 +1,8 @@ from django.db import connection, OperationalError from django.http import HttpResponse +from backend.types.htmx import HtmxAnyHttpRequest + class HealthCheckMiddleware: def __init__(self, get_response): @@ -19,6 +21,17 @@ def __call__(self, request): return self.get_response(request) +class HTMXPartialLoadMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request: HtmxAnyHttpRequest): + response = self.get_response(request) + if request.htmx.boosted: + response.headers["HX-Retarget"] = "#main_content" + return response + + class LastVisitedMiddleware: def __init__(self, get_response): self.get_response = get_response diff --git a/backend/types/htmx.py b/backend/types/htmx.py index 9ec6435a..29fca486 100644 --- a/backend/types/htmx.py +++ b/backend/types/htmx.py @@ -12,3 +12,9 @@ class HtmxHttpRequest(HttpRequest): class UnauthorizedHttpRequest(HttpRequest): user: AnonymousUser + htmx: HtmxDetails + + +class HtmxAnyHttpRequest(HttpRequest): + user: User | AnonymousUser + htmx: HtmxDetails diff --git a/backend/views/core/invoices/dashboard.py b/backend/views/core/invoices/dashboard.py index 013cfd24..8d8c0f27 100644 --- a/backend/views/core/invoices/dashboard.py +++ b/backend/views/core/invoices/dashboard.py @@ -1,27 +1,26 @@ +from django.contrib import messages from django.http import HttpRequest -from django.shortcuts import render +from django.shortcuts import render, redirect -from backend.decorators import * -from backend.models import * +from backend.models import Invoice from backend.types.htmx import HtmxHttpRequest def invoices_dashboard(request: HtmxHttpRequest): - context = {} - - return render(request, "pages/invoices/dashboard/dashboard.html", context) + return render(request, "pages/invoices/dashboard/dashboard.html") def invoices_dashboard_id(request: HtmxHttpRequest, invoice_id): + context = {} + if invoice_id == "create": return redirect("invoices:create") - elif type(invoice_id) != "int": + elif not isinstance(invoice_id, int): messages.error(request, "Invalid invoice ID") return redirect("invoices:dashboard") - invoices = Invoice.objects.get(id=invoice_id) - if not invoices: + + try: + Invoice.objects.get(id=invoice_id) + except Invoice.DoesNotExist: return redirect("invoices:dashboard") - return render( - request, - "pages/invoices/dashboard/dashboard.html", - ) + return render(request, "pages/invoices/dashboard/dashboard.html", context) diff --git a/backend/views/core/invoices/overview.py b/backend/views/core/invoices/overview.py index 1b6dbade..41b9eb16 100644 --- a/backend/views/core/invoices/overview.py +++ b/backend/views/core/invoices/overview.py @@ -4,12 +4,12 @@ def invoices_dashboard(request: HtmxHttpRequest): - context = {} - - return render(request, "pages/invoices/dashboard/dashboard.html", context) + return render(request, "pages/invoices/dashboard/dashboard.html") def manage_invoice(request: HtmxHttpRequest, invoice_id: str): + context = {} + if not invoice_id.isnumeric(): messages.error(request, "Invalid invoice ID") return redirect("invoices:dashboard") @@ -18,4 +18,6 @@ def manage_invoice(request: HtmxHttpRequest, invoice_id: str): if not invoice: return redirect("invoices:dashboard") - return render(request, "pages/invoices/dashboard/manage.html", {"invoice": invoice}) + + print(context | {"invoice": invoice}) + return render(request, "pages/invoices/dashboard/manage.html", context | {"invoice": invoice}) diff --git a/backend/views/core/quotas/view.py b/backend/views/core/quotas/view.py index 40fae9f5..e866ff61 100644 --- a/backend/views/core/quotas/view.py +++ b/backend/views/core/quotas/view.py @@ -7,11 +7,10 @@ from backend.types.htmx import HtmxHttpRequest -@cache_page(3600) def quotas_page(request: HtmxHttpRequest) -> HttpResponse: groups = list(QuotaLimit.objects.values_list("slug", flat=True).distinct()) - quotas = set(q.split("-")[0] for q in groups if q.split("-")) + quotas = {q.split("-")[0] for q in groups if q.split("-")} return render( request, diff --git a/backend/views/core/settings/teams.py b/backend/views/core/settings/teams.py index 9527fd49..24c294ad 100644 --- a/backend/views/core/settings/teams.py +++ b/backend/views/core/settings/teams.py @@ -9,6 +9,8 @@ def teams_dashboard(request: HtmxHttpRequest): + context = {} + users_team: Optional[Team] = request.user.logged_in_as_team if not users_team: @@ -16,7 +18,8 @@ def teams_dashboard(request: HtmxHttpRequest): return render( request, "pages/settings/teams/main.html", - { + context + | { "team": None, "team_count": user_with_counts.teams_joined.count() + user_with_counts.teams_leader_of.count(), }, @@ -40,7 +43,8 @@ def teams_dashboard(request: HtmxHttpRequest): return render( request, "pages/settings/teams/main.html", - { + context + | { "team": None, "team_count": user_with_counts.teams_joined.count() + user_with_counts.teams_leader_of.count(), }, @@ -49,9 +53,7 @@ def teams_dashboard(request: HtmxHttpRequest): return render( request, "pages/settings/teams/main.html", - { - "team": team, - }, + context | {"team": team}, ) diff --git a/frontend/templates/base/+left_drawer.html b/frontend/templates/base/+left_drawer.html index 9b56d715..a21a6ee8 100644 --- a/frontend/templates/base/+left_drawer.html +++ b/frontend/templates/base/+left_drawer.html @@ -1,10 +1,13 @@ {% load feature_enabled %} {% feature_enabled "areUserEmailsAllowed" as are_user_emails_allowed %} -