-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#2416: portfolio admin notification emails [BOB] #3441
Changes from 20 commits
b4c697f
37f52aa
1bd73b6
04b375f
f62dd76
4ce9eff
f786d13
b1f528b
5996c4d
041df21
6671545
1e280ed
be0c55d
c47122a
ff9c402
d29205f
a089dbc
f405ae8
2ab3b04
c1c4ec0
cf51677
05aca45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,11 @@ | |
from django_fsm import get_available_FIELD_transitions, FSMField | ||
from registrar.models import DomainInformation, Portfolio, UserPortfolioPermission, DomainInvitation | ||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices | ||
from registrar.utility.email_invitations import send_domain_invitation_email, send_portfolio_invitation_email | ||
from registrar.utility.email_invitations import ( | ||
send_domain_invitation_email, | ||
send_portfolio_admin_addition_emails, | ||
send_portfolio_invitation_email, | ||
) | ||
from registrar.views.utility.invitation_helper import ( | ||
get_org_membership, | ||
get_requested_user, | ||
|
@@ -1551,7 +1555,9 @@ def save_model(self, request, obj, form, change): | |
and not member_of_this_org | ||
and not member_of_a_different_org | ||
): | ||
send_portfolio_invitation_email(email=requested_email, requestor=requestor, portfolio=domain_org) | ||
send_portfolio_invitation_email( | ||
email=requested_email, requestor=requestor, portfolio=domain_org, is_admin_invitation=False | ||
) | ||
portfolio_invitation, _ = PortfolioInvitation.objects.get_or_create( | ||
email=requested_email, | ||
portfolio=domain_org, | ||
|
@@ -1642,30 +1648,57 @@ def save_model(self, request, obj, form, change): | |
Emails sent to requested user / email. | ||
When exceptions are raised, return without saving model. | ||
""" | ||
if not change: # Only send email if this is a new PortfolioInvitation (creation) | ||
try: | ||
portfolio = obj.portfolio | ||
requested_email = obj.email | ||
requestor = request.user | ||
# Look up a user with that email | ||
requested_user = get_requested_user(requested_email) | ||
|
||
permission_exists = UserPortfolioPermission.objects.filter( | ||
user__email=requested_email, portfolio=portfolio, user__email__isnull=False | ||
).exists() | ||
try: | ||
is_admin_invitation = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in obj.roles | ||
if not change: # Only send email if this is a new PortfolioInvitation (creation) | ||
# Look up a user with that email | ||
requested_user = get_requested_user(requested_email) | ||
|
||
permission_exists = UserPortfolioPermission.objects.filter( | ||
user__email=requested_email, portfolio=portfolio, user__email__isnull=False | ||
).exists() | ||
if not permission_exists: | ||
# if permission does not exist for a user with requested_email, send email | ||
send_portfolio_invitation_email(email=requested_email, requestor=requestor, portfolio=portfolio) | ||
if not send_portfolio_invitation_email( | ||
email=requested_email, | ||
requestor=requestor, | ||
portfolio=portfolio, | ||
is_admin_invitation=is_admin_invitation, | ||
): | ||
messages.warning( | ||
self.request, "Could not send email notification to existing organization admins." | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could be missing something, but don't we need an else after this if-statement? Or does the function somehow terminate before setting the following success message? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, we don't need an else. In this case, we don't want to raise exceptions when email send attempts fail. But we want views to create messages. So, we are returning a pass/fail boolean from the send_email methods. If it fails, we want to raise a warning. Whether pass or fail, we want to continue after the method completes. |
||
# if user exists for email, immediately retrieve portfolio invitation upon creation | ||
if requested_user is not None: | ||
obj.retrieve() | ||
messages.success(request, f"{requested_email} has been invited.") | ||
else: | ||
messages.warning(request, "User is already a member of this portfolio.") | ||
except Exception as e: | ||
# when exception is raised, handle and do not save the model | ||
handle_invitation_exceptions(request, e, requested_email) | ||
return | ||
else: # Handle the case when updating an existing PortfolioInvitation | ||
# Retrieve the existing object from the database | ||
existing_obj = PortfolioInvitation.objects.get(pk=obj.pk) | ||
|
||
# Check if the previous roles did NOT include ORGANIZATION_ADMIN | ||
# and the new roles DO include ORGANIZATION_ADMIN | ||
was_not_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN not in existing_obj.roles | ||
# Check also if status is INVITED, ignore role changes for other statuses | ||
is_invited = obj.status == PortfolioInvitation.PortfolioInvitationStatus.INVITED | ||
|
||
if was_not_admin and is_admin_invitation and is_invited: | ||
# send email to existing portfolio admins if new admin | ||
if not send_portfolio_admin_addition_emails( | ||
email=requested_email, | ||
requestor=requestor, | ||
portfolio=portfolio, | ||
): | ||
messages.warning(request, "Could not send email notification to existing organization admins.") | ||
except Exception as e: | ||
# when exception is raised, handle and do not save the model | ||
handle_invitation_exceptions(request, e, requested_email) | ||
return | ||
# Call the parent save method to save the object | ||
super().save_model(request, obj, form, change) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
from registrar.utility import StrEnum | ||
from django.db import models | ||
from django.db.models import Q | ||
from django.apps import apps | ||
from django.forms import ValidationError | ||
from registrar.utility.waffle import flag_is_active_for_user | ||
|
@@ -136,9 +137,10 @@ def validate_user_portfolio_permission(user_portfolio_permission): | |
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios." | ||
) | ||
|
||
existing_invitations = PortfolioInvitation.objects.exclude( | ||
portfolio=user_portfolio_permission.portfolio | ||
).filter(email=user_portfolio_permission.user.email) | ||
existing_invitations = PortfolioInvitation.objects.filter(email=user_portfolio_permission.user.email).exclude( | ||
Q(portfolio=user_portfolio_permission.portfolio) | ||
| Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED) | ||
) | ||
Comment on lines
-135
to
+139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this fixes an undocumented bug where retrieved invitations were blocking creation of new invitations |
||
if existing_invitations.exists(): | ||
raise ValidationError( | ||
"This user is already assigned to a portfolio invitation. " | ||
|
@@ -195,8 +197,8 @@ def validate_portfolio_invitation(portfolio_invitation): | |
if not flag_is_active_for_user(user, "multiple_portfolios"): | ||
existing_permissions = UserPortfolioPermission.objects.filter(user=user) | ||
|
||
existing_invitations = PortfolioInvitation.objects.exclude(id=portfolio_invitation.id).filter( | ||
email=portfolio_invitation.email | ||
existing_invitations = PortfolioInvitation.objects.filter(email=portfolio_invitation.email).exclude( | ||
Q(id=portfolio_invitation.id) | Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED) | ||
Comment on lines
-194
to
+197
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this fixes an undocumented bug where retrieved invitations were blocking creation of new invitations |
||
) | ||
|
||
if existing_permissions.exists(): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} | ||
Hi,{% if portfolio_admin and portfolio_admin.first_name %} {{ portfolio_admin.first_name }}.{% endif %} | ||
|
||
An admin was invited to your .gov organization. | ||
|
||
ORGANIZATION: {{ portfolio.organization_name }} | ||
INVITED BY: {{ requestor_email }} | ||
INVITED ON: {{date}} | ||
ADMIN INVITED: {{ invited_email_address }} | ||
|
||
---------------------------------------------------------------- | ||
|
||
NEXT STEPS | ||
The person who received the invitation will become an admin once they log in to the | ||
.gov registrar. They'll need to access the registrar using a Login.gov account that's | ||
associated with the invited email address. | ||
|
||
If you need to cancel this invitation or remove the admin, you can do that by going to | ||
the Members section for your organization <https://manage.get.gov/>. | ||
|
||
|
||
WHY DID YOU RECEIVE THIS EMAIL? | ||
You’re listed as an admin for {{ portfolio.organization_name }}. That means you'll receive a notification | ||
whenever a new admin is invited to that organization. | ||
|
||
If you have questions or concerns, reach out to the person who sent the invitation or reply to this email. | ||
|
||
|
||
THANK YOU | ||
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain. | ||
|
||
---------------------------------------------------------------- | ||
|
||
The .gov team | ||
Contact us: <https://get.gov/contact/> | ||
Learn about .gov <https://get.gov> | ||
|
||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency | ||
(CISA) <https://cisa.gov/> | ||
{% endautoescape %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
An admin was invited to your .gov organization |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} | ||
Hi,{% if portfolio_admin and portfolio_admin.first_name %} {{ portfolio_admin.first_name }}.{% endif %} | ||
|
||
An admin was removed from your .gov organization. | ||
|
||
ORGANIZATION: {{ portfolio.organization_name }} | ||
REMOVED BY: {{ requestor_email }} | ||
REMOVED ON: {{date}} | ||
ADMIN REMOVED: {{ removed_email_address }} | ||
|
||
You can view this update by going to the Members section for your .gov organization <https://manage.get.gov/>. | ||
|
||
---------------------------------------------------------------- | ||
|
||
WHY DID YOU RECEIVE THIS EMAIL? | ||
You’re listed as an admin for {{ portfolio.organization_name }}. That means you'll receive a notification | ||
whenever an admin is removed from that organization. | ||
|
||
If you have questions or concerns, reach out to the person who removed the admin or reply to this email. | ||
|
||
|
||
THANK YOU | ||
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain. | ||
|
||
---------------------------------------------------------------- | ||
|
||
The .gov team | ||
Contact us: <https://get.gov/contact/> | ||
Learn about .gov <https://get.gov> | ||
|
||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency | ||
(CISA) <https://cisa.gov/> | ||
{% endautoescape %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
An admin was removed from your .gov organization |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the way you moved the try statement up