October feature list #226
6 changed files with 159 additions and 54 deletions
|
|
@ -468,6 +468,7 @@ 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")
|
||||
|
|
|
|||
|
|
@ -14,20 +14,26 @@ 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, ["owner"])
|
||||
return has_organization_role(user, org, [OrganizationRole.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, ["owner", "admin"])
|
||||
return has_organization_role(
|
||||
user, org, [OrganizationRole.OWNER, OrganizationRole.ADMIN]
|
||||
)
|
||||
|
||||
|
||||
@rules.predicate
|
||||
|
|
|
|||
|
|
@ -67,18 +67,25 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endpartialdef members-list %}
|
||||
{% partialdef pending-invitations-card %}
|
||||
{% 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" %}
|
||||
</h5>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Email" %}</th>
|
||||
<th>{% translate "Role" %}</th>
|
||||
<th>{% translate "Sent" %}</th>
|
||||
<th>{% translate "Link" %}</th>
|
||||
<th>{% translate "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -96,14 +103,30 @@
|
|||
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 members-list %}
|
||||
{% endpartialdef pending-invitations-card %}
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="card">
|
||||
|
|
@ -214,6 +237,7 @@
|
|||
<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">
|
||||
|
|
@ -238,18 +262,12 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<form class="form"
|
||||
hx-post="{{ request.path }}"
|
||||
hx-target="#pending-invitations-card"
|
||||
hx-swap="outerHTML"
|
||||
hx-on::after-request="if(event.detail.successful) this.reset()">
|
||||
<form method="post" class="form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="fragment" value="pending-invitations-card">
|
||||
<input type="hidden" name="invite_email" value="1">
|
||||
<div class="row">{{ invitation_form }}</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<button type="submit" class="btn btn-primary" name="invite_email" value="1">
|
||||
<i class="bi bi-send"></i> {% translate "Send Invitation" %}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ 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(),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from .generic import (
|
|||
)
|
||||
from .organization import (
|
||||
InvitationAcceptView,
|
||||
InvitationDeleteView,
|
||||
OrganizationCreateView,
|
||||
OrganizationDashboardView,
|
||||
OrganizationUpdateView,
|
||||
|
|
@ -27,6 +28,7 @@ from .support import SupportView
|
|||
__all__ = [
|
||||
"IndexView",
|
||||
"InvitationAcceptView",
|
||||
"InvitationDeleteView",
|
||||
"LogoutView",
|
||||
"OrganizationCreateView",
|
||||
"OrganizationDashboardView",
|
||||
|
|
|
|||
|
|
@ -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, DetailView, TemplateView
|
||||
from django.views.generic import CreateView, DeleteView, DetailView, TemplateView
|
||||
from django_scopes import scopes_disabled
|
||||
from rules.contrib.views import AutoPermissionRequiredMixin
|
||||
|
||||
|
|
@ -14,7 +14,6 @@ from servala.core.models import (
|
|||
Organization,
|
||||
OrganizationInvitation,
|
||||
OrganizationMembership,
|
||||
OrganizationRole,
|
||||
ServiceInstance,
|
||||
)
|
||||
from servala.frontend.forms.organization import (
|
||||
|
|
@ -22,7 +21,11 @@ from servala.frontend.forms.organization import (
|
|||
OrganizationForm,
|
||||
OrganizationInvitationForm,
|
||||
)
|
||||
from servala.frontend.views.mixins import HtmxUpdateView, OrganizationViewMixin
|
||||
from servala.frontend.views.mixins import (
|
||||
HtmxUpdateView,
|
||||
HtmxViewMixin,
|
||||
OrganizationViewMixin,
|
||||
)
|
||||
|
||||
|
||||
class OrganizationCreateView(AutoPermissionRequiredMixin, CreateView):
|
||||
|
|
@ -108,10 +111,8 @@ class OrganizationDashboardView(
|
|||
return context
|
||||
|
||||
|
||||
class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
|
||||
class OrganizationMembershipMixin:
|
||||
template_name = "frontend/organizations/update.html"
|
||||
form_class = OrganizationForm
|
||||
fragments = ("org-name", "org-name-edit", "members-list")
|
||||
|
||||
@cached_property
|
||||
def user_role(self):
|
||||
|
|
@ -126,10 +127,9 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
|
|||
|
||||
@cached_property
|
||||
def can_manage_members(self):
|
||||
return self.user_role in [
|
||||
OrganizationRole.ADMIN,
|
||||
OrganizationRole.OWNER,
|
||||
]
|
||||
return self.request.user.has_perm(
|
||||
"core.change_organization", self.request.organization
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
|
@ -159,6 +159,18 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
|
|||
|
||||
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)
|
||||
|
|
@ -202,6 +214,9 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
|
|||
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())
|
||||
|
||||
def get_success_url(self):
|
||||
|
|
@ -260,3 +275,61 @@ 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())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue