Compare commits
7 commits
ce34afa10a
...
714cd9be54
| Author | SHA1 | Date | |
|---|---|---|---|
| 714cd9be54 | |||
| 892a19bbcc | |||
| 7c6464330d | |||
| 45b2b93aba | |||
| 850a791851 | |||
| 864c0ffc06 | |||
| 359bc58749 |
9 changed files with 211 additions and 55 deletions
|
|
@ -328,9 +328,8 @@ class CrdModelFormMixin:
|
||||||
field.required = False
|
field.required = False
|
||||||
|
|
||||||
# Mark advanced fields with a CSS class and data attribute
|
# Mark advanced fields with a CSS class and data attribute
|
||||||
advanced_fields = getattr(self, "ADVANCED_FIELDS", [])
|
|
||||||
for name, field in self.fields.items():
|
for name, field in self.fields.items():
|
||||||
if name in advanced_fields:
|
if self.is_field_advanced(name):
|
||||||
field.widget.attrs.update(
|
field.widget.attrs.update(
|
||||||
{
|
{
|
||||||
"class": (
|
"class": (
|
||||||
|
|
@ -356,6 +355,17 @@ class CrdModelFormMixin:
|
||||||
return True
|
return True
|
||||||
return False
|
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):
|
def get_fieldsets(self):
|
||||||
fieldsets = []
|
fieldsets = []
|
||||||
|
|
||||||
|
|
@ -371,6 +381,7 @@ class CrdModelFormMixin:
|
||||||
"fields": general_fields,
|
"fields": general_fields,
|
||||||
"fieldsets": [],
|
"fieldsets": [],
|
||||||
"has_mandatory": self.has_mandatory_fields(general_fields),
|
"has_mandatory": self.has_mandatory_fields(general_fields),
|
||||||
|
"is_advanced": self.are_all_fields_advanced(general_fields),
|
||||||
}
|
}
|
||||||
if all(
|
if all(
|
||||||
[
|
[
|
||||||
|
|
@ -437,6 +448,9 @@ class CrdModelFormMixin:
|
||||||
title = f"{fieldset['title']}: {sub_fieldset['title']}: "
|
title = f"{fieldset['title']}: {sub_fieldset['title']}: "
|
||||||
for field in sub_fieldset["fields"]:
|
for field in sub_fieldset["fields"]:
|
||||||
self.strip_title(field, title)
|
self.strip_title(field, title)
|
||||||
|
sub_fieldset["is_advanced"] = self.are_all_fields_advanced(
|
||||||
|
sub_fieldset["fields"]
|
||||||
|
)
|
||||||
nested_fieldsets_list.append(sub_fieldset)
|
nested_fieldsets_list.append(sub_fieldset)
|
||||||
|
|
||||||
fieldset["fieldsets"] = nested_fieldsets_list
|
fieldset["fieldsets"] = nested_fieldsets_list
|
||||||
|
|
@ -453,6 +467,8 @@ class CrdModelFormMixin:
|
||||||
all_fields.extend(sub_fieldset["fields"])
|
all_fields.extend(sub_fieldset["fields"])
|
||||||
fieldset["has_mandatory"] = self.has_mandatory_fields(all_fields)
|
fieldset["has_mandatory"] = self.has_mandatory_fields(all_fields)
|
||||||
|
|
||||||
|
fieldset["is_advanced"] = self.are_all_fields_advanced(all_fields)
|
||||||
|
|
||||||
fieldsets.append(fieldset)
|
fieldsets.append(fieldset)
|
||||||
|
|
||||||
# Add 'others' tab if there are any fields
|
# Add 'others' tab if there are any fields
|
||||||
|
|
@ -463,6 +479,7 @@ class CrdModelFormMixin:
|
||||||
"fields": others,
|
"fields": others,
|
||||||
"fieldsets": [],
|
"fieldsets": [],
|
||||||
"has_mandatory": self.has_mandatory_fields(others),
|
"has_mandatory": self.has_mandatory_fields(others),
|
||||||
|
"is_advanced": self.are_all_fields_advanced(others),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import secrets
|
||||||
|
|
||||||
import rules
|
import rules
|
||||||
import urlman
|
import urlman
|
||||||
|
from auditlog.registry import auditlog
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
|
|
@ -467,6 +468,7 @@ class OrganizationInvitation(ServalaModelMixin, models.Model):
|
||||||
|
|
||||||
class urls(urlman.Urls):
|
class urls(urlman.Urls):
|
||||||
accept = "/invitations/{self.secret}/accept/"
|
accept = "/invitations/{self.secret}/accept/"
|
||||||
|
delete = "{self.organization.urls.details}invitations/{self.pk}/delete/"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Organization invitation")
|
verbose_name = _("Organization invitation")
|
||||||
|
|
@ -536,3 +538,7 @@ The Servala Team"""
|
||||||
recipient_list=[self.email],
|
recipient_list=[self.email],
|
||||||
fail_silently=False,
|
fail_silently=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
auditlog.register(OrganizationInvitation, serialize_data=True)
|
||||||
|
auditlog.register(OrganizationMembership, serialize_data=True)
|
||||||
|
|
|
||||||
|
|
@ -14,20 +14,26 @@ def has_organization_role(user, org, roles):
|
||||||
|
|
||||||
@rules.predicate
|
@rules.predicate
|
||||||
def is_organization_owner(user, obj):
|
def is_organization_owner(user, obj):
|
||||||
|
from servala.core.models.organization import OrganizationRole
|
||||||
|
|
||||||
if hasattr(obj, "organization"):
|
if hasattr(obj, "organization"):
|
||||||
org = obj.organization
|
org = obj.organization
|
||||||
else:
|
else:
|
||||||
org = obj
|
org = obj
|
||||||
return has_organization_role(user, org, ["owner"])
|
return has_organization_role(user, org, [OrganizationRole.OWNER])
|
||||||
|
|
||||||
|
|
||||||
@rules.predicate
|
@rules.predicate
|
||||||
def is_organization_admin(user, obj):
|
def is_organization_admin(user, obj):
|
||||||
|
from servala.core.models.organization import OrganizationRole
|
||||||
|
|
||||||
if hasattr(obj, "organization"):
|
if hasattr(obj, "organization"):
|
||||||
org = obj.organization
|
org = obj.organization
|
||||||
else:
|
else:
|
||||||
org = obj
|
org = obj
|
||||||
return has_organization_role(user, org, ["owner", "admin"])
|
return has_organization_role(
|
||||||
|
user, org, [OrganizationRole.OWNER, OrganizationRole.ADMIN]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@rules.predicate
|
@rules.predicate
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
<div class="dynamic-array-widget"
|
<div class="dynamic-array-widget"
|
||||||
id="{{ widget.attrs.id|default:'id_'|add:widget.name }}_container"
|
id="{{ widget.attrs.id|default:'id_'|add:widget.name }}_container"
|
||||||
data-name="{{ widget.name }}">
|
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 %}>
|
||||||
<div class="array-items">
|
<div class="array-items">
|
||||||
{% for item in value_list %}
|
{% for item in value_list %}
|
||||||
<div class="array-item d-flex mb-2">
|
<div class="array-item d-flex mb-2">
|
||||||
|
|
|
||||||
|
|
@ -67,18 +67,25 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% endpartialdef members-list %}
|
||||||
|
{% partialdef pending-invitations-card %}
|
||||||
{% if pending_invitations %}
|
{% if pending_invitations %}
|
||||||
<h5 class="mt-4">
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="card-title">
|
||||||
<i class="bi bi-envelope"></i> {% translate "Pending Invitations" %}
|
<i class="bi bi-envelope"></i> {% translate "Pending Invitations" %}
|
||||||
</h5>
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% translate "Email" %}</th>
|
<th>{% translate "Email" %}</th>
|
||||||
<th>{% translate "Role" %}</th>
|
<th>{% translate "Role" %}</th>
|
||||||
<th>{% translate "Sent" %}</th>
|
<th>{% translate "Sent" %}</th>
|
||||||
<th>{% translate "Link" %}</th>
|
<th>{% translate "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -96,14 +103,30 @@
|
||||||
onclick="navigator.clipboard.writeText('{{ request.scheme }}://{{ request.get_host }}{{ invitation.urls.accept }}'); this.textContent='Copied!'">
|
onclick="navigator.clipboard.writeText('{{ request.scheme }}://{{ request.get_host }}{{ invitation.urls.accept }}'); this.textContent='Copied!'">
|
||||||
<i class="bi bi-clipboard"></i> {% translate "Copy Link" %}
|
<i class="bi bi-clipboard"></i> {% translate "Copy Link" %}
|
||||||
</button>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endpartialdef members-list %}
|
{% endpartialdef pending-invitations-card %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
@ -214,6 +237,7 @@
|
||||||
<div class="card-body">{% partial members-list %}</div>
|
<div class="card-body">{% partial members-list %}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="pending-invitations-card">{% partial pending-invitations-card %}</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4 class="card-title">
|
<h4 class="card-title">
|
||||||
|
|
@ -222,6 +246,22 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="card-body">
|
<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">
|
<form method="post" class="form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="row">{{ invitation_form }}</div>
|
<div class="row">{{ invitation_form }}</div>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@
|
||||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||||
{% for fieldset in form.get_fieldsets %}
|
{% for fieldset in form.get_fieldsets %}
|
||||||
{% if not fieldset.hidden %}
|
{% if not fieldset.hidden %}
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item{% if fieldset.is_advanced %} advanced-field-group collapse{% endif %}"
|
||||||
|
role="presentation">
|
||||||
<button class="nav-link {% if forloop.first %}active{% endif %}{% if fieldset.has_mandatory %} has-mandatory{% endif %}"
|
<button class="nav-link {% if forloop.first %}active{% endif %}{% if fieldset.has_mandatory %} has-mandatory{% endif %}"
|
||||||
id="{{ fieldset.title|slugify }}-tab"
|
id="{{ fieldset.title|slugify }}-tab"
|
||||||
data-bs-toggle="tab"
|
data-bs-toggle="tab"
|
||||||
|
|
@ -48,10 +49,12 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for subfieldset in fieldset.fieldsets %}
|
{% for subfieldset in fieldset.fieldsets %}
|
||||||
{% if subfieldset.fields %}
|
{% if subfieldset.fields %}
|
||||||
|
<div {% if subfieldset.is_advanced %}class="advanced-field-group collapse"{% endif %}>
|
||||||
<h4 class="mt-3">{{ subfieldset.title }}</h4>
|
<h4 class="mt-3">{{ subfieldset.title }}</h4>
|
||||||
{% for field in subfieldset.fields %}
|
{% for field in subfieldset.fields %}
|
||||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ urlpatterns = [
|
||||||
views.OrganizationUpdateView.as_view(),
|
views.OrganizationUpdateView.as_view(),
|
||||||
name="organization.details",
|
name="organization.details",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"details/invitations/<int:pk>/delete/",
|
||||||
|
views.InvitationDeleteView.as_view(),
|
||||||
|
name="invitation.delete",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"services/",
|
"services/",
|
||||||
views.ServiceListView.as_view(),
|
views.ServiceListView.as_view(),
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from .generic import (
|
||||||
)
|
)
|
||||||
from .organization import (
|
from .organization import (
|
||||||
InvitationAcceptView,
|
InvitationAcceptView,
|
||||||
|
InvitationDeleteView,
|
||||||
OrganizationCreateView,
|
OrganizationCreateView,
|
||||||
OrganizationDashboardView,
|
OrganizationDashboardView,
|
||||||
OrganizationUpdateView,
|
OrganizationUpdateView,
|
||||||
|
|
@ -27,6 +28,7 @@ from .support import SupportView
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"IndexView",
|
"IndexView",
|
||||||
"InvitationAcceptView",
|
"InvitationAcceptView",
|
||||||
|
"InvitationDeleteView",
|
||||||
"LogoutView",
|
"LogoutView",
|
||||||
"OrganizationCreateView",
|
"OrganizationCreateView",
|
||||||
"OrganizationDashboardView",
|
"OrganizationDashboardView",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, DetailView, TemplateView
|
from django.views.generic import CreateView, DeleteView, DetailView, TemplateView
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rules.contrib.views import AutoPermissionRequiredMixin
|
from rules.contrib.views import AutoPermissionRequiredMixin
|
||||||
|
|
||||||
|
|
@ -14,7 +14,6 @@ from servala.core.models import (
|
||||||
Organization,
|
Organization,
|
||||||
OrganizationInvitation,
|
OrganizationInvitation,
|
||||||
OrganizationMembership,
|
OrganizationMembership,
|
||||||
OrganizationRole,
|
|
||||||
ServiceInstance,
|
ServiceInstance,
|
||||||
)
|
)
|
||||||
from servala.frontend.forms.organization import (
|
from servala.frontend.forms.organization import (
|
||||||
|
|
@ -22,7 +21,11 @@ from servala.frontend.forms.organization import (
|
||||||
OrganizationForm,
|
OrganizationForm,
|
||||||
OrganizationInvitationForm,
|
OrganizationInvitationForm,
|
||||||
)
|
)
|
||||||
from servala.frontend.views.mixins import HtmxUpdateView, OrganizationViewMixin
|
from servala.frontend.views.mixins import (
|
||||||
|
HtmxUpdateView,
|
||||||
|
HtmxViewMixin,
|
||||||
|
OrganizationViewMixin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationCreateView(AutoPermissionRequiredMixin, CreateView):
|
class OrganizationCreateView(AutoPermissionRequiredMixin, CreateView):
|
||||||
|
|
@ -108,10 +111,8 @@ class OrganizationDashboardView(
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
|
class OrganizationMembershipMixin:
|
||||||
template_name = "frontend/organizations/update.html"
|
template_name = "frontend/organizations/update.html"
|
||||||
form_class = OrganizationForm
|
|
||||||
fragments = ("org-name", "org-name-edit", "members-list")
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def user_role(self):
|
def user_role(self):
|
||||||
|
|
@ -126,10 +127,9 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def can_manage_members(self):
|
def can_manage_members(self):
|
||||||
return self.user_role in [
|
return self.request.user.has_perm(
|
||||||
OrganizationRole.ADMIN,
|
"core.change_organization", self.request.organization
|
||||||
OrganizationRole.OWNER,
|
)
|
||||||
]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
@ -159,6 +159,18 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
|
||||||
|
|
||||||
return context
|
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):
|
def post(self, request, *args, **kwargs):
|
||||||
if "invite_email" in request.POST:
|
if "invite_email" in request.POST:
|
||||||
return self.handle_invitation(request)
|
return self.handle_invitation(request)
|
||||||
|
|
@ -199,7 +211,11 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for error in form.errors.values():
|
for error in form.errors.values():
|
||||||
messages.error(request, error.as_text())
|
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)
|
||||||
|
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
|
@ -259,3 +275,61 @@ class InvitationAcceptView(TemplateView):
|
||||||
|
|
||||||
request.session.pop("invitation_next", None)
|
request.session.pop("invitation_next", None)
|
||||||
return redirect(invitation.organization.urls.base)
|
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())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue