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/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/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/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
new file mode 100644
index 0000000..3100278
--- /dev/null
+++ b/src/servala/frontend/templates/account/login.html
@@ -0,0 +1,53 @@
+{% extends "frontend/base.html" %}
+{% load static i18n %}
+{% load allauth account socialaccount %}
+{% block html_title %}
+ {% block page_title %}
+ {% translate "Sign In" %}
+ {% endblock page_title %}
+{% endblock html_title %}
+{% block content %}
+
+
+
+
+ {% if SOCIALACCOUNT_ENABLED %}
+ {% get_providers as socialaccount_providers %}
+ {% if socialaccount_providers %}
+ {% for provider in socialaccount_providers %}
+ {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %}
+
+ {% endfor %}
+ {% endif %}
+ {% endif %}
+
+
+
+
+
+{% endblock content %}
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 %}
+
+
+ {% if errors|length > 1 %}
+
+ {% for error in errors %}{{ error }} {% endfor %}
+
+ {% else %}
+ {{ errors.0 }}
+ {% endif %}
+
+
+{% 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 %}
+
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/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 %}
+
diff --git a/src/servala/frontend/templates/includes/sidebar.html b/src/servala/frontend/templates/includes/sidebar.html
index 287977a..8c47293 100644
--- a/src/servala/frontend/templates/includes/sidebar.html
+++ b/src/servala/frontend/templates/includes/sidebar.html
@@ -56,42 +56,67 @@
diff --git a/src/servala/frontend/urls.py b/src/servala/frontend/urls.py
new file mode 100644
index 0000000..fbaaa08
--- /dev/null
+++ b/src/servala/frontend/urls.py
@@ -0,0 +1,14 @@
+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(
+ "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
new file mode 100644
index 0000000..20b4398
--- /dev/null
+++ b/src/servala/frontend/views/__init__.py
@@ -0,0 +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/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/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
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 "/"
diff --git a/src/servala/settings.py b/src/servala/settings.py
index 785169a..36d7673 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",
@@ -112,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.
@@ -144,22 +147,23 @@ TEMPLATES = [
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.static",
+ "servala.frontend.context_processors.add_organizations",
],
"loaders": template_loaders,
},
},
]
+FORM_RENDERER = "servala.frontend.forms.renderers.VerticalFormRenderer"
MESSAGE_TAGS = {
messages.ERROR: "danger",
}
AUTH_USER_MODEL = "core.User"
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
-ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_UNIQUE_EMAIL = True
-ACCOUNT_USERNAME_REQUIRED = False
-ACCOUNT_AUTHENTICATION_METHOD = "email"
+ACCOUNT_LOGIN_METHODS = {"email"}
+ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"]
AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, regardless of `allauth`
diff --git a/src/servala/static/img/keycloak.svg b/src/servala/static/img/keycloak.svg
new file mode 100644
index 0000000..cdcd0fa
--- /dev/null
+++ b/src/servala/static/img/keycloak.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
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