Compare commits
10 commits
0d09d338e8
...
2baa3fd5ec
Author | SHA1 | Date | |
---|---|---|---|
2baa3fd5ec | |||
325e767b0e | |||
0be7c6fb6f | |||
78119dc6b3 | |||
eb91f59e09 | |||
78f5766a7e | |||
5cbb4ef449 | |||
a780d31c15 | |||
024eae0e1a | |||
092a92d986 |
19 changed files with 295 additions and 30 deletions
|
@ -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")
|
||||
|
|
5
src/servala/frontend/context_processors.py
Normal file
5
src/servala/frontend/context_processors.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
def add_organizations(request):
|
||||
if not request.user.is_authenticated:
|
||||
return {"user_organizations": []}
|
||||
|
||||
return {"user_organizations": request.user.organizations.all()}
|
3
src/servala/frontend/forms/__init__.py
Normal file
3
src/servala/frontend/forms/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from .organization import OrganizationCreateForm
|
||||
|
||||
__all__ = ["OrganizationCreateForm"]
|
9
src/servala/frontend/forms/organization.py
Normal file
9
src/servala/frontend/forms/organization.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.forms import ModelForm
|
||||
|
||||
from servala.core.models import Organization
|
||||
|
||||
|
||||
class OrganizationCreateForm(ModelForm):
|
||||
class Meta:
|
||||
model = Organization
|
||||
fields = ("name",)
|
28
src/servala/frontend/forms/renderers.py
Normal file
28
src/servala/frontend/forms/renderers.py
Normal file
|
@ -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)
|
53
src/servala/frontend/templates/account/login.html
Normal file
53
src/servala/frontend/templates/account/login.html
Normal file
|
@ -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 %}
|
||||
<section class="section">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="card-body">
|
||||
{% 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 %}
|
||||
<form method="post" action="{{ href }}">
|
||||
{% csrf_token %}
|
||||
<button href="{{ href }}"
|
||||
class="btn btn-warning btn-lg icon icon-left"
|
||||
title="{{ provider.name }}">
|
||||
<img src="{% static 'img/keycloak.svg' %}" style="height: 30px">
|
||||
<span class="mx-1">{% translate "Sign in with your" %} {{ provider.name }}</span>
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="mt-2">
|
||||
<a data-bs-toggle="collapse"
|
||||
href="#login-form"
|
||||
role="button"
|
||||
aria-controls="login-form"
|
||||
class="d-flex align-items-center">
|
||||
<i class="bi bi-chevron-right me-2 ms-1 mb-2 collapse-icon"></i>
|
||||
{% translate "Log in with email and password instead" %}
|
||||
</a>
|
||||
<div class="collapse mt-3 ms-3"
|
||||
id="login-form"
|
||||
class="form form-vertical"
|
||||
style="max-width: 400px">
|
||||
{% url 'account_login' as form_action %}
|
||||
{% translate "Sign In" as form_submit_label %}
|
||||
{% include "includes/form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
19
src/servala/frontend/templates/frontend/forms/form.html
Normal file
19
src/servala/frontend/templates/frontend/forms/form.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% if errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<div>
|
||||
{% if errors|length > 1 %}
|
||||
<ul>
|
||||
{% for error in errors %}<li>{{ error }}</li>{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{{ errors.0 }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-body">
|
||||
<div class="row">
|
||||
{% for field, errors in fields %}{{ field.as_field_group }}{% endfor %}
|
||||
{% for field in hidden_fields %}{{ field }}{% endfor %}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,26 @@
|
|||
{% load i18n %}
|
||||
<div class="col-12">
|
||||
<div class="form-group{% with classes=field.css_classes %}{% if classes %} {{ classes }}{% endif %}{% endwith %}">
|
||||
{% if field.field.widget.input_type != "checkbox" or field.field.widget.allow_multiple_selected %}
|
||||
<label for="{{ field.auto_id }}" class="{{ label_class }}">
|
||||
{{ field.label }}
|
||||
{% if not field.field.required %}
|
||||
<span class="optional">{% translate "Optional" %}</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if field.use_fieldset %}
|
||||
<fieldset {% if field.help_text and field.auto_id and "aria-describedby" not in field.field.widget.attrs %} aria-describedby="{{ field.auto_id }}_helptext"{% endif %}>
|
||||
{% endif %}
|
||||
{{ field }}
|
||||
{% if field.field.widget.input_type == "checkbox" and not field.field.widget.allow_multiple_selected %}
|
||||
<label for="{{ field.auto_id }}">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
{% if field.use_fieldset %}</fieldset>{% endif %}
|
||||
{% for text in field.errors %}<div class="invalid-feedback">{{ text }}</div>{% endfor %}
|
||||
{% if field.help_text %}
|
||||
<small class="form-text text-muted"
|
||||
{% if field.auto_id %}id="{{ field.auto_id }}_helptext"{% endif %}>{{ field.help_text|safe }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -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 %}
|
||||
<section class="section">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="card-body">{% include "includes/form.html" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
14
src/servala/frontend/templates/includes/form.html
Normal file
14
src/servala/frontend/templates/includes/form.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% load i18n %}
|
||||
<form class="form form-vertical"
|
||||
method="post"
|
||||
{% if form_action %}action="{{ form_action }}"{% endif %}>
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{% if form_submit_label %}
|
||||
{{ form_submit_label }}
|
||||
{% else %}
|
||||
{% translate "Save" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
|
@ -56,42 +56,67 @@
|
|||
</div>
|
||||
<div class="sidebar-menu">
|
||||
<ul class="menu">
|
||||
<li class="sidebar-item active">
|
||||
<a href="index.html" class='sidebar-link'>
|
||||
<i class="bi bi-grid-fill"></i>
|
||||
<span>{% translate 'Dashboard' %}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-title">{% translate 'Account' %}</li>
|
||||
{% if not request.user.is_authenticated %}
|
||||
<li class="sidebar-item has-sub">
|
||||
<a href="#" class='sidebar-link'>
|
||||
<li class="sidebar-item">
|
||||
<a href="{% url 'account_login' %}" class="sidebar-link">
|
||||
<i class="bi bi-person-badge-fill"></i>
|
||||
<span>{% translate 'Authentication' %}</span>
|
||||
<span>{% translate 'Login' %}</span>
|
||||
</a>
|
||||
<ul class="submenu">
|
||||
<li class="submenu-item">
|
||||
<a href="{% url 'account_login' %}" class="submenu-link">{% translate 'Login' %}</a>
|
||||
</li>
|
||||
<li class="submenu-item">
|
||||
<a href="{% url 'account_signup' %}" class="submenu-link">{% translate 'Register' %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
{# request.user.is_authenticated #}
|
||||
<li class="sidebar-item">
|
||||
<a href="{% url 'profile' %}" class='sidebar-link'>
|
||||
{% if user_organizations.count > 1 %}
|
||||
<button class="btn btn-primary dropdown-toggle me-1"
|
||||
type="button"
|
||||
id="organizationDropdown"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
{% if current_organization %}
|
||||
{{ current_organization.name }}
|
||||
{% else %}
|
||||
{% translate "Organizations" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="organizationDropdown">
|
||||
{% for organization in user_organizations %}
|
||||
<a class="dropdown-item" href="#TODO">{{ organization.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif current_organization %}
|
||||
{% translate "Organization" %}: {{ current_organization.name }}
|
||||
{% else %}
|
||||
<a href="{% url 'frontend:organization.create' %}" class="sidebar-link">
|
||||
<i class="bi bi-plus-square"></i>
|
||||
<span>{% translate "Create organization" %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="sidebar-item">
|
||||
<a href="index.html" class='sidebar-link'>
|
||||
<i class="bi bi-grid-fill"></i>
|
||||
<span>{% translate 'Dashboard' %}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-title">{% translate 'Account' %}</li>
|
||||
<li class="sidebar-item">
|
||||
<a href="{% url 'frontend:profile' %}" class='sidebar-link'>
|
||||
<i class="bi bi-file-person"></i>
|
||||
<span>{% translate 'Profile' %}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-item">
|
||||
<a href="{% url 'account_logout' %}" class='sidebar-link'>
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
<span>{% translate 'Log out' %}</span>
|
||||
</a>
|
||||
<form action="{% url 'frontend:logout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class='sidebar-link btn'>
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
<span>{% translate 'Log out' %}</span>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
{# request.user.is_authenticated #}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
14
src/servala/frontend/urls.py
Normal file
14
src/servala/frontend/urls.py
Normal file
|
@ -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"),
|
||||
]
|
10
src/servala/frontend/views/__init__.py
Normal file
10
src/servala/frontend/views/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from .auth import LogoutView
|
||||
from .generic import IndexView, ProfileView
|
||||
from .organization import OrganizationCreateView
|
||||
|
||||
__all__ = [
|
||||
"IndexView",
|
||||
"LogoutView",
|
||||
"OrganizationCreateView",
|
||||
"ProfileView",
|
||||
]
|
12
src/servala/frontend/views/auth.py
Normal file
12
src/servala/frontend/views/auth.py
Normal file
|
@ -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)
|
16
src/servala/frontend/views/organization.py
Normal file
16
src/servala/frontend/views/organization.py
Normal file
|
@ -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 "/"
|
|
@ -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`
|
||||
|
|
6
src/servala/static/img/keycloak.svg
Normal file
6
src/servala/static/img/keycloak.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg width="107" height="60" fill="none" xmlns="http://www.w3.org/2000/svg" class="image" aria-hidden="true">
|
||||
<circle cx="53.002" cy="30" r="19" fill="#4CC3FF"></circle>
|
||||
<path opacity="0.25" d="M67.833 41.874A18.92 18.92 0 0071.999 30c0-10.493-8.506-19-19-19a18.932 18.932 0 00-12.868 5.021c14.446.554 26.182 11.675 27.702 25.853z" fill="#000D1A"></path>
|
||||
<path d="M31.594 6.491v8.571l5.217-3.173a31.165 31.165 0 0132.426 0l.622.378v.006l.14.078v-8.39l-.137-.07a38.382 38.382 0 00-36.807 1.712l-1.461.888zM74.436 53.508v-8.57l-5.218 3.173a31.165 31.165 0 01-32.425 0l-.625-.38v-.005L36 47.64v8.39l.168.079a38.382 38.382 0 0036.807-1.712l1.461-.889z" fill="#4CC3FF"></path>
|
||||
<path d="M31.571 15.063v-8.57L0 25.694v8.614L31.571 53.51v-8.57L7.011 30l24.56-14.938zM74.432 53.511l31.571-19.202v-8.614L74.432 6.492v8.571l24.56 14.939-24.56 14.938v8.571z" fill="#000D1A"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 893 B |
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue