October feature list #226
5 changed files with 119 additions and 40 deletions
|
|
@ -12,7 +12,13 @@ from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from servala.api.permissions import OSBBasicAuthPermission
|
from servala.api.permissions import OSBBasicAuthPermission
|
||||||
from servala.core.exoscale import get_exoscale_origin
|
from servala.core.exoscale import get_exoscale_origin
|
||||||
from servala.core.models import BillingEntity, Organization, User
|
from servala.core.models import (
|
||||||
|
BillingEntity,
|
||||||
|
Organization,
|
||||||
|
OrganizationInvitation,
|
||||||
|
OrganizationRole,
|
||||||
|
User,
|
||||||
|
)
|
||||||
from servala.core.models.service import Service, ServiceOffering
|
from servala.core.models.service import Service, ServiceOffering
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -127,8 +133,13 @@ class OSBServiceInstanceView(OSBBasicAuthPermission, View):
|
||||||
origin=exoscale_origin,
|
origin=exoscale_origin,
|
||||||
osb_guid=organization_guid,
|
osb_guid=organization_guid,
|
||||||
)
|
)
|
||||||
organization = Organization.create_organization(organization, user)
|
organization = Organization.create_organization(organization)
|
||||||
self._send_invitation_email(request, organization, user)
|
invitation = OrganizationInvitation.objects.create(
|
||||||
|
organization=organization,
|
||||||
|
email=user.email.lower(),
|
||||||
|
role=OrganizationRole.OWNER,
|
||||||
|
)
|
||||||
|
invitation.send_invitation_email(request)
|
||||||
except Exception:
|
except Exception:
|
||||||
return JsonResponse({"error": "Internal server error"}, status=500)
|
return JsonResponse({"error": "Internal server error"}, status=500)
|
||||||
|
|
||||||
|
|
@ -138,28 +149,6 @@ class OSBServiceInstanceView(OSBBasicAuthPermission, View):
|
||||||
)
|
)
|
||||||
return JsonResponse({"message": "Successfully enabled service"}, status=201)
|
return JsonResponse({"message": "Successfully enabled service"}, status=201)
|
||||||
|
|
||||||
def _send_invitation_email(self, request, organization, user):
|
|
||||||
subject = f"Welcome to Servala - {organization.name}"
|
|
||||||
url = request.build_absolute_uri(organization.urls.base)
|
|
||||||
message = f"""Hello {user.first_name or user.email},
|
|
||||||
|
|
||||||
You have been invited to join the organization "{organization.name}" on Servala Portal.
|
|
||||||
|
|
||||||
You can access your organization at: {url}
|
|
||||||
|
|
||||||
Please use this email address ({user.email}) when prompted to log in.
|
|
||||||
|
|
||||||
Best regards,
|
|
||||||
The Servala Team"""
|
|
||||||
|
|
||||||
send_mail(
|
|
||||||
subject=subject,
|
|
||||||
message=message,
|
|
||||||
from_email=settings.EMAIL_DEFAULT_FROM,
|
|
||||||
recipient_list=[user.email],
|
|
||||||
fail_silently=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _send_service_welcome_email(
|
def _send_service_welcome_email(
|
||||||
self, request, organization, user, service, service_offering
|
self, request, organization, user, service, service_offering
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ class OrganizationInvitationAdmin(admin.ModelAdmin):
|
||||||
"updated_at",
|
"updated_at",
|
||||||
)
|
)
|
||||||
date_hierarchy = "created_at"
|
date_hierarchy = "created_at"
|
||||||
|
actions = ["send_invitation_emails"]
|
||||||
|
|
||||||
def is_accepted(self, obj):
|
def is_accepted(self, obj):
|
||||||
return obj.is_accepted
|
return obj.is_accepted
|
||||||
|
|
@ -127,6 +128,35 @@ class OrganizationInvitationAdmin(admin.ModelAdmin):
|
||||||
is_accepted.boolean = True
|
is_accepted.boolean = True
|
||||||
is_accepted.short_description = _("Accepted")
|
is_accepted.short_description = _("Accepted")
|
||||||
|
|
||||||
|
def send_invitation_emails(self, request, queryset):
|
||||||
|
pending_invitations = queryset.filter(accepted_by__isnull=True)
|
||||||
|
sent_count = 0
|
||||||
|
failed_count = 0
|
||||||
|
|
||||||
|
for invitation in pending_invitations:
|
||||||
|
try:
|
||||||
|
invitation.send_invitation_email(request)
|
||||||
|
sent_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
failed_count += 1
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
_(f"Failed to send invitation to {invitation.email}: {str(e)}"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if sent_count > 0:
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_(f"Successfully sent {sent_count} invitation email(s)."),
|
||||||
|
)
|
||||||
|
|
||||||
|
if failed_count > 0:
|
||||||
|
messages.warning(
|
||||||
|
request, _(f"Failed to send {failed_count} invitation email(s).")
|
||||||
|
)
|
||||||
|
|
||||||
|
send_invitation_emails.short_description = _("Send invitation emails")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ServiceCategory)
|
@admin.register(ServiceCategory)
|
||||||
class ServiceCategoryAdmin(admin.ModelAdmin):
|
class ServiceCategoryAdmin(admin.ModelAdmin):
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ import secrets
|
||||||
import rules
|
import rules
|
||||||
import urlman
|
import urlman
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
|
from django.core.mail import send_mail
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
|
from django.http import HttpRequest
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
@ -112,7 +115,7 @@ class Organization(ServalaModelMixin, models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create_organization(cls, instance, owner):
|
def create_organization(cls, instance, owner=None):
|
||||||
try:
|
try:
|
||||||
instance.origin
|
instance.origin
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -120,6 +123,7 @@ class Organization(ServalaModelMixin, models.Model):
|
||||||
pk=settings.SERVALA_DEFAULT_ORIGIN
|
pk=settings.SERVALA_DEFAULT_ORIGIN
|
||||||
)
|
)
|
||||||
instance.save()
|
instance.save()
|
||||||
|
if owner:
|
||||||
instance.set_owner(owner)
|
instance.set_owner(owner)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -486,3 +490,49 @@ class OrganizationInvitation(ServalaModelMixin, models.Model):
|
||||||
@property
|
@property
|
||||||
def can_be_accepted(self):
|
def can_be_accepted(self):
|
||||||
return not self.is_accepted
|
return not self.is_accepted
|
||||||
|
|
||||||
|
def send_invitation_email(self, request=None):
|
||||||
|
subject = _("You're invited to join {organization} on Servala").format(
|
||||||
|
organization=self.organization.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if request:
|
||||||
|
invitation_url = request.build_absolute_uri(self.urls.accept)
|
||||||
|
organization_url = request.build_absolute_uri(self.organization.urls.base)
|
||||||
|
else:
|
||||||
|
fake_request = HttpRequest()
|
||||||
|
fake_request.META["SERVER_NAME"] = get_current_site(None).domain
|
||||||
|
fake_request.META["SERVER_PORT"] = "443"
|
||||||
|
fake_request.META["wsgi.url_scheme"] = "https"
|
||||||
|
invitation_url = fake_request.build_absolute_uri(self.urls.accept)
|
||||||
|
organization_url = fake_request.build_absolute_uri(
|
||||||
|
self.organization.urls.base
|
||||||
|
)
|
||||||
|
|
||||||
|
message = _(
|
||||||
|
"""Hello,
|
||||||
|
|
||||||
|
You have been invited to join the organization "{organization}" on Servala Portal as a {role}.
|
||||||
|
|
||||||
|
To accept this invitation, please click the link below:
|
||||||
|
{invitation_url}
|
||||||
|
|
||||||
|
Once you accept, you'll be able to access the organization at:
|
||||||
|
{organization_url}
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
The Servala Team"""
|
||||||
|
).format(
|
||||||
|
organization=self.organization.name,
|
||||||
|
role=self.get_role_display(),
|
||||||
|
invitation_url=invitation_url,
|
||||||
|
organization_url=organization_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
send_mail(
|
||||||
|
subject=subject,
|
||||||
|
message=message,
|
||||||
|
from_email=settings.EMAIL_DEFAULT_FROM,
|
||||||
|
recipient_list=[self.email],
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -179,9 +179,20 @@ class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
|
||||||
invitation.created_by = request.user
|
invitation.created_by = request.user
|
||||||
invitation.save()
|
invitation.save()
|
||||||
|
|
||||||
|
try:
|
||||||
|
invitation.send_invitation_email(request)
|
||||||
messages.success(
|
messages.success(
|
||||||
request,
|
request,
|
||||||
_("Invitation sent to {email}. Share this link: {url}").format(
|
_(
|
||||||
|
"Invitation sent to {email}. They will receive an email with the invitation link."
|
||||||
|
).format(email=invitation.email),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
_(
|
||||||
|
"Invitation created for {email}, but email failed to send. Share this link manually: {url}"
|
||||||
|
).format(
|
||||||
email=invitation.email,
|
email=invitation.email,
|
||||||
url=request.build_absolute_uri(invitation.urls.accept),
|
url=request.build_absolute_uri(invitation.urls.accept),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -73,12 +73,8 @@ def test_successful_onboarding_new_organization(
|
||||||
assert org.origin == exoscale_origin
|
assert org.origin == exoscale_origin
|
||||||
assert org.namespace.startswith("org-")
|
assert org.namespace.startswith("org-")
|
||||||
|
|
||||||
user = User.objects.get(email="test@example.com")
|
|
||||||
assert user.first_name == "Test"
|
|
||||||
assert user.last_name == "User"
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
membership = org.memberships.get(user=user)
|
assert org.invitations.all().filter(email="test@example.com").exists()
|
||||||
assert membership.role == "owner"
|
|
||||||
|
|
||||||
billing_entity = org.billing_entity
|
billing_entity = org.billing_entity
|
||||||
assert billing_entity.name == "Test Organization Display (Exoscale)"
|
assert billing_entity.name == "Test Organization Display (Exoscale)"
|
||||||
|
|
@ -91,7 +87,10 @@ def test_successful_onboarding_new_organization(
|
||||||
|
|
||||||
assert len(mail.outbox) == 2
|
assert len(mail.outbox) == 2
|
||||||
invitation_email = mail.outbox[0]
|
invitation_email = mail.outbox[0]
|
||||||
assert invitation_email.subject == "Welcome to Servala - Test Organization Display"
|
assert (
|
||||||
|
invitation_email.subject
|
||||||
|
== "You're invited to join Test Organization Display on Servala"
|
||||||
|
)
|
||||||
assert "test@example.com" in invitation_email.to
|
assert "test@example.com" in invitation_email.to
|
||||||
|
|
||||||
welcome_email = mail.outbox[1]
|
welcome_email = mail.outbox[1]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue