Allow users to accept invitations

ref #19
This commit is contained in:
Tobias Kunze 2025-10-08 11:24:44 +02:00
parent 8b1e0f74bb
commit 09ab83d1e4
4 changed files with 111 additions and 2 deletions

View file

@ -0,0 +1,42 @@
{% extends "frontend/base.html" %}
{% load i18n %}
{% block html_title %}
{% block page_title %}
{% translate "Accept Organization Invitation" %}
{% endblock page_title %}
{% endblock html_title %}
{% block content %}
<section class="section">
<div class="card">
<div class="card-content">
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
{% blocktranslate with org_name=invitation.organization.name role=invitation.get_role_display %}
You have been invited to join <strong>{{ org_name }}</strong> as a <strong>{{ role }}</strong>.
{% endblocktranslate %}
</div>
{% if user.email|lower != invitation.email|lower %}
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i>
{% blocktranslate with invitation_email=invitation.email user_email=user.email %}
<strong>Note:</strong> This invitation was sent to <strong>{{ invitation_email }}</strong>,
but you are currently logged in as <strong>{{ user_email }}</strong>.
{% endblocktranslate %}
</div>
{% endif %}
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-end gap-2">
<a href="{% url 'frontend:organization.selection' %}"
class="btn btn-secondary">{% translate "Cancel" %}</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> {% translate "Accept Invitation" %}
</button>
</div>
</form>
</div>
</div>
</div>
</section>
{% endblock content %}

View file

@ -6,6 +6,11 @@ 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(
"invitations/<str:secret>/accept/",
views.InvitationAcceptView.as_view(),
name="invitation.accept",
),
path(
"organizations/",
views.OrganizationSelectionView.as_view(),

View file

@ -8,6 +8,7 @@ from .generic import (
custom_500,
)
from .organization import (
InvitationAcceptView,
OrganizationCreateView,
OrganizationDashboardView,
OrganizationUpdateView,
@ -25,6 +26,7 @@ from .support import SupportView
__all__ = [
"IndexView",
"InvitationAcceptView",
"LogoutView",
"OrganizationCreateView",
"OrganizationDashboardView",

View file

@ -1,11 +1,17 @@
from django.shortcuts import redirect
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView
from django.views.generic import CreateView, DetailView, TemplateView
from django_scopes import scopes_disabled
from rules.contrib.views import AutoPermissionRequiredMixin
from servala.core.models import (
BillingEntity,
Organization,
OrganizationInvitation,
OrganizationMembership,
ServiceInstance,
)
@ -103,3 +109,57 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
def get_success_url(self):
return self.request.path
@method_decorator(scopes_disabled(), name="dispatch")
class InvitationAcceptView(TemplateView):
template_name = "frontend/organizations/invitation_accept.html"
def get_invitation(self):
secret = self.kwargs.get("secret")
return get_object_or_404(OrganizationInvitation, secret=secret)
def dispatch(self, request, *args, **kwargs):
invitation = self.get_invitation()
if invitation.is_accepted:
messages.warning(
request,
_("This invitation has already been accepted."),
)
return redirect("frontend:organization.selection")
if not request.user.is_authenticated:
request.session["invitation_next"] = request.path
messages.info(
request,
_("Please log in or sign up to accept this invitation."),
)
return redirect(f"{reverse('account_login')}?next={request.path}")
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["invitation"] = self.get_invitation()
return context
def post(self, request, *args, **kwargs):
invitation = self.get_invitation()
invitation.accepted_by = request.user
invitation.accepted_at = timezone.now()
invitation.save()
OrganizationMembership.objects.get_or_create(
user=request.user,
organization=invitation.organization,
defaults={"role": invitation.role},
)
messages.success(
request,
_("You have successfully joined {organization}!").format(
organization=invitation.organization.name
),
)
request.session.pop("invitation_next", None)
return redirect(invitation.organization.urls.base)