Merge branch 'main' into nl/3390-remove-portfolio-member-roles-column

This commit is contained in:
CuriousX 2025-02-14 12:45:36 -07:00 committed by GitHub
commit 1cd55cd5be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 351 additions and 85 deletions

View file

@ -1,8 +1,8 @@
# This workflow can be run from the CLI
# gh workflow run reset-db.yaml -f environment=ENVIRONMENT
name: Reset database
run-name: Reset database for ${{ github.event.inputs.environment }}
name: Delete and Recreate database
run-name: Delete and Recreate for ${{ github.event.inputs.environment }}
on:
workflow_dispatch:
@ -53,7 +53,7 @@ jobs:
sudo apt-get update
sudo apt-get install cf8-cli
cf api api.fr.cloud.gov
cf auth "$CF_USERNAME" "$CF_PASSWORD"
cf auth "$cf_username" "$cf_password"
cf target -o cisa-dotgov -s $DESTINATION_ENVIRONMENT

View file

@ -11,6 +11,7 @@ from django.db.models import (
Value,
When,
)
from django.db.models.functions import Concat, Coalesce
from django.http import HttpResponseRedirect
from registrar.models.federal_agency import FederalAgency
@ -24,7 +25,7 @@ from registrar.utility.admin_helpers import (
from django.conf import settings
from django.contrib.messages import get_messages
from django.contrib.admin.helpers import AdminForm
from django.shortcuts import redirect
from django.shortcuts import redirect, get_object_or_404
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
@ -1533,6 +1534,27 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
def change_view(self, request, object_id, form_url="", extra_context=None):
"""Override the change_view to add the invitation obj for the change_form_object_tools template"""
if extra_context is None:
extra_context = {}
# Get the domain invitation object
invitation = get_object_or_404(DomainInvitation, id=object_id)
extra_context["invitation"] = invitation
if request.method == "POST" and "cancel_invitation" in request.POST:
if invitation.status == DomainInvitation.DomainInvitationStatus.INVITED:
invitation.cancel_invitation()
invitation.save(update_fields=["status"])
messages.success(request, _("Invitation canceled successfully."))
# Redirect back to the change view
return redirect(reverse("admin:registrar_domaininvitation_change", args=[object_id]))
return super().change_view(request, object_id, form_url, extra_context)
def delete_view(self, request, object_id, extra_context=None):
"""
Custom delete_view to perform additional actions or customize the template.
@ -1551,6 +1573,7 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
which will be successful if a single User exists for that email; otherwise, will
just continue to create the invitation.
"""
if not change:
domain = obj.domain
domain_org = getattr(domain.domain_info, "portfolio", None)

View file

@ -1,3 +1,4 @@
/** An IIFE for admin in DjangoAdmin to listen to clicks on the growth report export button,
* attach the seleted start and end dates to a url that'll trigger the view, and finally
* redirect to that url.
@ -58,6 +59,51 @@
/** An IIFE to initialize the analytics page
*/
(function () {
/**
* Creates a diagonal stripe pattern for chart.js
* Inspired by https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns
* and https://github.com/ashiguruma/patternomaly
* @param {string} backgroundColor - Background color of the pattern
* @param {string} [lineColor="white"] - Color of the diagonal lines
* @param {boolean} [rightToLeft=false] - Direction of the diagonal lines
* @param {number} [lineGap=1] - Gap between lines
* @returns {CanvasPattern} A canvas pattern object for use with backgroundColor
*/
function createDiagonalPattern(backgroundColor, lineColor, rightToLeft=false, lineGap=1) {
// Define the canvas and the 2d context so we can draw on it
let shape = document.createElement("canvas");
shape.width = 20;
shape.height = 20;
let context = shape.getContext("2d");
// Fill with specified background color
context.fillStyle = backgroundColor;
context.fillRect(0, 0, shape.width, shape.height);
// Set stroke properties
context.strokeStyle = lineColor;
context.lineWidth = 2;
// Rotate canvas for a right-to-left pattern
if (rightToLeft) {
context.translate(shape.width, 0);
context.rotate(90 * Math.PI / 180);
};
// First diagonal line
let halfSize = shape.width / 2;
context.moveTo(halfSize - lineGap, -lineGap);
context.lineTo(shape.width + lineGap, halfSize + lineGap);
// Second diagonal line (x,y are swapped)
context.moveTo(-lineGap, halfSize - lineGap);
context.lineTo(halfSize + lineGap, shape.width + lineGap);
context.stroke();
return context.createPattern(shape, "repeat");
}
function createComparativeColumnChart(canvasId, title, labelOne, labelTwo) {
var canvas = document.getElementById(canvasId);
if (!canvas) {
@ -74,17 +120,20 @@
datasets: [
{
label: labelOne,
backgroundColor: "rgba(255, 99, 132, 0.2)",
backgroundColor: "rgba(255, 99, 132, 0.3)",
borderColor: "rgba(255, 99, 132, 1)",
borderWidth: 1,
data: listOne,
// Set this line style to be rightToLeft for visual distinction
backgroundColor: createDiagonalPattern('rgba(255, 99, 132, 0.3)', 'white', true)
},
{
label: labelTwo,
backgroundColor: "rgba(75, 192, 192, 0.2)",
backgroundColor: "rgba(75, 192, 192, 0.3)",
borderColor: "rgba(75, 192, 192, 1)",
borderWidth: 1,
data: listTwo,
backgroundColor: createDiagonalPattern('rgba(75, 192, 192, 0.3)', 'white')
},
],
};

View file

@ -498,6 +498,28 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too
font-size: 13px;
}
.object-tools li button {
font-family: Source Sans Pro Web, Helvetica Neue, Helvetica, Roboto, Arial, sans-serif;
text-transform: none !important;
font-size: 14px !important;
display: block;
float: left;
padding: 3px 12px;
background: var(--object-tools-bg) !important;
color: var(--object-tools-fg);
font-weight: 400;
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
border-radius: 15px;
cursor: pointer;
border: none;
line-height: 20px;
&:focus, &:hover{
background: var(--object-tools-hover-bg) !important;
}
}
.module--custom {
a {
font-size: 13px;

View file

@ -46,7 +46,6 @@ body {
background-color: color('gray-1');
}
.section-outlined {
background-color: color('white');
border: 1px solid color('base-lighter');

View file

@ -107,6 +107,7 @@ DEBUG = env_debug
# Controls production specific feature toggles
IS_PRODUCTION = env_is_production
SECRET_ENCRYPT_METADATA = secret_encrypt_metadata
BASE_URL = env_base_url
# Applications are modular pieces of code.
# They are provided by Django, by third-parties, or by yourself.

View file

@ -68,19 +68,9 @@ def portfolio_permissions(request):
"has_organization_requests_flag": False,
"has_organization_members_flag": False,
"is_portfolio_admin": False,
"has_domain_renewal_flag": False,
}
try:
portfolio = request.session.get("portfolio")
# These feature flags will display and doesn't depend on portfolio
portfolio_context.update(
{
"has_organization_feature_flag": True,
"has_domain_renewal_flag": request.user.has_domain_renewal_flag(),
}
)
if portfolio:
return {
"has_view_portfolio_permission": request.user.has_view_portfolio_permission(portfolio),
@ -95,7 +85,6 @@ def portfolio_permissions(request):
"has_organization_requests_flag": request.user.has_organization_requests_flag(),
"has_organization_members_flag": request.user.has_organization_members_flag(),
"is_portfolio_admin": request.user.is_portfolio_admin(portfolio),
"has_domain_renewal_flag": request.user.has_domain_renewal_flag(),
}
return portfolio_context

View file

@ -171,6 +171,13 @@ class UserFixture:
"email": "gina.summers@ecstech.com",
"title": "Scrum Master",
},
{
"username": "89f2db87-87a2-4778-a5ea-5b27b585b131",
"first_name": "Jaxon",
"last_name": "Silva",
"email": "jaxon.silva@cisa.dhs.gov",
"title": "Designer",
},
]
STAFF = [

View file

@ -41,7 +41,6 @@ from .utility.time_stamped_model import TimeStampedModel
from .public_contact import PublicContact
from .user_domain_role import UserDomainRole
from waffle.decorators import flag_is_active
logger = logging.getLogger(__name__)
@ -1172,7 +1171,7 @@ class Domain(TimeStampedModel, DomainHelper):
"""Return the display status of the domain."""
if self.is_expired() and (self.state != self.State.UNKNOWN):
return "Expired"
elif flag_is_active(request, "domain_renewal") and self.is_expiring():
elif self.is_expiring():
return "Expiring soon"
elif self.state == self.State.UNKNOWN or self.state == self.State.DNS_NEEDED:
return "DNS needed"
@ -1588,7 +1587,7 @@ class Domain(TimeStampedModel, DomainHelper):
# Given expired is not a physical state, but it is displayed as such,
# We need custom logic to determine this message.
help_text = "This domain has expired. Complete the online renewal process to maintain access."
elif flag_is_active(request, "domain_renewal") and self.is_expiring():
elif self.is_expiring():
help_text = "This domain is expiring soon. Complete the online renewal process to maintain access."
else:
help_text = Domain.State.get_help_text(self.state)

View file

@ -271,9 +271,6 @@ class User(AbstractUser):
def is_portfolio_admin(self, portfolio):
return "Admin" in self.portfolio_role_summary(portfolio)
def has_domain_renewal_flag(self):
return flag_is_active_for_user(self, "domain_renewal")
def get_first_portfolio(self):
permission = self.portfolio_permissions.first()
if permission:

View file

@ -15,13 +15,28 @@
</ul>
{% else %}
<ul>
{% if opts.model_name == 'domaininvitation' %}
{% if invitation.status == invitation.DomainInvitationStatus.INVITED %}
<li>
<form method="post">
{% csrf_token %}
<input type="hidden" name="cancel_invitation" value="true">
<button type="submit" class="usa-button--dja">
Cancel invitation
</button>
</form>
</li>
{% endif %}
{% endif %}
<li>
<a href="{% add_preserved_filters history_url %}">{% translate "History" %}</a>
</li>
{% if opts.model_name == 'domainrequest' %}
<li>
<a id="id-copy-to-clipboard-summary" class="usa-button--dja" type="button" href="#">
<svg class="usa-icon" >
<svg class="usa-icon">
<use xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
</svg>
<!-- the span is targeted in JS, do not remove -->
@ -32,4 +47,3 @@
</ul>
{% endif %}
{% endblock %}

View file

@ -11,4 +11,4 @@
</div>
</div>
{{ block.super }}
{% endblock %}
{% endblock %}

View file

@ -35,7 +35,7 @@
{# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
Expired
{% elif has_domain_renewal_flag and domain.is_expiring %}
{% elif domain.is_expiring %}
Expiring soon
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
DNS needed
@ -46,17 +46,17 @@
{% if domain.get_state_help_text %}
<p class="margin-y-0 text-primary-darker">
{% if has_domain_renewal_flag and domain.is_expired and is_domain_manager %}
{% if domain.is_expired and is_domain_manager %}
This domain has expired, but it is still online.
{% url 'domain-renewal' pk=domain.id as url %}
<a href="{{ url }}" class="usa-link">Renew to maintain access.</a>
{% elif has_domain_renewal_flag and domain.is_expiring and is_domain_manager %}
{% elif domain.is_expiring and is_domain_manager %}
This domain will expire soon.
{% url 'domain-renewal' pk=domain.id as url %}
<a href="{{ url }}" class="usa-link">Renew to maintain access.</a>
{% elif has_domain_renewal_flag and domain.is_expiring and is_portfolio_user %}
{% elif domain.is_expiring and is_portfolio_user %}
This domain will expire soon. Contact one of the listed domain managers to renew the domain.
{% elif has_domain_renewal_flag and domain.is_expired and is_portfolio_user %}
{% elif domain.is_expired and is_portfolio_user %}
This domain has expired, but it is still online. Contact one of the listed domain managers to renew the domain.
{% else %}
{{ domain.get_state_help_text }}

View file

@ -81,7 +81,7 @@
{% endwith %}
{% if has_domain_renewal_flag and is_domain_manager%}
{% if is_domain_manager%}
{% if domain.is_expiring or domain.is_expired %}
{% with url_name="domain-renewal" %}
{% include "includes/domain_sidenav_item.html" with item_text="Renewal form" %}

View file

@ -17,7 +17,7 @@ Domains should uniquely identify a government organization and be clear to the g
ACTION NEEDED
First, we need you to identify a new domain name that meets our naming requirements for your type of organization. Then, log in to the registrar and update the name in your domain request. <https://manage.get.gov/> Once you submit your updated request, well resume the adjudication process.
First, we need you to identify a new domain name that meets our naming requirements for your type of organization. Then, log in to the registrar and update the name in your domain request. <{{ manage_url }}> Once you submit your updated request, well resume the adjudication process.
If you have questions or want to discuss potential domain names, reply to this email.

View file

@ -21,7 +21,7 @@ We expect a senior official to be someone in a role of significant, executive re
ACTION NEEDED
Reply to this email with a justification for naming {{ domain_request.senior_official.get_formatted_name }} as the senior official. If you have questions or comments, include those in your reply.
Alternatively, you can log in to the registrar and enter a different senior official for this domain request. <https://manage.get.gov/> Once you submit your updated request, well resume the adjudication process.
Alternatively, you can log in to the registrar and enter a different senior official for this domain request. <{{ manage_url }}> Once you submit your updated request, well resume the adjudication process.
THANK YOU

View file

@ -4,7 +4,7 @@ Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first
{{ requestor_email }} has invited you to manage:
{% for domain in domains %}{{ domain.name }}
{% endfor %}
To manage domain information, visit the .gov registrar <https://manage.get.gov>.
To manage domain information, visit the .gov registrar <{{ manage_url }}>.
----------------------------------------------------------------
{% if not requested_user %}

View file

@ -15,7 +15,7 @@ The person who received the invitation will become a domain manager once they lo
associated with the invited email address.
If you need to cancel this invitation or remove the domain manager, you can do that by going to
this domain in the .gov registrar <https://manage.get.gov/>.
this domain in the .gov registrar <{{ manage_url }}>.
WHY DID YOU RECEIVE THIS EMAIL?

View file

@ -11,7 +11,7 @@ STATUS: Withdrawn
----------------------------------------------------------------
YOU CAN EDIT YOUR WITHDRAWN REQUEST
You can edit and resubmit this request by signing in to the registrar <https://manage.get.gov/>.
You can edit and resubmit this request by signing in to the registrar <{{ manage_url }}>.
SOMETHING WRONG?

View file

@ -16,7 +16,7 @@ The person who received the invitation will become an admin once they log in to
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/>.
the Members section for your organization <{{ manage_url }}>.
WHY DID YOU RECEIVE THIS EMAIL?

View file

@ -8,7 +8,7 @@ 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/>.
You can view this update by going to the Members section for your .gov organization <{{ manage_url }}>.
----------------------------------------------------------------

View file

@ -3,7 +3,7 @@ Hi.
{{ requestor_email }} has invited you to {{ portfolio.organization_name }}.
You can view this organization on the .gov registrar <https://manage.get.gov>.
You can view this organization on the .gov registrar <{{ manage_url }}>.
----------------------------------------------------------------

View file

@ -8,7 +8,7 @@ REQUESTED BY: {{ domain_request.creator.email }}
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Approved
You can manage your approved domain on the .gov registrar <https://manage.get.gov>.
You can manage your approved domain on the .gov registrar <{{ manage_url }}>.
----------------------------------------------------------------

View file

@ -20,7 +20,7 @@ During our review, well verify that:
- You work at the organization and/or can make requests on its behalf
- Your requested domain meets our naming requirements
{% endif %}
Well email you if we have questions. Well also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. <https://manage.get.gov>.
Well email you if we have questions. Well also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. <{{ manage_url }}>.
NEED TO MAKE CHANGES?

View file

@ -31,7 +31,7 @@ CHECK YOUR .GOV DOMAIN CONTACTS
This is a good time to check who has access to your .gov domain{% if domains|length > 1 %}s{% endif %}. The admin, technical, and billing contacts listed for your domain{% if domains|length > 1 %}s{% endif %} in our old system also received this email. In our new registrar, these contacts are all considered “domain managers.” We no longer have the admin, technical, and billing roles, and you arent limited to three domain managers like in the old system.
1. Once you have your Login.gov account, sign in to the new registrar at <https://manage.get.gov>.
1. Once you have your Login.gov account, sign in to the new registrar at <{{ manage_url }}>.
2. Click the “Manage” link next to your .gov domain, then click on “Domain managers” to see who has access to your domain.
3. If any of these users should not have access to your domain, let us know in a reply to this email.
@ -57,7 +57,7 @@ THANK YOU
The .gov team
.Gov blog <https://get.gov/updates/>
Domain management <https://manage.get.gov>
Domain management <{{ manage_url }}}>
Get.gov <https://get.gov>
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <https://cisa.gov/>

View file

@ -8,7 +8,7 @@ UPDATED BY: {{user}}
UPDATED ON: {{date}}
INFORMATION UPDATED: {{changes}}
You can view this update in the .gov registrar <https://manage.get.gov/>.
You can view this update in the .gov registrar <{{ manage_url }}>.
Get help with managing your .gov domain <https://get.gov/help/domain-management/>.

View file

@ -9,7 +9,7 @@
<span id="get_domains_json_url" class="display-none">{{url}}</span>
<!-- Org model banner (org manager can view, domain manager can edit) -->
{% if has_domain_renewal_flag and num_expiring_domains > 0 and has_any_domains_portfolio_permission %}
{% if num_expiring_domains > 0 and has_any_domains_portfolio_permission %}
<section class="usa-site-alert--slim usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert">
<div class="usa-alert__body">
@ -75,7 +75,7 @@
</div>
<!-- Non org model banner -->
{% if has_domain_renewal_flag and num_expiring_domains > 0 and not portfolio %}
{% if num_expiring_domains > 0 and not portfolio %}
<section class="usa-site-alert--slim usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert">
<div class="usa-alert__body">
@ -173,7 +173,6 @@
>Deleted</label
>
</div>
{% if has_domain_renewal_flag %}
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
@ -185,7 +184,6 @@
<label class="usa-checkbox__label" for="filter-status-expiring"
>Expiring soon</label>
</div>
{% endif %}
</fieldset>
</div>
</div>

View file

@ -12,6 +12,7 @@ from registrar.models import (
Domain,
DomainRequest,
DomainInformation,
DomainInvitation,
User,
Host,
Portfolio,
@ -495,6 +496,107 @@ class TestDomainInformationInline(MockEppLib):
self.assertIn("poopy@gov.gov", domain_managers)
class TestDomainInvitationAdmin(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.staffuser = create_user(email="staffdomainmanager@meoward.com", is_staff=True)
cls.site = AdminSite()
cls.admin = DomainAdmin(model=Domain, admin_site=cls.site)
cls.factory = RequestFactory()
def setUp(self):
self.client = Client(HTTP_HOST="localhost:8080")
self.client.force_login(self.staffuser)
super().setUp()
def test_successful_cancel_invitation_flow_in_admin(self):
"""Testing canceling a domain invitation in Django Admin."""
# 1. Create a domain and assign staff user role + domain manager
domain = Domain.objects.create(name="cancelinvitationflowviaadmin.gov")
UserDomainRole.objects.create(user=self.staffuser, domain=domain, role="manager")
# 2. Invite a domain manager to the above domain
invitation = DomainInvitation.objects.create(
email="inviteddomainmanager@meoward.com",
domain=domain,
status=DomainInvitation.DomainInvitationStatus.INVITED,
)
# 3. Go to the Domain Invitations list in /admin
domain_invitation_list_url = reverse("admin:registrar_domaininvitation_changelist")
response = self.client.get(domain_invitation_list_url)
self.assertEqual(response.status_code, 200)
# 4. Go to the change view of that invitation and make sure you can see the button
domain_invitation_change_url = reverse("admin:registrar_domaininvitation_change", args=[invitation.id])
response = self.client.get(domain_invitation_change_url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Cancel invitation")
# 5. Click the cancel invitation button
response = self.client.post(domain_invitation_change_url, {"cancel_invitation": "true"}, follow=True)
# 6. Make sure we're redirect back to the change view page in /admin
self.assertRedirects(response, domain_invitation_change_url)
# 7. Confirm cancellation confirmation message appears
expected_message = f"Invitation for {invitation.email} on {domain.name} is canceled"
self.assertContains(response, expected_message)
def test_no_cancel_invitation_button_in_retrieved_state(self):
"""Shouldn't be able to see the "Cancel invitation" button if invitation is RETRIEVED state"""
# 1. Create a domain and assign staff user role + domain manager
domain = Domain.objects.create(name="retrieved.gov")
UserDomainRole.objects.create(user=self.staffuser, domain=domain, role="manager")
# 2. Invite a domain manager to the above domain and NOT in invited state
invitation = DomainInvitation.objects.create(
email="retrievedinvitation@meoward.com",
domain=domain,
status=DomainInvitation.DomainInvitationStatus.RETRIEVED,
)
# 3. Go to the Domain Invitations list in /admin
domain_invitation_list_url = reverse("admin:registrar_domaininvitation_changelist")
response = self.client.get(domain_invitation_list_url)
self.assertEqual(response.status_code, 200)
# 4. Go to the change view of that invitation and make sure you CANNOT see the button
domain_invitation_change_url = reverse("admin:registrar_domaininvitation_change", args=[invitation.id])
response = self.client.get(domain_invitation_change_url)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, "Cancel invitation")
def test_no_cancel_invitation_button_in_canceled_state(self):
"""Shouldn't be able to see the "Cancel invitation" button if invitation is CANCELED state"""
# 1. Create a domain and assign staff user role + domain manager
domain = Domain.objects.create(name="canceled.gov")
UserDomainRole.objects.create(user=self.staffuser, domain=domain, role="manager")
# 2. Invite a domain manager to the above domain and NOT in invited state
invitation = DomainInvitation.objects.create(
email="canceledinvitation@meoward.com",
domain=domain,
status=DomainInvitation.DomainInvitationStatus.CANCELED,
)
# 3. Go to the Domain Invitations list in /admin
domain_invitation_list_url = reverse("admin:registrar_domaininvitation_changelist")
response = self.client.get(domain_invitation_list_url)
self.assertEqual(response.status_code, 200)
# 4. Go to the change view of that invitation and make sure you CANNOT see the button
domain_invitation_change_url = reverse("admin:registrar_domaininvitation_change", args=[invitation.id])
response = self.client.get(domain_invitation_change_url)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, "Cancel invitation")
class TestDomainAdminWithClient(TestCase):
"""Test DomainAdmin class as super user.

View file

@ -108,6 +108,82 @@ class TestEmails(TestCase):
self.assertEqual(["testy2@town.com", "mayor@igorville.gov"], kwargs["Destination"]["CcAddresses"])
@boto3_mocking.patching
@override_settings(IS_PRODUCTION=True, BASE_URL="manage.get.gov")
def test_email_production_subject_and_url_check(self):
"""Test sending an email in production that:
1. Does not have a prefix in the email subject (no [MANAGE])
2. Uses the production URL in the email body of manage.get.gov still"""
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
send_templated_email(
"emails/update_to_approved_domain.txt",
"emails/update_to_approved_domain_subject.txt",
"doesnotexist@igorville.com",
context={"domain": "test", "user": "test", "date": 1, "changes": "test"},
bcc_address=None,
cc_addresses=["testy2@town.com", "mayor@igorville.gov"],
)
# check that an email was sent
self.assertTrue(self.mock_client.send_email.called)
# check the call sequence for the email
args, kwargs = self.mock_client.send_email.call_args
self.assertIn("Destination", kwargs)
self.assertIn("CcAddresses", kwargs["Destination"])
self.assertEqual(["testy2@town.com", "mayor@igorville.gov"], kwargs["Destination"]["CcAddresses"])
# Grab email subject
email_subject = kwargs["Content"]["Simple"]["Subject"]["Data"]
# Check that the subject does NOT contain a prefix for production
self.assertNotIn("[MANAGE]", email_subject)
self.assertIn("An update was made to", email_subject)
# Grab email body
email_body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
# Check that manage_url is correctly set for production
self.assertIn("https://manage.get.gov", email_body)
@boto3_mocking.patching
@override_settings(IS_PRODUCTION=False, BASE_URL="https://getgov-rh.app.cloud.gov")
def test_email_non_production_subject_and_url_check(self):
"""Test sending an email in production that:
1. Does prefix in the email subject (ie [GETGOV-RH])
2. Uses the sandbox url in the email body (ie getgov-rh.app.cloud.gov)"""
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
send_templated_email(
"emails/update_to_approved_domain.txt",
"emails/update_to_approved_domain_subject.txt",
"doesnotexist@igorville.com",
context={"domain": "test", "user": "test", "date": 1, "changes": "test"},
bcc_address=None,
cc_addresses=["testy2@town.com", "mayor@igorville.gov"],
)
# check that an email was sent
self.assertTrue(self.mock_client.send_email.called)
# check the call sequence for the email
args, kwargs = self.mock_client.send_email.call_args
self.assertIn("Destination", kwargs)
self.assertIn("CcAddresses", kwargs["Destination"])
self.assertEqual(["testy2@town.com", "mayor@igorville.gov"], kwargs["Destination"]["CcAddresses"])
# Grab email subject
email_subject = kwargs["Content"]["Simple"]["Subject"]["Data"]
# Check that the subject DOES contain a prefix of the current sandbox
self.assertIn("[GETGOV-RH]", email_subject)
# Grab email body
email_body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
# Check that manage_url is correctly set of the sandbox
self.assertIn("https://getgov-rh.app.cloud.gov", email_body)
@boto3_mocking.patching
@less_console_noise_decorator
def test_submission_confirmation(self):

View file

@ -477,7 +477,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.domain_with_ip.expiration_date = self.expiration_date_one_year_out()
self.domain_with_ip.save()
@override_flag("domain_renewal", active=True)
def test_expiring_domain_on_detail_page_as_domain_manager(self):
"""If a user is a domain manager and their domain is expiring soon,
user should be able to see the "Renew to maintain access" link domain overview detail box."""
@ -496,7 +495,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.assertNotContains(detail_page, "DNS needed")
self.assertNotContains(detail_page, "Expired")
@override_flag("domain_renewal", active=True)
@override_flag("organization_feature", active=True)
def test_expiring_domain_on_detail_page_in_org_model_as_a_non_domain_manager(self):
"""In org model: If a user is NOT a domain manager and their domain is expiring soon,
@ -534,7 +532,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
)
self.assertContains(detail_page, "Contact one of the listed domain managers to renew the domain.")
@override_flag("domain_renewal", active=True)
@override_flag("organization_feature", active=True)
def test_expiring_domain_on_detail_page_in_org_model_as_a_domain_manager(self):
"""Inorg model: If a user is a domain manager and their domain is expiring soon,
@ -555,7 +552,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
)
self.assertContains(detail_page, "Renew to maintain access")
@override_flag("domain_renewal", active=True)
def test_domain_renewal_form_and_sidebar_expiring(self):
"""If a user is a domain manager and their domain is expiring soon,
user should be able to see Renewal Form on the sidebar."""
@ -584,7 +580,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.assertEqual(response.status_code, 200)
self.assertContains(response, f"Renew {self.domain_to_renew.name}")
@override_flag("domain_renewal", active=True)
def test_domain_renewal_form_and_sidebar_expired(self):
"""If a user is a domain manager and their domain is expired,
user should be able to see Renewal Form on the sidebar."""
@ -614,7 +609,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.assertEqual(response.status_code, 200)
self.assertContains(response, f"Renew {self.domain_to_renew.name}")
@override_flag("domain_renewal", active=True)
def test_domain_renewal_form_your_contact_info_edit(self):
"""Checking that if a user is a domain manager they can edit the
Your Profile portion of the Renewal Form."""
@ -634,7 +628,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.assertEqual(edit_page.status_code, 200)
self.assertContains(edit_page, "Review the details below and update any required information")
@override_flag("domain_renewal", active=True)
def test_domain_renewal_form_security_email_edit(self):
"""Checking that if a user is a domain manager they can edit the
Security Email portion of the Renewal Form."""
@ -657,7 +650,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.assertEqual(edit_page.status_code, 200)
self.assertContains(edit_page, "A security contact should be capable of evaluating")
@override_flag("domain_renewal", active=True)
def test_domain_renewal_form_domain_manager_edit(self):
"""Checking that if a user is a domain manager they can edit the
Domain Manager portion of the Renewal Form."""
@ -677,7 +669,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.assertEqual(edit_page.status_code, 200)
self.assertContains(edit_page, "Domain managers can update all information related to a domain")
@override_flag("domain_renewal", active=True)
def test_domain_renewal_form_not_expired_or_expiring(self):
"""Checking that if the user's domain is not expired or expiring that user should not be able
to access /renewal and that it should receive a 403."""
@ -686,7 +677,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domain_not_expiring.id}))
self.assertEqual(renewal_page.status_code, 403)
@override_flag("domain_renewal", active=True)
def test_domain_renewal_form_does_not_appear_if_not_domain_manager(self):
"""If user is not a domain manager and tries to access /renewal, user should receive a 403."""
with patch.object(Domain, "is_expired", self.custom_is_expired_true), patch.object(
@ -695,7 +685,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
renewal_page = self.client.get(reverse("domain-renewal", kwargs={"pk": self.domain_no_domain_manager.id}))
self.assertEqual(renewal_page.status_code, 403)
@override_flag("domain_renewal", active=True)
def test_ack_checkbox_not_checked(self):
"""If user don't check the checkbox, user should receive an error message."""
# Grab the renewal URL
@ -707,7 +696,6 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
error_message = "Check the box if you read and agree to the requirements for operating a .gov domain."
self.assertContains(response, error_message)
@override_flag("domain_renewal", active=True)
def test_ack_checkbox_checked(self):
"""If user check the checkbox and submits the form,
user should be redirected Domain Over page with an updated by 1 year expiration date"""
@ -2992,26 +2980,15 @@ class TestDomainRenewal(TestWithUser):
pass
super().tearDown()
# Remove test_without_domain_renewal_flag when domain renewal is released as a feature
@less_console_noise_decorator
@override_flag("domain_renewal", active=False)
def test_without_domain_renewal_flag(self):
self.client.force_login(self.user)
domains_page = self.client.get("/")
self.assertNotContains(domains_page, "will expire soon")
self.assertNotContains(domains_page, "Expiring soon")
@less_console_noise_decorator
@override_flag("domain_renewal", active=True)
def test_domain_renewal_flag_single_domain(self):
def test_domain_with_single_domain(self):
self.client.force_login(self.user)
domains_page = self.client.get("/")
self.assertContains(domains_page, "One domain will expire soon")
self.assertContains(domains_page, "Expiring soon")
@less_console_noise_decorator
@override_flag("domain_renewal", active=True)
def test_with_domain_renewal_flag_mulitple_domains(self):
def test_with_mulitple_domains(self):
today = datetime.now()
expiring_date = (today + timedelta(days=30)).strftime("%Y-%m-%d")
self.domain_with_another_expiring, _ = Domain.objects.get_or_create(
@ -3027,8 +3004,7 @@ class TestDomainRenewal(TestWithUser):
self.assertContains(domains_page, "Expiring soon")
@less_console_noise_decorator
@override_flag("domain_renewal", active=True)
def test_with_domain_renewal_flag_no_expiring_domains(self):
def test_with_no_expiring_domains(self):
UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expired_date).delete()
UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expiring_soon_date).delete()
self.client.force_login(self.user)
@ -3036,18 +3012,16 @@ class TestDomainRenewal(TestWithUser):
self.assertNotContains(domains_page, "will expire soon")
@less_console_noise_decorator
@override_flag("domain_renewal", active=True)
@override_flag("organization_feature", active=True)
def test_domain_renewal_flag_single_domain_w_org_feature_flag(self):
def test_single_domain_w_org_feature_flag(self):
self.client.force_login(self.user)
domains_page = self.client.get("/")
self.assertContains(domains_page, "One domain will expire soon")
self.assertContains(domains_page, "Expiring soon")
@less_console_noise_decorator
@override_flag("domain_renewal", active=True)
@override_flag("organization_feature", active=True)
def test_with_domain_renewal_flag_mulitple_domains_w_org_feature_flag(self):
def test_with_mulitple_domains_w_org_feature_flag(self):
today = datetime.now()
expiring_date = (today + timedelta(days=31)).strftime("%Y-%m-%d")
self.domain_with_another_expiring_org_model, _ = Domain.objects.get_or_create(
@ -3063,9 +3037,8 @@ class TestDomainRenewal(TestWithUser):
self.assertContains(domains_page, "Expiring soon")
@less_console_noise_decorator
@override_flag("domain_renewal", active=True)
@override_flag("organization_feature", active=True)
def test_with_domain_renewal_flag_no_expiring_domains_w_org_feature_flag(self):
def test_no_expiring_domains_w_org_feature_flag(self):
UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expired_date).delete()
UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expiring_soon_date).delete()
self.client.force_login(self.user)

View file

@ -3,6 +3,7 @@
import boto3
import logging
import textwrap
import re
from datetime import datetime
from django.apps import apps
from django.conf import settings
@ -48,6 +49,21 @@ def send_templated_email( # noqa
No valid recipient addresses are provided
"""
if context is None:
context = {}
env_base_url = settings.BASE_URL
# The regular expression is to get both http (localhost) and https (everything else)
env_name = re.sub(r"^https?://", "", env_base_url).split(".")[0]
# If NOT in prod, add env to the subject line
# IE adds [GETGOV-RH] if we are in the -RH sandbox
prefix = f"[{env_name.upper()}] " if not settings.IS_PRODUCTION else ""
# If NOT in prod, update instances of "manage.get.gov" links to point to
# current environment, ie "getgov-rh.app.cloud.gov"
manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov"
context["manage_url"] = manage_url
# by default assume we can send to all addresses (prod has no whitelist)
sendable_cc_addresses = cc_addresses
@ -70,8 +86,10 @@ def send_templated_email( # noqa
if email_body:
email_body.strip().lstrip("\n")
# Update the subject to have prefix here versus every email
subject_template = get_template(subject_template_name)
subject = subject_template.render(context=context)
subject = f"{prefix}{subject}"
try:
ses_client = boto3.client(

View file

@ -366,7 +366,7 @@ class DomainRenewalView(DomainBaseView):
return HttpResponseRedirect(reverse("domain", kwargs={"pk": pk}))
# if not valid, render the template with error messages
# passing editable, has_domain_renewal_flag, and is_editable for re-render
# passing editable and is_editable for re-render
return render(
request,
"domain_renewal.html",
@ -374,7 +374,6 @@ class DomainRenewalView(DomainBaseView):
"domain": domain,
"form": form,
"is_editable": True,
"has_domain_renewal_flag": True,
"is_domain_manager": True,
},
)