From 092a92d986c44f311f350f4fd18846a16af2a21e Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 17 Mar 2025 03:30:44 +0100 Subject: [PATCH 01/10] Move views to module --- src/servala/frontend/views/__init__.py | 6 ++++++ src/servala/frontend/{views.py => views/generic.py} | 0 2 files changed, 6 insertions(+) create mode 100644 src/servala/frontend/views/__init__.py rename src/servala/frontend/{views.py => views/generic.py} (100%) diff --git a/src/servala/frontend/views/__init__.py b/src/servala/frontend/views/__init__.py new file mode 100644 index 0000000..e712464 --- /dev/null +++ b/src/servala/frontend/views/__init__.py @@ -0,0 +1,6 @@ +from .generic import IndexView, ProfileView + +__all__ = [ + "IndexView", + "ProfileView", +] diff --git a/src/servala/frontend/views.py b/src/servala/frontend/views/generic.py similarity index 100% rename from src/servala/frontend/views.py rename to src/servala/frontend/views/generic.py From 024eae0e1a899746fa92d11653e4d6a7acb14802 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 17 Mar 2025 09:12:58 +0100 Subject: [PATCH 02/10] Add our own logout view --- .../frontend/templates/includes/sidebar.html | 13 ++++++++----- src/servala/frontend/urls.py | 9 +++++++++ src/servala/frontend/views/__init__.py | 2 ++ src/servala/frontend/views/auth.py | 12 ++++++++++++ src/servala/urls.py | 7 +++---- 5 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 src/servala/frontend/urls.py create mode 100644 src/servala/frontend/views/auth.py diff --git a/src/servala/frontend/templates/includes/sidebar.html b/src/servala/frontend/templates/includes/sidebar.html index 287977a..2baecb5 100644 --- a/src/servala/frontend/templates/includes/sidebar.html +++ b/src/servala/frontend/templates/includes/sidebar.html @@ -80,16 +80,19 @@ {% else %} {% endif %} diff --git a/src/servala/frontend/urls.py b/src/servala/frontend/urls.py new file mode 100644 index 0000000..b9a0c6b --- /dev/null +++ b/src/servala/frontend/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from servala.frontend import views + +urlpatterns = [ + path("accounts/profile/", views.ProfileView.as_view(), name="profile"), + path("accounts/logout/", views.LogoutView.as_view(), name="logout"), + path("", views.IndexView.as_view(), name="index"), +] diff --git a/src/servala/frontend/views/__init__.py b/src/servala/frontend/views/__init__.py index e712464..3fc930c 100644 --- a/src/servala/frontend/views/__init__.py +++ b/src/servala/frontend/views/__init__.py @@ -1,6 +1,8 @@ +from .auth import LogoutView from .generic import IndexView, ProfileView __all__ = [ "IndexView", + "LogoutView", "ProfileView", ] diff --git a/src/servala/frontend/views/auth.py b/src/servala/frontend/views/auth.py new file mode 100644 index 0000000..6b351f1 --- /dev/null +++ b/src/servala/frontend/views/auth.py @@ -0,0 +1,12 @@ +from allauth.account.internal import flows +from allauth.account.utils import get_next_redirect_url +from django.shortcuts import redirect +from django.views import View + + +class LogoutView(View): + + def post(self, request): + flows.logout.logout(request) + url = get_next_redirect_url(request, "next") or "/" + return redirect(url) diff --git a/src/servala/urls.py b/src/servala/urls.py index 8ddedb6..d1d40ec 100644 --- a/src/servala/urls.py +++ b/src/servala/urls.py @@ -6,7 +6,7 @@ from django.urls import path from django.urls.conf import include from django.utils.translation import gettext_lazy as _ -from servala.frontend import views +from servala.frontend import urls admin.site.site_title = _("Servala Admin") admin.site.site_header = _("Servala Management") @@ -14,13 +14,12 @@ admin.site.index_title = _("Dashboard") admin.site.login = secure_admin_login(admin.site.login) urlpatterns = [ + path("admin/", admin.site.urls), + path("", include((urls, "servala.frontend"), namespace="frontend")), # This adds the allauth urls to the project: # - accounts/keycloak/login/ # - accounts/keycloak/login/callback/ path("accounts/", include("allauth.urls")), - path("accounts/profile/", views.ProfileView.as_view(), name="profile"), - path("admin/", admin.site.urls), - path("", views.IndexView.as_view(), name="index"), ] # Serve static and media files in development From a780d31c15397e893cb31f15df2891489bd4a68f Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 17 Mar 2025 09:51:52 +0100 Subject: [PATCH 03/10] Add organization indicator --- src/servala/frontend/context_processors.py | 5 ++++ .../frontend/templates/includes/sidebar.html | 30 +++++++++++++++++++ src/servala/settings.py | 6 ++-- 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/servala/frontend/context_processors.py diff --git a/src/servala/frontend/context_processors.py b/src/servala/frontend/context_processors.py new file mode 100644 index 0000000..1a180d8 --- /dev/null +++ b/src/servala/frontend/context_processors.py @@ -0,0 +1,5 @@ +def add_organizations(request): + if not request.user.is_authenticated: + return {"user_organizations": []} + + return {"user_organizations": request.user.organizations.all()} diff --git a/src/servala/frontend/templates/includes/sidebar.html b/src/servala/frontend/templates/includes/sidebar.html index 2baecb5..617ff56 100644 --- a/src/servala/frontend/templates/includes/sidebar.html +++ b/src/servala/frontend/templates/includes/sidebar.html @@ -56,6 +56,36 @@ From eb91f59e09b609faf4ea5d0e39892936f2f0532a Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 17 Mar 2025 21:33:51 +0100 Subject: [PATCH 06/10] Hide admin login form --- .../frontend/templates/account/login.html | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/servala/frontend/templates/account/login.html b/src/servala/frontend/templates/account/login.html index bb945ee..b0242b9 100644 --- a/src/servala/frontend/templates/account/login.html +++ b/src/servala/frontend/templates/account/login.html @@ -28,14 +28,23 @@ {% endfor %} {% endif %} {% endif %} -
- {% translate "Log in with email and password instead" %} -
- {% csrf_token %} - {{ form }} - -
-
+
+ + + {% translate "Log in with email and password instead" %} + +
+
+ {% csrf_token %} + {{ form }} + +
+
+
From 78119dc6b3089498c42565b2e37082bde79d70c2 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 17 Mar 2025 22:38:26 +0100 Subject: [PATCH 07/10] Build template-based form rendering with bootstrap attrs --- src/servala/frontend/forms/renderers.py | 28 +++++++++++++++++++ .../frontend/templates/account/login.html | 5 +++- .../templates/frontend/forms/form.html | 19 +++++++++++++ .../frontend/forms/vertical_field.html | 26 +++++++++++++++++ src/servala/settings.py | 2 ++ 5 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/servala/frontend/forms/renderers.py create mode 100644 src/servala/frontend/templates/frontend/forms/form.html create mode 100644 src/servala/frontend/templates/frontend/forms/vertical_field.html diff --git a/src/servala/frontend/forms/renderers.py b/src/servala/frontend/forms/renderers.py new file mode 100644 index 0000000..f8cdb5d --- /dev/null +++ b/src/servala/frontend/forms/renderers.py @@ -0,0 +1,28 @@ +from django.forms.renderers import TemplatesSetting + + +def inject_class(f, class_name): + def inner(*args, **kwargs): + result = f(*args, **kwargs) + class_list = result.get("class", "") + class_list = f"{class_list} {class_name}".strip() + result["class"] = class_list + return result + + return inner + + +class VerticalFormRenderer(TemplatesSetting): + form_template_name = "frontend/forms/form.html" + field_template_name = "frontend/forms/vertical_field.html" + + def render(self, template_name, context, request=None): + if field := context.get("field"): + if field.field.widget.input_type == "checkbox": + class_name = "form-check-input" + else: + class_name = "form-control" + field.build_widget_attrs = inject_class( + field.build_widget_attrs, class_name + ) + return super().render(template_name, context, request) diff --git a/src/servala/frontend/templates/account/login.html b/src/servala/frontend/templates/account/login.html index b0242b9..214b77d 100644 --- a/src/servala/frontend/templates/account/login.html +++ b/src/servala/frontend/templates/account/login.html @@ -37,7 +37,10 @@ {% translate "Log in with email and password instead" %} -
+
{% csrf_token %} {{ form }} diff --git a/src/servala/frontend/templates/frontend/forms/form.html b/src/servala/frontend/templates/frontend/forms/form.html new file mode 100644 index 0000000..a9e8c47 --- /dev/null +++ b/src/servala/frontend/templates/frontend/forms/form.html @@ -0,0 +1,19 @@ +{% if errors %} + +{% endif %} +
+
+ {% for field, errors in fields %}{{ field.as_field_group }}{% endfor %} + {% for field in hidden_fields %}{{ field }}{% endfor %} +
+
diff --git a/src/servala/frontend/templates/frontend/forms/vertical_field.html b/src/servala/frontend/templates/frontend/forms/vertical_field.html new file mode 100644 index 0000000..bcbf8d6 --- /dev/null +++ b/src/servala/frontend/templates/frontend/forms/vertical_field.html @@ -0,0 +1,26 @@ +{% load i18n %} +
+
+ {% if field.field.widget.input_type != "checkbox" or field.field.widget.allow_multiple_selected %} + + {% endif %} + {% if field.use_fieldset %} +
+ {% endif %} + {{ field }} + {% if field.field.widget.input_type == "checkbox" and not field.field.widget.allow_multiple_selected %} + + {% endif %} + {% if field.use_fieldset %}
{% endif %} + {% for text in field.errors %}
{{ text }}
{% endfor %} + {% if field.help_text %} + {{ field.help_text|safe }} + {% endif %} +
+
diff --git a/src/servala/settings.py b/src/servala/settings.py index 73c771b..eba2e0a 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -95,6 +95,7 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "django.forms", "servala.frontend", "allauth", "allauth.account", @@ -151,6 +152,7 @@ TEMPLATES = [ }, ] +FORM_RENDERER = "servala.frontend.forms.renderers.VerticalFormRenderer" MESSAGE_TAGS = { messages.ERROR: "danger", } From 0be7c6fb6f364c21706f3b7cfc624cc8a00ebd94 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 18 Mar 2025 02:44:22 +0100 Subject: [PATCH 08/10] Make login required by default --- src/servala/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/servala/settings.py b/src/servala/settings.py index eba2e0a..36d7673 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -113,7 +113,9 @@ MIDDLEWARE = [ "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "allauth.account.middleware.AccountMiddleware", + "django.contrib.auth.middleware.LoginRequiredMiddleware", ] +LOGIN_URL = "account_login" ROOT_URLCONF = "servala.urls" STATIC_URL = "static/" # CSS, JavaScript, etc. From 325e767b0ef017cd4e2016b1aa085add938c99d1 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 18 Mar 2025 05:08:43 +0100 Subject: [PATCH 09/10] Add reusable form template snippet --- src/servala/frontend/templates/account/login.html | 8 +++----- src/servala/frontend/templates/includes/form.html | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 src/servala/frontend/templates/includes/form.html diff --git a/src/servala/frontend/templates/account/login.html b/src/servala/frontend/templates/account/login.html index 214b77d..3100278 100644 --- a/src/servala/frontend/templates/account/login.html +++ b/src/servala/frontend/templates/account/login.html @@ -41,11 +41,9 @@ id="login-form" class="form form-vertical" style="max-width: 400px"> - - {% csrf_token %} - {{ form }} - - + {% url 'account_login' as form_action %} + {% translate "Sign In" as form_submit_label %} + {% include "includes/form.html" %}
diff --git a/src/servala/frontend/templates/includes/form.html b/src/servala/frontend/templates/includes/form.html new file mode 100644 index 0000000..d8a01e5 --- /dev/null +++ b/src/servala/frontend/templates/includes/form.html @@ -0,0 +1,14 @@ +{% load i18n %} +
+ {% csrf_token %} + {{ form }} + +
From 2baa3fd5ec81fd44a3d678940f5b6cfd6119b796 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 18 Mar 2025 06:58:40 +0100 Subject: [PATCH 10/10] First draft of organization creation --- src/servala/core/models/organization.py | 6 ++++++ src/servala/frontend/forms/__init__.py | 3 +++ src/servala/frontend/forms/organization.py | 9 +++++++++ .../templates/frontend/organizations/create.html | 16 ++++++++++++++++ .../frontend/templates/includes/sidebar.html | 2 +- src/servala/frontend/urls.py | 5 +++++ src/servala/frontend/views/__init__.py | 2 ++ src/servala/frontend/views/organization.py | 16 ++++++++++++++++ 8 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/servala/frontend/forms/__init__.py create mode 100644 src/servala/frontend/forms/organization.py create mode 100644 src/servala/frontend/templates/frontend/organizations/create.html create mode 100644 src/servala/frontend/views/organization.py diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 6fd6c67..fec3fa4 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -27,6 +27,12 @@ class Organization(ServalaModelMixin, models.Model): verbose_name=_("Members"), ) + def set_owner(self, user): + OrganizationMembership.objects.filter(user=user, organization=self).delete() + OrganizationMembership.objects.create( + user=user, organization=self, role=OrganizationRole.OWNER + ) + class Meta: verbose_name = _("Organization") verbose_name_plural = _("Organizations") diff --git a/src/servala/frontend/forms/__init__.py b/src/servala/frontend/forms/__init__.py new file mode 100644 index 0000000..8169b61 --- /dev/null +++ b/src/servala/frontend/forms/__init__.py @@ -0,0 +1,3 @@ +from .organization import OrganizationCreateForm + +__all__ = ["OrganizationCreateForm"] diff --git a/src/servala/frontend/forms/organization.py b/src/servala/frontend/forms/organization.py new file mode 100644 index 0000000..b6a3391 --- /dev/null +++ b/src/servala/frontend/forms/organization.py @@ -0,0 +1,9 @@ +from django.forms import ModelForm + +from servala.core.models import Organization + + +class OrganizationCreateForm(ModelForm): + class Meta: + model = Organization + fields = ("name",) diff --git a/src/servala/frontend/templates/frontend/organizations/create.html b/src/servala/frontend/templates/frontend/organizations/create.html new file mode 100644 index 0000000..ca46190 --- /dev/null +++ b/src/servala/frontend/templates/frontend/organizations/create.html @@ -0,0 +1,16 @@ +{% extends "frontend/base.html" %} +{% load i18n %} +{% block html_title %} + {% block page_title %} + {% translate "Create a new organization" %} + {% endblock page_title %} +{% endblock html_title %} +{% block content %} +
+
+
+
{% include "includes/form.html" %}
+
+
+
+{% endblock content %} diff --git a/src/servala/frontend/templates/includes/sidebar.html b/src/servala/frontend/templates/includes/sidebar.html index e3c62f4..8c47293 100644 --- a/src/servala/frontend/templates/includes/sidebar.html +++ b/src/servala/frontend/templates/includes/sidebar.html @@ -87,7 +87,7 @@ {% elif current_organization %} {% translate "Organization" %}: {{ current_organization.name }} {% else %} - + {% translate "Create organization" %} diff --git a/src/servala/frontend/urls.py b/src/servala/frontend/urls.py index b9a0c6b..fbaaa08 100644 --- a/src/servala/frontend/urls.py +++ b/src/servala/frontend/urls.py @@ -5,5 +5,10 @@ from servala.frontend import views urlpatterns = [ path("accounts/profile/", views.ProfileView.as_view(), name="profile"), path("accounts/logout/", views.LogoutView.as_view(), name="logout"), + path( + "organizations/create", + views.OrganizationCreateView.as_view(), + name="organization.create", + ), path("", views.IndexView.as_view(), name="index"), ] diff --git a/src/servala/frontend/views/__init__.py b/src/servala/frontend/views/__init__.py index 3fc930c..20b4398 100644 --- a/src/servala/frontend/views/__init__.py +++ b/src/servala/frontend/views/__init__.py @@ -1,8 +1,10 @@ from .auth import LogoutView from .generic import IndexView, ProfileView +from .organization import OrganizationCreateView __all__ = [ "IndexView", "LogoutView", + "OrganizationCreateView", "ProfileView", ] diff --git a/src/servala/frontend/views/organization.py b/src/servala/frontend/views/organization.py new file mode 100644 index 0000000..9bc1397 --- /dev/null +++ b/src/servala/frontend/views/organization.py @@ -0,0 +1,16 @@ +from django.views.generic import FormView + +from servala.frontend.forms import OrganizationCreateForm + + +class OrganizationCreateView(FormView): + form_class = OrganizationCreateForm + template_name = "frontend/organizations/create.html" + + def form_valid(self, form): + form.save() + form.instance.set_owner(self.request.user) + return super().form_valid(form) + + def get_success_url(self): + return "/"