diff --git a/src/servala/core/middleware.py b/src/servala/core/middleware.py deleted file mode 100644 index 07ebe4a..0000000 --- a/src/servala/core/middleware.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.shortcuts import get_object_or_404 -from django.urls import resolve - -from servala.core.models import Organization - - -class OrganizationMiddleware: - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - url = resolve(request.path_info) - - organization_slug = url.kwargs.get("organization") - if organization_slug: - pk = organization_slug.rsplit("-", maxsplit=1)[-1] - request.organization = get_object_or_404(Organization, pk=pk) - else: - request.organization = None - - return self.get_response(request) diff --git a/src/servala/core/migrations/0003_billing_entity_nullable.py b/src/servala/core/migrations/0003_billing_entity_nullable.py deleted file mode 100644 index bae2ae2..0000000 --- a/src/servala/core/migrations/0003_billing_entity_nullable.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.2b1 on 2025-03-20 08:12 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0002_billingentity_created_at_billingentity_updated_at_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="organization", - name="billing_entity", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="organizations", - to="core.billingentity", - verbose_name="Billing entity", - ), - ), - ] diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 8a1c552..0419175 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -1,8 +1,5 @@ from django.conf import settings from django.db import models -from django.urls import reverse -from django.utils.functional import cached_property -from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from .mixins import ServalaModelMixin @@ -16,7 +13,6 @@ class Organization(ServalaModelMixin, models.Model): on_delete=models.PROTECT, related_name="organizations", verbose_name=_("Billing entity"), - null=True, # TODO: billing entity should be required ) origin = models.ForeignKey( to="OrganizationOrigin", @@ -32,15 +28,6 @@ class Organization(ServalaModelMixin, models.Model): verbose_name=_("Members"), ) - @cached_property - def slug(self): - return f"{slugify(self.name)}-{self.id}" - - def get_absolute_url(self): - return reverse( - "frontend:organization.dashboard", kwargs={"organization": self.slug} - ) - def set_owner(self, user): OrganizationMembership.objects.filter(user=user, organization=self).delete() OrganizationMembership.objects.create( diff --git a/src/servala/frontend/context_processors.py b/src/servala/frontend/context_processors.py index 1ff2a14..1a180d8 100644 --- a/src/servala/frontend/context_processors.py +++ b/src/servala/frontend/context_processors.py @@ -2,4 +2,4 @@ def add_organizations(request): if not request.user.is_authenticated: return {"user_organizations": []} - return {"user_organizations": request.user.organizations.all().order_by("name")} + return {"user_organizations": request.user.organizations.all()} diff --git a/src/servala/frontend/forms/__init__.py b/src/servala/frontend/forms/__init__.py index fe0ffc1..8169b61 100644 --- a/src/servala/frontend/forms/__init__.py +++ b/src/servala/frontend/forms/__init__.py @@ -1,4 +1,3 @@ from .organization import OrganizationCreateForm -from .profile import UserProfileForm -__all__ = ["OrganizationCreateForm", "UserProfileForm"] +__all__ = ["OrganizationCreateForm"] diff --git a/src/servala/frontend/forms/mixins.py b/src/servala/frontend/forms/mixins.py deleted file mode 100644 index 4ca1796..0000000 --- a/src/servala/frontend/forms/mixins.py +++ /dev/null @@ -1,20 +0,0 @@ -from servala.frontend.forms.renderers import InlineFormRenderer - - -class HtmxMixin: - """ - Form mixin that retains only a single field when specified. - Useful when sending single fields with htmx. - """ - - default_renderer = InlineFormRenderer - - def __init__(self, *args, **kwargs): - self.single_field = kwargs.pop("single_field", None) - - super().__init__(*args, **kwargs) - - if self.single_field and self.single_field in self.fields: - field = self.fields[self.single_field] - self.fields.clear() - self.fields[self.single_field] = field diff --git a/src/servala/frontend/forms/profile.py b/src/servala/frontend/forms/profile.py deleted file mode 100644 index 23f1df6..0000000 --- a/src/servala/frontend/forms/profile.py +++ /dev/null @@ -1,11 +0,0 @@ -from django import forms - -from servala.core.models import User -from servala.frontend.forms.mixins import HtmxMixin - - -class UserProfileForm(HtmxMixin, forms.ModelForm): - - class Meta: - model = User - fields = ("email", "company") diff --git a/src/servala/frontend/forms/renderers.py b/src/servala/frontend/forms/renderers.py index 24e412b..f8cdb5d 100644 --- a/src/servala/frontend/forms/renderers.py +++ b/src/servala/frontend/forms/renderers.py @@ -1,5 +1,4 @@ from django.forms.renderers import TemplatesSetting -from django.forms.widgets import Textarea def inject_class(f, class_name): @@ -17,31 +16,13 @@ class VerticalFormRenderer(TemplatesSetting): form_template_name = "frontend/forms/form.html" field_template_name = "frontend/forms/vertical_field.html" - def get_class_names(self, input_type): - if input_type == "checkbox": - return "form-check-input" - return "form-control" - - def get_widget_input_type(self, widget): - if isinstance(widget, Textarea): - return "textarea" - return widget.input_type - - def get_field_input_type(self, field): - widget = field.field.widget - if inner_widget := getattr(widget, "widget", None): - widget = inner_widget - return self.get_widget_input_type(widget) - def render(self, template_name, context, request=None): if field := context.get("field"): - input_type = self.get_field_input_type(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, self.get_class_names(input_type) + field.build_widget_attrs, class_name ) return super().render(template_name, context, request) - - -class InlineFormRenderer(VerticalFormRenderer): - form_template_name = "frontend/forms/form.html" - field_template_name = "frontend/forms/inline_field.html" diff --git a/src/servala/frontend/templates/frontend/base.html b/src/servala/frontend/templates/frontend/base.html index 87c8a1c..62c6b33 100644 --- a/src/servala/frontend/templates/frontend/base.html +++ b/src/servala/frontend/templates/frontend/base.html @@ -8,7 +8,6 @@ - diff --git a/src/servala/frontend/templates/frontend/forms/field.html b/src/servala/frontend/templates/frontend/forms/field.html deleted file mode 100644 index fe56619..0000000 --- a/src/servala/frontend/templates/frontend/forms/field.html +++ /dev/null @@ -1,21 +0,0 @@ -{% load i18n %} -<div class="form-group{% if field.field.required %} mandatory{% endif %}{% if errors %} is-invalid{% endif %}{% if extra_class %} {{ extra_class }}{% endif %}"> - {% if not hide_label %} - {% if field.field.widget.input_type != "checkbox" or field.field.widget.allow_multiple_selected %} - <label for="{{ field.auto_id }}" class="form-label">{{ field.label }}</label> - {% endif %} - {% 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 }}" class="form-check-label form-label">{{ 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> diff --git a/src/servala/frontend/templates/frontend/forms/inline_field.html b/src/servala/frontend/templates/frontend/forms/inline_field.html deleted file mode 100644 index ab8edba..0000000 --- a/src/servala/frontend/templates/frontend/forms/inline_field.html +++ /dev/null @@ -1 +0,0 @@ -{% include "frontend/forms/field.html" with extra_class="d-inline" hide_label=True %} diff --git a/src/servala/frontend/templates/frontend/forms/vertical_field.html b/src/servala/frontend/templates/frontend/forms/vertical_field.html index 544d07d..a2457d9 100644 --- a/src/servala/frontend/templates/frontend/forms/vertical_field.html +++ b/src/servala/frontend/templates/frontend/forms/vertical_field.html @@ -1 +1,21 @@ -<div class="col-12">{% include "frontend/forms/field.html" %}</div> +{% load i18n %} +<div class="col-12"> + <div class="form-group{% if field.field.required %} mandatory{% endif %}{% if errors %} is-invalid{% endif %}"> + {% if field.field.widget.input_type != "checkbox" or field.field.widget.allow_multiple_selected %} + <label for="{{ field.auto_id }}" class="form-label">{{ field.label }}</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 }}" class="form-check-label form-label">{{ 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> diff --git a/src/servala/frontend/templates/frontend/organizations/dashboard.html b/src/servala/frontend/templates/frontend/index.html similarity index 100% rename from src/servala/frontend/templates/frontend/organizations/dashboard.html rename to src/servala/frontend/templates/frontend/index.html diff --git a/src/servala/frontend/templates/frontend/profile.html b/src/servala/frontend/templates/frontend/profile.html index cfad0e9..9532e96 100644 --- a/src/servala/frontend/templates/frontend/profile.html +++ b/src/servala/frontend/templates/frontend/profile.html @@ -1,6 +1,5 @@ {% extends "frontend/base.html" %} {% load i18n static %} -{% load partials %} {% block html_title %} {% block page_title %} {% translate "Profile" %} @@ -11,62 +10,6 @@ <h4 class="card-title">{% translate "Account" %}</h4> </div> {% endblock %} -{% partialdef user-email %} -<td> - {{ request.user.email }} - <button class="btn btn-primary" - hx-get="{% url 'frontend:profile' %}?fragment=user-email-edit" - hx-target="closest td" - hx-swap="outerHTML">{% translate "Edit" %}</button> -</td> -{% endpartialdef user-email %} -{% partialdef user-company %} -<td> - {{ request.user.company|default:"–" }} - <button class="btn btn-primary" - hx-get="{% url 'frontend:profile' %}?fragment=user-company-edit" - hx-target="closest td" - hx-swap="outerHTML">{% translate "Edit" %}</button> -</td> -{% endpartialdef user-company %} -{% partialdef user-email-edit %} -<td> - <form hx-target="closest td" - hx-swap="outerHTML" - hx-post="{{ request.url }}"> - <div class="d-flex align-items-baseline"> - {{ form.email.as_field_group }} - <input type="hidden" name="hx-single-field" value="email"> - <input type="hidden" name="fragment" value="user-email"> - <button type="submit" class="btn btn-primary mx-1">{% translate "Save" %}</button> - <button type="button" - class="btn btn-secondary" - hx-get="{{ request.path }}?fragment=user-email" - hx-target="closest td" - hx-swap="outerHTML">{% translate "Cancel" %}</button> - </div> - </form> -</td> -{% endpartialdef %} -{% partialdef user-company-edit %} -<td> - <form hx-target="closest td" - hx-swap="outerHTML" - hx-post="{{ request.url }}"> - <div class="d-flex align-items-baseline"> - {{ form.company.as_field_group }} - <input type="hidden" name="hx-single-field" value="company"> - <input type="hidden" name="fragment" value="user-company"> - <button type="submit" class="btn mx-1 btn-primary">{% translate "Save" %}</button> - <button type="button" - class="btn btn-secondary" - hx-get="{{ request.path }}?fragment=user-company" - hx-target="closest td" - hx-swap="outerHTML">{% translate "Cancel" %}</button> - </div> - </form> -</td> -{% endpartialdef %} {% block content %} <section> <div class="row match-height"> @@ -81,20 +24,20 @@ <table class="table table-lg"> <tbody> <tr> - <th class="w-25">{% translate "Name" %}</th> - <td>{{ request.user.first_name }} {{ request.user.last_name }}</td> + <th>{% translate "E-mail" %}</th> + <td>{{ request.user.email }}</td> </tr> <tr> - <th class="w-25"> - <span class="d-flex mt-2">{% translate "E-mail" %}</span> - </th> - {% partial user-email %} + <th>{% translate "First name" %}</th> + <td>{{ request.user.first_name }}</td> </tr> <tr> - <th class="w-25"> - <span class="d-flex mt-2">{% translate "Company" %}</span> - </th> - {% partial user-company %} + <th>{% translate "Last name" %}</th> + <td>{{ request.user.last_name }}</td> + </tr> + <tr> + <th>{% translate "Company" %}</th> + <td>{{ request.user.company }}</td> </tr> </tbody> </table> diff --git a/src/servala/frontend/templates/includes/form.html b/src/servala/frontend/templates/includes/form.html index d998fc1..fec4356 100644 --- a/src/servala/frontend/templates/includes/form.html +++ b/src/servala/frontend/templates/includes/form.html @@ -5,13 +5,11 @@ {% include "includes/form_errors.html" %} {% csrf_token %} {{ form }} - <div class="col-sm-12 d-flex justify-content-end"> - <button class="btn btn-primary me-1 mb-1" type="submit"> - {% if form_submit_label %} - {{ form_submit_label }} - {% else %} - {% translate "Save" %} - {% endif %} - </button> - </div> + <button class="btn btn-primary" type="submit"> + {% if form_submit_label %} + {{ form_submit_label }} + {% else %} + {% translate "Save" %} + {% endif %} + </button> </form> diff --git a/src/servala/frontend/templates/includes/sidebar.html b/src/servala/frontend/templates/includes/sidebar.html index ae08b51..8c47293 100644 --- a/src/servala/frontend/templates/includes/sidebar.html +++ b/src/servala/frontend/templates/includes/sidebar.html @@ -5,7 +5,7 @@ <div class="sidebar-header position-relative"> <div class="d-flex justify-content-between align-items-center"> <div class="logo"> - <a href="{% if request.organization %}{{ request.organization.get_absolute_url }}{% else %}/{% endif %}"> + <a href="index.html"> <img src="" alt="{% translate 'Logo' %}" srcset=""> </a> </div> @@ -66,34 +66,26 @@ {% else %} {# request.user.is_authenticated #} <li class="sidebar-item"> - {% if user_organizations.count %} - <button class="btn btn-outline-primary dropdown-toggle w-100" + {% 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 request.organization %} - {{ request.organization.name }} + {% if current_organization %} + {{ current_organization.name }} {% else %} {% translate "Organizations" %} {% endif %} </button> - <div class="dropdown-menu shadow" - aria-labelledby="organizationDropdown" - id="organization-dropdown"> + <div class="dropdown-menu" aria-labelledby="organizationDropdown"> {% for organization in user_organizations %} - <a class="dropdown-item{% if organization == request.organization %} active{% endif %}" - href="{{ organization.get_absolute_url }}"> - <i class="bi bi-building-fill me-1"></i> - {{ organization.name }} - </a> + <a class="dropdown-item" href="#TODO">{{ organization.name }}</a> {% endfor %} - <a href="{% url 'frontend:organization.create' %}" class="dropdown-item"> - <i class="bi bi-building-add me-1"></i> - <span>{% translate "Create organization" %}</span> - </a> </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> @@ -101,15 +93,12 @@ </a> {% endif %} </li> - {% if request.organization %} - <li class="sidebar-item"> - <a href="{{ request.organization.get_absolute_url }}" - class='sidebar-link'> - <i class="bi bi-grid-fill"></i> - <span>{% translate 'Dashboard' %}</span> - </a> - </li> - {% endif %} + <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'> diff --git a/src/servala/frontend/urls.py b/src/servala/frontend/urls.py index e947a7b..fbaaa08 100644 --- a/src/servala/frontend/urls.py +++ b/src/servala/frontend/urls.py @@ -1,5 +1,4 @@ -from django.urls import include, path -from django.views.generic import RedirectView +from django.urls import path from servala.frontend import views @@ -11,17 +10,5 @@ urlpatterns = [ views.OrganizationCreateView.as_view(), name="organization.create", ), - path( - "<slug:organization>/", - include( - [ - path( - "", - views.OrganizationDashboardView.as_view(), - name="organization.dashboard", - ), - ] - ), - ), - path("", RedirectView.as_view(pattern_name="frontend:profile"), name="index"), + path("", views.IndexView.as_view(), name="index"), ] diff --git a/src/servala/frontend/views/__init__.py b/src/servala/frontend/views/__init__.py index 8dc1ee8..20b4398 100644 --- a/src/servala/frontend/views/__init__.py +++ b/src/servala/frontend/views/__init__.py @@ -1,11 +1,10 @@ from .auth import LogoutView from .generic import IndexView, ProfileView -from .organization import OrganizationCreateView, OrganizationDashboardView +from .organization import OrganizationCreateView __all__ = [ "IndexView", "LogoutView", "OrganizationCreateView", - "OrganizationDashboardView", "ProfileView", ] diff --git a/src/servala/frontend/views/generic.py b/src/servala/frontend/views/generic.py index 12300fd..3b2196b 100644 --- a/src/servala/frontend/views/generic.py +++ b/src/servala/frontend/views/generic.py @@ -1,41 +1,21 @@ from django.conf import settings -from django.urls import reverse_lazy -from django.utils.functional import cached_property -from django.views.generic import TemplateView, UpdateView - -from servala.core.models import User -from servala.frontend.forms.profile import UserProfileForm -from servala.frontend.views.mixins import HtmxMixin +from django.views.generic import TemplateView class IndexView(TemplateView): template_name = "frontend/index.html" -class ProfileView(HtmxMixin, UpdateView): +class ProfileView(TemplateView): template_name = "frontend/profile.html" - form_class = UserProfileForm - success_url = reverse_lazy("frontend:profile") - fragments = ("user-email", "user-email-edit", "user-company", "user-company-edit") - model = User - - def get_object(self): - return self.request.user - - @cached_property - def object(self): - return self.get_object() def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - keycloak_settings = settings.SOCIALACCOUNT_PROVIDERS["openid_connect"] - keycloak_server_url = keycloak_settings["APPS"][0]["settings"]["server_url"] + keycloak_server_url = settings.SOCIALACCOUNT_PROVIDERS["openid_connect"][ + "APPS" + ][0]["settings"]["server_url"] account_url = keycloak_server_url.replace( "/.well-known/openid-configuration", "/account" ) context["account_href"] = account_url return context - - def form_valid(self, form): - form.save() - return super().form_valid(form) diff --git a/src/servala/frontend/views/mixins.py b/src/servala/frontend/views/mixins.py deleted file mode 100644 index 3184e86..0000000 --- a/src/servala/frontend/views/mixins.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.utils.functional import cached_property -from django.views.generic import TemplateView - - -class HtmxMixin(TemplateView): - fragments = [] - - @cached_property - def is_htmx(self): - return self.request.headers.get("HX-Request") - - def _get_fragment(self): - if self.request.method == "POST": - fragment = self.request.POST.get("fragment") - else: - fragment = self.request.GET.get("fragment") - if fragment and fragment in self.fragments: - return fragment - - def get_template_names(self): - template_names = super().get_template_names() - if self.is_htmx and (fragment := self._get_fragment()): - return [f"{template_names[0]}#{fragment}"] - return template_names - - def get_form_kwargs(self): - result = super().get_form_kwargs() - if self.is_htmx and (field_name := self.request.POST.get("hx-single-field")): - result["single_field"] = field_name - return result - - def form_valid(self, form): - result = super().form_valid(form) - if self.is_htmx and self._get_fragment(): - return self.get(self.request, *self.args, **self.kwargs) - return result diff --git a/src/servala/frontend/views/organization.py b/src/servala/frontend/views/organization.py index 9ef4e37..020d28b 100644 --- a/src/servala/frontend/views/organization.py +++ b/src/servala/frontend/views/organization.py @@ -1,5 +1,4 @@ -from django.shortcuts import redirect -from django.views.generic import FormView, TemplateView +from django.views.generic import FormView from servala.frontend.forms import OrganizationCreateForm @@ -9,11 +8,8 @@ class OrganizationCreateView(FormView): template_name = "frontend/organizations/create.html" def form_valid(self, form): - instance = form.instance.create_organization( - form.instance, owner=self.request.user - ) - return redirect(instance.get_absolute_url()) + form.instance.create_organization(form.instance, owner=self.request.user) + return super().form_valid(form) - -class OrganizationDashboardView(TemplateView): - template_name = "frontend/organizations/dashboard.html" + def get_success_url(self): + return "/" diff --git a/src/servala/settings.py b/src/servala/settings.py index 7bc2f9f..9260988 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -116,7 +116,6 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", "allauth.account.middleware.AccountMiddleware", "django.contrib.auth.middleware.LoginRequiredMiddleware", - "servala.core.middleware.OrganizationMiddleware", ] LOGIN_URL = "account_login" diff --git a/src/servala/static/css/servala.css b/src/servala/static/css/servala.css deleted file mode 100644 index 826a458..0000000 --- a/src/servala/static/css/servala.css +++ /dev/null @@ -1,3 +0,0 @@ -.form-group.d-inline { - margin-bottom: 0; -}