Compare commits

..

No commits in common. "714cd9be5452adf635b4e602c19c83b07e63074b" and "ce34afa10a8dd18aebaa13666bc3eff4180c76e4" have entirely different histories.

9 changed files with 55 additions and 211 deletions

View file

@ -328,8 +328,9 @@ class CrdModelFormMixin:
field.required = False
# Mark advanced fields with a CSS class and data attribute
advanced_fields = getattr(self, "ADVANCED_FIELDS", [])
for name, field in self.fields.items():
if self.is_field_advanced(name):
if name in advanced_fields:
field.widget.attrs.update(
{
"class": (
@ -355,17 +356,6 @@ class CrdModelFormMixin:
return True
return False
def is_field_advanced(self, field_name):
advanced_fields = getattr(self, "ADVANCED_FIELDS", [])
return field_name in advanced_fields or any(
field_name.startswith(f"{af}.") for af in advanced_fields
)
def are_all_fields_advanced(self, field_list):
if not field_list:
return False
return all(self.is_field_advanced(field_name) for field_name in field_list)
def get_fieldsets(self):
fieldsets = []
@ -381,7 +371,6 @@ class CrdModelFormMixin:
"fields": general_fields,
"fieldsets": [],
"has_mandatory": self.has_mandatory_fields(general_fields),
"is_advanced": self.are_all_fields_advanced(general_fields),
}
if all(
[
@ -448,9 +437,6 @@ class CrdModelFormMixin:
title = f"{fieldset['title']}: {sub_fieldset['title']}: "
for field in sub_fieldset["fields"]:
self.strip_title(field, title)
sub_fieldset["is_advanced"] = self.are_all_fields_advanced(
sub_fieldset["fields"]
)
nested_fieldsets_list.append(sub_fieldset)
fieldset["fieldsets"] = nested_fieldsets_list
@ -467,8 +453,6 @@ class CrdModelFormMixin:
all_fields.extend(sub_fieldset["fields"])
fieldset["has_mandatory"] = self.has_mandatory_fields(all_fields)
fieldset["is_advanced"] = self.are_all_fields_advanced(all_fields)
fieldsets.append(fieldset)
# Add 'others' tab if there are any fields
@ -479,7 +463,6 @@ class CrdModelFormMixin:
"fields": others,
"fieldsets": [],
"has_mandatory": self.has_mandatory_fields(others),
"is_advanced": self.are_all_fields_advanced(others),
}
)

View file

@ -2,7 +2,6 @@ import secrets
import rules
import urlman
from auditlog.registry import auditlog
from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import send_mail
@ -468,7 +467,6 @@ class OrganizationInvitation(ServalaModelMixin, models.Model):
class urls(urlman.Urls):
accept = "/invitations/{self.secret}/accept/"
delete = "{self.organization.urls.details}invitations/{self.pk}/delete/"
class Meta:
verbose_name = _("Organization invitation")
@ -538,7 +536,3 @@ The Servala Team"""
recipient_list=[self.email],
fail_silently=False,
)
auditlog.register(OrganizationInvitation, serialize_data=True)
auditlog.register(OrganizationMembership, serialize_data=True)

View file

@ -14,26 +14,20 @@ def has_organization_role(user, org, roles):
@rules.predicate
def is_organization_owner(user, obj):
from servala.core.models.organization import OrganizationRole
if hasattr(obj, "organization"):
org = obj.organization
else:
org = obj
return has_organization_role(user, org, [OrganizationRole.OWNER])
return has_organization_role(user, org, ["owner"])
@rules.predicate
def is_organization_admin(user, obj):
from servala.core.models.organization import OrganizationRole
if hasattr(obj, "organization"):
org = obj.organization
else:
org = obj
return has_organization_role(
user, org, [OrganizationRole.OWNER, OrganizationRole.ADMIN]
)
return has_organization_role(user, org, ["owner", "admin"])
@rules.predicate

View file

@ -1,9 +1,6 @@
<div class="dynamic-array-widget"
id="{{ widget.attrs.id|default:'id_'|add:widget.name }}_container"
data-name="{{ widget.name }}"
{% for name, value in widget.attrs.items %}{% if value is not False and name != "id" and name != "class" %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}
{% endif %}
{% endfor %}>
data-name="{{ widget.name }}">
<div class="array-items">
{% for item in value_list %}
<div class="array-item d-flex mb-2">

View file

@ -67,25 +67,18 @@
</tbody>
</table>
</div>
{% endpartialdef members-list %}
{% partialdef pending-invitations-card %}
{% if pending_invitations %}
<div class="card">
<div class="card-header">
<h4 class="card-title">
<h5 class="mt-4">
<i class="bi bi-envelope"></i> {% translate "Pending Invitations" %}
</h4>
</div>
<div class="card-content">
<div class="card-body">
</h5>
<div class="table-responsive">
<table class="table table-hover">
<table class="table table-sm">
<thead>
<tr>
<th>{% translate "Email" %}</th>
<th>{% translate "Role" %}</th>
<th>{% translate "Sent" %}</th>
<th>{% translate "Actions" %}</th>
<th>{% translate "Link" %}</th>
</tr>
</thead>
<tbody>
@ -103,30 +96,14 @@
onclick="navigator.clipboard.writeText('{{ request.scheme }}://{{ request.get_host }}{{ invitation.urls.accept }}'); this.textContent='Copied!'">
<i class="bi bi-clipboard"></i> {% translate "Copy Link" %}
</button>
<form method="post"
action="{{ invitation.urls.delete }}"
style="display: inline"
hx-post="{{ invitation.urls.delete }}"
hx-target="#pending-invitations-card"
hx-swap="outerHTML"
hx-confirm="{% translate 'Are you sure you want to delete this invitation?' %}">
{% csrf_token %}
<input type="hidden" name="fragment" value="pending-invitations-card">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i> {% translate "Delete" %}
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endif %}
{% endpartialdef pending-invitations-card %}
{% endpartialdef members-list %}
{% block content %}
<section class="section">
<div class="card">
@ -237,7 +214,6 @@
<div class="card-body">{% partial members-list %}</div>
</div>
</div>
<div id="pending-invitations-card">{% partial pending-invitations-card %}</div>
<div class="card">
<div class="card-header">
<h4 class="card-title">
@ -246,22 +222,6 @@
</div>
<div class="card-content">
<div class="card-body">
<div class="alert alert-light mb-3">
<h6>
<i class="bi bi-info-circle"></i> {% translate "Role Permissions" %}
</h6>
<ul class="mb-0">
<li>
<strong>{% translate "Owner" %}:</strong> {% translate "Can manage all organization settings, members, services, and can appoint administrators." %}
</li>
<li>
<strong>{% translate "Administrator" %}:</strong> {% translate "Can manage members, invite users, and manage all services and instances." %}
</li>
<li>
<strong>{% translate "Member" %}:</strong> {% translate "Can view organization details, create and manage their own service instances." %}
</li>
</ul>
</div>
<form method="post" class="form">
{% csrf_token %}
<div class="row">{{ invitation_form }}</div>

View file

@ -21,8 +21,7 @@
<ul class="nav nav-tabs" id="myTab" role="tablist">
{% for fieldset in form.get_fieldsets %}
{% if not fieldset.hidden %}
<li class="nav-item{% if fieldset.is_advanced %} advanced-field-group collapse{% endif %}"
role="presentation">
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.first %}active{% endif %}{% if fieldset.has_mandatory %} has-mandatory{% endif %}"
id="{{ fieldset.title|slugify }}-tab"
data-bs-toggle="tab"
@ -49,12 +48,10 @@
{% endfor %}
{% for subfieldset in fieldset.fieldsets %}
{% if subfieldset.fields %}
<div {% if subfieldset.is_advanced %}class="advanced-field-group collapse"{% endif %}>
<h4 class="mt-3">{{ subfieldset.title }}</h4>
{% for field in subfieldset.fields %}
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
{% endfor %}
</div>
{% endif %}
{% endfor %}
</div>

View file

@ -30,11 +30,6 @@ urlpatterns = [
views.OrganizationUpdateView.as_view(),
name="organization.details",
),
path(
"details/invitations/<int:pk>/delete/",
views.InvitationDeleteView.as_view(),
name="invitation.delete",
),
path(
"services/",
views.ServiceListView.as_view(),

View file

@ -9,7 +9,6 @@ from .generic import (
)
from .organization import (
InvitationAcceptView,
InvitationDeleteView,
OrganizationCreateView,
OrganizationDashboardView,
OrganizationUpdateView,
@ -28,7 +27,6 @@ from .support import SupportView
__all__ = [
"IndexView",
"InvitationAcceptView",
"InvitationDeleteView",
"LogoutView",
"OrganizationCreateView",
"OrganizationDashboardView",

View file

@ -5,7 +5,7 @@ from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DeleteView, DetailView, TemplateView
from django.views.generic import CreateView, DetailView, TemplateView
from django_scopes import scopes_disabled
from rules.contrib.views import AutoPermissionRequiredMixin
@ -14,6 +14,7 @@ from servala.core.models import (
Organization,
OrganizationInvitation,
OrganizationMembership,
OrganizationRole,
ServiceInstance,
)
from servala.frontend.forms.organization import (
@ -21,11 +22,7 @@ from servala.frontend.forms.organization import (
OrganizationForm,
OrganizationInvitationForm,
)
from servala.frontend.views.mixins import (
HtmxUpdateView,
HtmxViewMixin,
OrganizationViewMixin,
)
from servala.frontend.views.mixins import HtmxUpdateView, OrganizationViewMixin
class OrganizationCreateView(AutoPermissionRequiredMixin, CreateView):
@ -111,8 +108,10 @@ class OrganizationDashboardView(
return context
class OrganizationMembershipMixin:
class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
template_name = "frontend/organizations/update.html"
form_class = OrganizationForm
fragments = ("org-name", "org-name-edit", "members-list")
@cached_property
def user_role(self):
@ -127,9 +126,10 @@ class OrganizationMembershipMixin:
@cached_property
def can_manage_members(self):
return self.request.user.has_perm(
"core.change_organization", self.request.organization
)
return self.user_role in [
OrganizationRole.ADMIN,
OrganizationRole.OWNER,
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -159,18 +159,6 @@ class OrganizationMembershipMixin:
return context
class OrganizationUpdateView(
OrganizationViewMixin, OrganizationMembershipMixin, HtmxUpdateView
):
form_class = OrganizationForm
fragments = (
"org-name",
"org-name-edit",
"members-list",
"pending-invitations-card",
)
def post(self, request, *args, **kwargs):
if "invite_email" in request.POST:
return self.handle_invitation(request)
@ -211,11 +199,7 @@ class OrganizationUpdateView(
)
else:
for error in form.errors.values():
for error_msg in error:
messages.error(request, error_msg)
if self.is_htmx and self._get_fragment():
return self.get(request, *self.args, **self.kwargs)
messages.error(request, error.as_text())
return redirect(self.get_success_url())
@ -275,61 +259,3 @@ class InvitationAcceptView(TemplateView):
request.session.pop("invitation_next", None)
return redirect(invitation.organization.urls.base)
class InvitationDeleteView(HtmxViewMixin, OrganizationMembershipMixin, DeleteView):
model = OrganizationInvitation
http_method_names = ["get", "post"]
fragments = ("pending-invitations-card",)
def get_queryset(self):
return OrganizationInvitation.objects.filter(accepted_by__isnull=True)
def get_success_url(self):
return self.object.organization.urls.details
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
organization = self.request.organization
context["pending_invitations"] = OrganizationInvitation.objects.filter(
organization=organization, accepted_by__isnull=True
).order_by("-created_at")
return context
def _check_permission(self):
return self.request.user.has_perm(
"core.change_organization", self.request.organization
)
def get_object(self):
if self.request.method == "POST" and self.is_htmx:
try:
return super().get_object()
except Exception:
return
return super().get_object()
def post(self, request, *args, **kwargs):
self.object = self.get_object()
organization = self.object.organization
if not self._check_permission():
if not self.is_htmx:
messages.error(
request,
_("You do not have permission to delete this invitation."),
)
return redirect(organization.urls.details)
email = self.object.email
self.object.delete()
if not self.is_htmx:
messages.success(
request,
_("Invitation for {email} has been deleted.").format(email=email),
)
if self.is_htmx and self._get_fragment():
return self.get(request, *args, **kwargs)
return redirect(self.get_success_url())