mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-15 14:04:10 +02:00
first refactor attempt, moving send_domain_invitation_email to a helper
This commit is contained in:
parent
d766aa9b2a
commit
d2d44bfea1
4 changed files with 150 additions and 120 deletions
|
@ -7,6 +7,7 @@ from typing import Optional
|
||||||
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from registrar.models.host import Host
|
from registrar.models.host import Host
|
||||||
|
@ -1087,6 +1088,13 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
help_text="Record of the last change event for ds data",
|
help_text="Record of the last change event for ds data",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
"""
|
||||||
|
Returns the absolute URL for the domain instance.
|
||||||
|
This is the standard implementation for Django models.
|
||||||
|
"""
|
||||||
|
return reverse("domain", kwargs={"pk": self.pk})
|
||||||
|
|
||||||
def isActive(self):
|
def isActive(self):
|
||||||
return self.state == Domain.State.CREATED
|
return self.state == Domain.State.CREATED
|
||||||
|
|
||||||
|
|
97
src/registrar/utility/email_invitations.py
Normal file
97
src/registrar/utility/email_invitations.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from registrar.models import DomainInvitation
|
||||||
|
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||||
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
|
from registrar.utility.errors import (
|
||||||
|
MissingEmailError,
|
||||||
|
AlreadyManagerError,
|
||||||
|
AlreadyInvitedError,
|
||||||
|
OutsideOrgMemberError,
|
||||||
|
)
|
||||||
|
from registrar.utility.waffle import flag_is_active_for_user
|
||||||
|
from registrar.utility.email import send_templated_email, EmailSendingError
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_member_of_different_org(email, requestor, requested_user):
|
||||||
|
"""Verifies if an email belongs to a different organization as a member or invited member."""
|
||||||
|
# Check if requested_user is a already member of a different organization than the requestor's org
|
||||||
|
requestor_org = UserPortfolioPermission.objects.filter(user=requestor).first().portfolio
|
||||||
|
existing_org_permission = UserPortfolioPermission.objects.filter(user=requested_user).first()
|
||||||
|
existing_org_invitation = PortfolioInvitation.objects.filter(email=email).first()
|
||||||
|
|
||||||
|
return (existing_org_permission and existing_org_permission.portfolio != requestor_org) or (
|
||||||
|
existing_org_invitation and existing_org_invitation.portfolio != requestor_org
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_domain_invitation_email(email: str, requestor, domain, requested_user=None):
|
||||||
|
"""
|
||||||
|
Sends a domain invitation email to the specified address.
|
||||||
|
|
||||||
|
Raises exceptions for validation or email-sending issues.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
email (str): Email address of the recipient.
|
||||||
|
requestor (User): The user initiating the invitation.
|
||||||
|
domain (Domain): The domain object for which the invitation is being sent.
|
||||||
|
requested_user (User): The user of the recipient, if exists; defaults to None
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
MissingEmailError: If the requestor has no email associated with their account.
|
||||||
|
AlreadyManagerError: If the email corresponds to an existing domain manager.
|
||||||
|
AlreadyInvitedError: If an invitation has already been sent.
|
||||||
|
OutsideOrgMemberError: If the requested_user is part of a different organization.
|
||||||
|
EmailSendingError: If there is an error while sending the email.
|
||||||
|
"""
|
||||||
|
# Default email address for staff
|
||||||
|
requestor_email = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
|
# Check if the requestor is staff and has an email
|
||||||
|
if not requestor.is_staff:
|
||||||
|
if not requestor.email or requestor.email.strip() == "":
|
||||||
|
raise MissingEmailError(requestor.username)
|
||||||
|
else:
|
||||||
|
requestor_email = requestor.email
|
||||||
|
|
||||||
|
# Check if the recipient is part of a different organization
|
||||||
|
if flag_is_active_for_user(requestor, "organization_feature") and _is_member_of_different_org(
|
||||||
|
email, requestor, requested_user
|
||||||
|
):
|
||||||
|
raise OutsideOrgMemberError
|
||||||
|
|
||||||
|
# Check for an existing invitation
|
||||||
|
try:
|
||||||
|
invite = DomainInvitation.objects.get(email=email, domain=domain)
|
||||||
|
if invite.status == DomainInvitation.DomainInvitationStatus.RETRIEVED:
|
||||||
|
raise AlreadyManagerError(email)
|
||||||
|
elif invite.status == DomainInvitation.DomainInvitationStatus.CANCELED:
|
||||||
|
invite.update_cancellation_status()
|
||||||
|
invite.save()
|
||||||
|
else:
|
||||||
|
raise AlreadyInvitedError(email)
|
||||||
|
except DomainInvitation.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Send the email
|
||||||
|
try:
|
||||||
|
send_templated_email(
|
||||||
|
"emails/domain_invitation.txt",
|
||||||
|
"emails/domain_invitation_subject.txt",
|
||||||
|
to_address=email,
|
||||||
|
context={
|
||||||
|
"domain_url": domain.get_absolute_url(),
|
||||||
|
"domain": domain,
|
||||||
|
"requestor_email": requestor_email,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except EmailSendingError as exc:
|
||||||
|
logger.warning(
|
||||||
|
"Could not send email invitation to %s for domain %s",
|
||||||
|
email,
|
||||||
|
domain,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
raise EmailSendingError("Could not send email invitation.") from exc
|
|
@ -23,6 +23,34 @@ class InvalidDomainError(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvitationError(Exception):
|
||||||
|
"""Base exception for invitation-related errors."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AlreadyManagerError(InvitationError):
|
||||||
|
"""Raised when the user is already a manager for the domain."""
|
||||||
|
|
||||||
|
def __init__(self, email):
|
||||||
|
super().__init__(f"{email} is already a manager for this domain.")
|
||||||
|
|
||||||
|
|
||||||
|
class AlreadyInvitedError(InvitationError):
|
||||||
|
"""Raised when the user has already been invited to the domain."""
|
||||||
|
|
||||||
|
def __init__(self, email):
|
||||||
|
super().__init__(f"{email} has already been invited to this domain.")
|
||||||
|
|
||||||
|
|
||||||
|
class MissingEmailError(InvitationError):
|
||||||
|
"""Raised when the requestor has no email associated with their account."""
|
||||||
|
|
||||||
|
def __init__(self, username):
|
||||||
|
super().__init__(f"Can't send invitation email. No email is associated with the account for '{username}'.")
|
||||||
|
self.username = username
|
||||||
|
|
||||||
|
|
||||||
class OutsideOrgMemberError(ValueError):
|
class OutsideOrgMemberError(ValueError):
|
||||||
"""
|
"""
|
||||||
Error raised when an org member tries adding a user from a different .gov org.
|
Error raised when an org member tries adding a user from a different .gov org.
|
||||||
|
|
|
@ -25,14 +25,16 @@ from registrar.models import (
|
||||||
PortfolioInvitation,
|
PortfolioInvitation,
|
||||||
User,
|
User,
|
||||||
UserDomainRole,
|
UserDomainRole,
|
||||||
UserPortfolioPermission,
|
|
||||||
PublicContact,
|
PublicContact,
|
||||||
)
|
)
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||||
from registrar.utility.enums import DefaultEmail
|
from registrar.utility.enums import DefaultEmail
|
||||||
from registrar.utility.errors import (
|
from registrar.utility.errors import (
|
||||||
|
AlreadyInvitedError,
|
||||||
|
AlreadyManagerError,
|
||||||
GenericError,
|
GenericError,
|
||||||
GenericErrorCodes,
|
GenericErrorCodes,
|
||||||
|
MissingEmailError,
|
||||||
NameserverError,
|
NameserverError,
|
||||||
NameserverErrorCodes as nsErrorCodes,
|
NameserverErrorCodes as nsErrorCodes,
|
||||||
DsDataError,
|
DsDataError,
|
||||||
|
@ -63,40 +65,13 @@ from epplibwrapper import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..utility.email import send_templated_email, EmailSendingError
|
from ..utility.email import send_templated_email, EmailSendingError
|
||||||
|
from ..utility.email_invitations import send_domain_invitation_email
|
||||||
from .utility import DomainPermissionView, DomainInvitationPermissionCancelView
|
from .utility import DomainPermissionView, DomainInvitationPermissionCancelView
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class InvitationError(Exception):
|
|
||||||
"""Base exception for invitation-related errors."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AlreadyManagerError(InvitationError):
|
|
||||||
"""Raised when the user is already a manager for the domain."""
|
|
||||||
|
|
||||||
def __init__(self, email):
|
|
||||||
super().__init__(f"{email} is already a manager for this domain.")
|
|
||||||
|
|
||||||
|
|
||||||
class AlreadyInvitedError(InvitationError):
|
|
||||||
"""Raised when the user has already been invited to the domain."""
|
|
||||||
|
|
||||||
def __init__(self, email):
|
|
||||||
super().__init__(f"{email} has already been invited to this domain.")
|
|
||||||
|
|
||||||
|
|
||||||
class MissingEmailError(InvitationError):
|
|
||||||
"""Raised when the requestor has no email associated with their account."""
|
|
||||||
|
|
||||||
def __init__(self, username):
|
|
||||||
super().__init__(f"Can't send invitation email. No email is associated with the account for '{username}'.")
|
|
||||||
self.username = username
|
|
||||||
|
|
||||||
|
|
||||||
class DomainBaseView(DomainPermissionView):
|
class DomainBaseView(DomainPermissionView):
|
||||||
"""
|
"""
|
||||||
Base View for the Domain. Handles getting and setting the domain
|
Base View for the Domain. Handles getting and setting the domain
|
||||||
|
@ -1211,93 +1186,8 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse("domain-users", kwargs={"pk": self.object.pk})
|
return reverse("domain-users", kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
def _domain_abs_url(self):
|
|
||||||
"""Get an absolute URL for this domain."""
|
|
||||||
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
|
|
||||||
|
|
||||||
def _is_member_of_different_org(self, email, requestor, requested_user):
|
|
||||||
"""Verifies if an email belongs to a different organization as a member or invited member."""
|
|
||||||
# Check if user is a already member of a different organization than the requestor's org
|
|
||||||
requestor_org = UserPortfolioPermission.objects.filter(user=requestor).first().portfolio
|
|
||||||
existing_org_permission = UserPortfolioPermission.objects.filter(user=requested_user).first()
|
|
||||||
existing_org_invitation = PortfolioInvitation.objects.filter(email=email).first()
|
|
||||||
|
|
||||||
return (existing_org_permission and existing_org_permission.portfolio != requestor_org) or (
|
|
||||||
existing_org_invitation and existing_org_invitation.portfolio != requestor_org
|
|
||||||
)
|
|
||||||
|
|
||||||
def _check_existing_invitation_status(self, email, domain):
|
|
||||||
"""Check if existing invitation exists; checks its status for canceled, invited or retrieved, and gives the appropiate response.
|
|
||||||
|
|
||||||
Exceptions raised for RETRIEVED or INVITED. Existing CANCELED invitations updated to INVITED.
|
|
||||||
Existing CANCELED invitations do not raise an exception."""
|
|
||||||
try:
|
|
||||||
invite = DomainInvitation.objects.get(email=email, domain=domain)
|
|
||||||
except Exception as err:
|
|
||||||
# No existing invitation, do nothing
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
if invite.status == DomainInvitation.DomainInvitationStatus.RETRIEVED:
|
|
||||||
raise AlreadyManagerError(email)
|
|
||||||
elif invite.status == DomainInvitation.DomainInvitationStatus.CANCELED:
|
|
||||||
invite.update_cancellation_status()
|
|
||||||
invite.save()
|
|
||||||
else:
|
|
||||||
# Status is INVITED
|
|
||||||
raise AlreadyInvitedError(email)
|
|
||||||
|
|
||||||
def _send_domain_invitation_email(self, email: str, requestor: User, requested_user=None):
|
|
||||||
"""Performs the sending of the domain invitation email,
|
|
||||||
does not make a domain information object
|
|
||||||
email: string- email to send to
|
|
||||||
|
|
||||||
raises EmailSendingError
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Set a default email address to send to for staff
|
|
||||||
requestor_email = settings.DEFAULT_FROM_EMAIL
|
|
||||||
|
|
||||||
# refactor below to raise an error
|
|
||||||
# if the user is not a staff member
|
|
||||||
if not requestor.is_staff:
|
|
||||||
if not requestor.email or requestor.email.strip() == "":
|
|
||||||
raise MissingEmailError(requestor.username)
|
|
||||||
else:
|
|
||||||
requestor_email = requestor.email
|
|
||||||
|
|
||||||
# Check is user is a member or invited member of a different org from this domain's org
|
|
||||||
if flag_is_active_for_user(requestor, "organization_feature") and self._is_member_of_different_org(
|
|
||||||
email, requestor, requested_user
|
|
||||||
):
|
|
||||||
raise OutsideOrgMemberError
|
|
||||||
|
|
||||||
# Check to see if an invite has already been sent
|
|
||||||
self._check_existing_invitation_status(email=email, domain=self.object)
|
|
||||||
|
|
||||||
try:
|
|
||||||
send_templated_email(
|
|
||||||
"emails/domain_invitation.txt",
|
|
||||||
"emails/domain_invitation_subject.txt",
|
|
||||||
to_address=email,
|
|
||||||
context={
|
|
||||||
"domain_url": self._domain_abs_url(),
|
|
||||||
"domain": self.object,
|
|
||||||
"requestor_email": requestor_email,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except EmailSendingError as exc:
|
|
||||||
logger.warn(
|
|
||||||
"Could not sent email invitation to %s for domain %s",
|
|
||||||
email,
|
|
||||||
self.object,
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
logger.info(exc)
|
|
||||||
raise EmailSendingError("Could not send email invitation.") from exc
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""Add the specified user on this domain.
|
"""Add the specified user on this domain."""
|
||||||
Throws EmailSendingError."""
|
|
||||||
requested_email = form.cleaned_data["email"]
|
requested_email = form.cleaned_data["email"]
|
||||||
requestor = self.request.user
|
requestor = self.request.user
|
||||||
|
|
||||||
|
@ -1311,13 +1201,20 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
try:
|
try:
|
||||||
if requested_user is None:
|
if requested_user is None:
|
||||||
# no user exists, send an email and make an invitation
|
# no user exists, send an email and make an invitation
|
||||||
self._send_domain_invitation_email(email=requested_email, requestor=requestor)
|
send_domain_invitation_email(
|
||||||
|
email=requested_email,
|
||||||
|
requestor=requestor,
|
||||||
|
domain=self.object,
|
||||||
|
)
|
||||||
DomainInvitation.objects.get_or_create(email=requested_email, domain=self.object)
|
DomainInvitation.objects.get_or_create(email=requested_email, domain=self.object)
|
||||||
messages.success(self.request, f"{requested_email} has been invited to this domain.")
|
messages.success(self.request, f"{requested_email} has been invited to this domain.")
|
||||||
else:
|
else:
|
||||||
# user exists, send email and make user domain role
|
# user exists, send email and make user domain role
|
||||||
self._send_domain_invitation_email(
|
send_domain_invitation_email(
|
||||||
email=requested_email, requestor=requestor, requested_user=requested_user
|
email=requested_email,
|
||||||
|
requestor=requestor,
|
||||||
|
requested_user=requested_user,
|
||||||
|
domain=self.object,
|
||||||
)
|
)
|
||||||
UserDomainRole.objects.create(
|
UserDomainRole.objects.create(
|
||||||
user=requested_user,
|
user=requested_user,
|
||||||
|
@ -1353,6 +1250,8 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
f"No email exists for the requestor '{e.username}'.",
|
f"No email exists for the requestor '{e.username}'.",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
except IntegrityError:
|
||||||
|
messages.warning(self.request, f"{requested_email} is already a manager for this domain")
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Could not send email invitation (Other Exception)",
|
"Could not send email invitation (Other Exception)",
|
||||||
|
@ -1360,8 +1259,6 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
messages.warning(self.request, "Could not send email invitation.")
|
messages.warning(self.request, "Could not send email invitation.")
|
||||||
except IntegrityError:
|
|
||||||
messages.warning(self.request, f"{requested_email} is already a manager for this domain")
|
|
||||||
|
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue