Compare commits

...

10 commits

19 changed files with 295 additions and 30 deletions

View file

@ -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")

View 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()}

View file

@ -0,0 +1,3 @@
from .organization import OrganizationCreateForm
__all__ = ["OrganizationCreateForm"]

View 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",)

View 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)

View 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 %}

View 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>

View file

@ -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>

View file

@ -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 %}

View 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>

View file

@ -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>

View 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"),
]

View file

@ -0,0 +1,10 @@
from .auth import LogoutView
from .generic import IndexView, ProfileView
from .organization import OrganizationCreateView
__all__ = [
"IndexView",
"LogoutView",
"OrganizationCreateView",
"ProfileView",
]

View 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)

View 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 "/"

View file

@ -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`

View 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

View file

@ -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