Merge branch 'main' of https://github.com/cisagov/manage.get.gov into es/3175-email-updates

This commit is contained in:
Erin Song 2025-01-14 10:38:01 -08:00
commit bff19e3a28
No known key found for this signature in database
19 changed files with 678 additions and 53 deletions

View file

@ -103,3 +103,31 @@ response = registry._client.transport.receive()
```
This is helpful for debugging situations where epplib is not correctly or fully parsing the XML returned from the registry.
### Adding in a expiring soon domain
The below scenario is if you are NOT in org model mode (`organization_feature` waffle flag is off).
1. Go to the `staging` sandbox and to `/admin`
2. Go to Domains and find a domain that is actually expired by sorting the Expiration Date column
3. Click into the domain to check the expiration date
4. Click into Manage Domain to double check the expiration date as well
5. Now hold onto that domain name, and save it for the command below
6. In a terminal, run these commands:
```
cf ssh getgov-<your-intials>
/tmp/lifecycle/shell
./manage.py shell
from registrar.models import Domain, DomainInvitation
from registrar.models import User
user = User.objects.filter(first_name="<your-first-name>")
domain = Domain.objects.get_or_create(name="<that-domain-here>")
```
7. Go back to `/admin` and create Domain Information for that domain you just added in via the terminal
8. Go to Domain to find it
9. Click Manage Domain
10. Add yourself as domain manager
11. Go to the Registrar page and you should now see the expiring domain
If you want to be in the org model mode, turn the `organization_feature` waffle flag on, and add that domain via Django Admin to a portfolio to be able to view it.

View file

@ -345,6 +345,11 @@ urlpatterns = [
views.DomainSecurityEmailView.as_view(),
name="domain-security-email",
),
path(
"domain/<int:pk>/renewal",
views.DomainRenewalView.as_view(),
name="domain-renewal",
),
path(
"domain/<int:pk>/users/add",
views.DomainAddUserView.as_view(),

View file

@ -10,6 +10,7 @@ from .domain import (
DomainDsdataFormset,
DomainDsdataForm,
DomainSuborganizationForm,
DomainRenewalForm,
)
from .portfolio import (
PortfolioOrgAddressForm,

View file

@ -661,3 +661,15 @@ DomainDsdataFormset = formset_factory(
extra=0,
can_delete=True,
)
class DomainRenewalForm(forms.Form):
"""Form making sure domain renewal ack is checked"""
is_policy_acknowledged = forms.BooleanField(
required=True,
label="I have read and agree to the requirements for operating a .gov domain.",
error_messages={
"required": "Check the box if you read and agree to the requirements for operating a .gov domain."
},
)

View file

@ -5,6 +5,7 @@ import logging
from django.core.management import BaseCommand, CommandError
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper
from registrar.models import DomainInformation, DomainRequest, FederalAgency, Suborganization, Portfolio, User
from registrar.models.utility.generic_helper import normalize_string
logger = logging.getLogger(__name__)
@ -21,10 +22,21 @@ class Command(BaseCommand):
self.failed_portfolios = set()
def add_arguments(self, parser):
"""Add three arguments:
1. agency_name => the value of FederalAgency.agency
2. --parse_requests => if true, adds the given portfolio to each related DomainRequest
3. --parse_domains => if true, adds the given portfolio to each related DomainInformation
"""Add command line arguments to create federal portfolios.
Required (mutually exclusive) arguments:
--agency_name: Name of a specific FederalAgency to create a portfolio for
--branch: Federal branch to process ("executive", "legislative", or "judicial").
Creates portfolios for all FederalAgencies in that branch.
Required (at least one):
--parse_requests: Add the created portfolio(s) to related DomainRequest records
--parse_domains: Add the created portfolio(s) to related DomainInformation records
Note: You can use both --parse_requests and --parse_domains together
Optional (mutually exclusive with parse options):
--both: Shorthand for using both --parse_requests and --parse_domains
Cannot be used with --parse_requests or --parse_domains
"""
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
@ -78,12 +90,14 @@ class Command(BaseCommand):
else:
raise CommandError(f"Cannot find '{branch}' federal agencies in our database.")
portfolios = []
for federal_agency in agencies:
message = f"Processing federal agency '{federal_agency.agency}'..."
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
try:
# C901 'Command.handle' is too complex (12)
self.handle_populate_portfolio(federal_agency, parse_domains, parse_requests, both)
portfolio = self.handle_populate_portfolio(federal_agency, parse_domains, parse_requests, both)
portfolios.append(portfolio)
except Exception as exec:
self.failed_portfolios.add(federal_agency)
logger.error(exec)
@ -99,9 +113,65 @@ class Command(BaseCommand):
display_as_str=True,
)
# POST PROCESSING STEP: Remove the federal agency if it matches the portfolio name.
# We only do this for started domain requests.
if parse_requests or both:
TerminalHelper.prompt_for_execution(
system_exit_on_terminate=True,
prompt_message="This action will update domain requests even if they aren't on a portfolio.",
prompt_title=(
"POST PROCESS STEP: Do you want to clear federal agency on (related) started domain requests?"
),
verify_message=None,
)
self.post_process_started_domain_requests(agencies, portfolios)
def post_process_started_domain_requests(self, agencies, portfolios):
"""
Removes duplicate organization data by clearing federal_agency when it matches the portfolio name.
Only processes domain requests in STARTED status.
"""
message = "Removing duplicate portfolio and federal_agency values from domain requests..."
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
# For each request, clear the federal agency under these conditions:
# 1. A portfolio *already exists* with the same name as the federal agency.
# 2. Said portfolio (or portfolios) are only the ones specified at the start of the script.
# 3. The domain request is in status "started".
# Note: Both names are normalized so excess spaces are stripped and the string is lowercased.
domain_requests_to_update = DomainRequest.objects.filter(
federal_agency__in=agencies,
federal_agency__agency__isnull=False,
status=DomainRequest.DomainRequestStatus.STARTED,
organization_name__isnull=False,
)
portfolio_set = {normalize_string(portfolio.organization_name) for portfolio in portfolios if portfolio}
# Update the request, assuming the given agency name matches the portfolio name
updated_requests = []
for req in domain_requests_to_update:
agency_name = normalize_string(req.federal_agency.agency)
if agency_name in portfolio_set:
req.federal_agency = None
updated_requests.append(req)
# Execute the update and Log the results
if TerminalHelper.prompt_for_execution(
system_exit_on_terminate=False,
prompt_message=(
f"{len(domain_requests_to_update)} domain requests will be updated. "
f"These records will be changed: {[str(req) for req in updated_requests]}"
),
prompt_title="Do you wish to commit this update to the database?",
):
DomainRequest.objects.bulk_update(updated_requests, ["federal_agency"])
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKBLUE, "Action completed successfully.")
def handle_populate_portfolio(self, federal_agency, parse_domains, parse_requests, both):
"""Attempts to create a portfolio. If successful, this function will
also create new suborganizations"""
also create new suborganizations.
Returns the portfolio for the given federal_agency.
"""
portfolio, created = self.create_portfolio(federal_agency)
if created:
self.create_suborganizations(portfolio, federal_agency)
@ -111,6 +181,8 @@ class Command(BaseCommand):
if parse_requests or both:
self.handle_portfolio_requests(portfolio, federal_agency)
return portfolio
def create_portfolio(self, federal_agency):
"""Creates a portfolio if it doesn't presently exist.
Returns portfolio, created."""
@ -172,7 +244,7 @@ class Command(BaseCommand):
return
# Check for existing suborgs on the current portfolio
existing_suborgs = Suborganization.objects.filter(name__in=org_names)
existing_suborgs = Suborganization.objects.filter(name__in=org_names, name__isnull=False)
if existing_suborgs.exists():
message = f"Some suborganizations already exist for portfolio '{portfolio}'."
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKBLUE, message)
@ -180,9 +252,7 @@ class Command(BaseCommand):
# Create new suborgs, as long as they don't exist in the db already
new_suborgs = []
for name in org_names - set(existing_suborgs.values_list("name", flat=True)):
# Stored in variables due to linter wanting type information here.
portfolio_name: str = portfolio.organization_name if portfolio.organization_name is not None else ""
if name is not None and name.lower() == portfolio_name.lower():
if normalize_string(name) == normalize_string(portfolio.organization_name):
# You can use this to populate location information, when this occurs.
# However, this isn't needed for now so we can skip it.
message = (
@ -229,12 +299,30 @@ class Command(BaseCommand):
# Get all suborg information and store it in a dict to avoid doing a db call
suborgs = Suborganization.objects.filter(portfolio=portfolio).in_bulk(field_name="name")
for domain_request in domain_requests:
# Set the portfolio
domain_request.portfolio = portfolio
if domain_request.organization_name in suborgs:
domain_request.sub_organization = suborgs.get(domain_request.organization_name)
# Set suborg info
domain_request.sub_organization = suborgs.get(domain_request.organization_name, None)
if domain_request.sub_organization is None:
domain_request.requested_suborganization = normalize_string(
domain_request.organization_name, lowercase=False
)
domain_request.suborganization_city = normalize_string(domain_request.city, lowercase=False)
domain_request.suborganization_state_territory = domain_request.state_territory
self.updated_portfolios.add(portfolio)
DomainRequest.objects.bulk_update(domain_requests, ["portfolio", "sub_organization"])
DomainRequest.objects.bulk_update(
domain_requests,
[
"portfolio",
"sub_organization",
"requested_suborganization",
"suborganization_city",
"suborganization_state_territory",
],
)
message = f"Added portfolio '{portfolio}' to {len(domain_requests)} domain requests."
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
@ -242,6 +330,8 @@ class Command(BaseCommand):
"""
Associate portfolio with domains for a federal agency.
Updates all relevant domain information records.
Returns a queryset of DomainInformation objects, or None if nothing changed.
"""
domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency, portfolio__isnull=True)
if not domain_infos.exists():
@ -257,8 +347,7 @@ class Command(BaseCommand):
suborgs = Suborganization.objects.filter(portfolio=portfolio).in_bulk(field_name="name")
for domain_info in domain_infos:
domain_info.portfolio = portfolio
if domain_info.organization_name in suborgs:
domain_info.sub_organization = suborgs.get(domain_info.organization_name)
domain_info.sub_organization = suborgs.get(domain_info.organization_name, None)
DomainInformation.objects.bulk_update(domain_infos, ["portfolio", "sub_organization"])
message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains."

View file

@ -326,9 +326,8 @@ class Domain(TimeStampedModel, DomainHelper):
exp_date = self.registry_expiration_date
except KeyError:
# if no expiration date from registry, set it to today
logger.warning("current expiration date not set; setting to today")
logger.warning("current expiration date not set; setting to today", exc_info=True)
exp_date = date.today()
# create RenewDomain request
request = commands.RenewDomain(name=self.name, cur_exp_date=exp_date, period=epp.Period(length, unit))
@ -338,13 +337,14 @@ class Domain(TimeStampedModel, DomainHelper):
self._cache["ex_date"] = registry.send(request, cleaned=True).res_data[0].ex_date
self.expiration_date = self._cache["ex_date"]
self.save()
except RegistryError as err:
# if registry error occurs, log the error, and raise it as well
logger.error(f"registry error renewing domain: {err}")
logger.error(f"Registry error renewing domain '{self.name}': {err}")
raise (err)
except Exception as e:
# exception raised during the save to registrar
logger.error(f"error updating expiration date in registrar: {e}")
logger.error(f"Error updating expiration date for domain '{self.name}' in registrar: {e}")
raise (e)
@Cache
@ -1575,7 +1575,7 @@ class Domain(TimeStampedModel, DomainHelper):
logger.info("Changing to DNS_NEEDED state")
logger.info("able to transition to DNS_NEEDED state")
def get_state_help_text(self) -> str:
def get_state_help_text(self, request=None) -> str:
"""Returns a str containing additional information about a given state.
Returns custom content for when the domain itself is expired."""
@ -1585,6 +1585,8 @@ class Domain(TimeStampedModel, DomainHelper):
help_text = (
"This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov."
)
elif flag_is_active(request, "domain_renewal") and self.is_expiring():
help_text = "This domain will expire soon. Contact one of the listed domain managers to renew the domain."
else:
help_text = Domain.State.get_help_text(self.state)

View file

@ -343,3 +343,13 @@ def value_of_attribute(obj, attribute_name: str):
if callable(value):
value = value()
return value
def normalize_string(string_to_normalize, lowercase=True):
"""Normalizes a given string. Returns a string without extra spaces, in all lowercase."""
if not isinstance(string_to_normalize, str):
logger.error(f"normalize_string => {string_to_normalize} is not type str.")
return string_to_normalize
new_string = " ".join(string_to_normalize.split())
return new_string.lower() if lowercase else new_string

View file

@ -1,5 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% load static url_helpers %}
{% block title %}{{ domain.name }} | {% endblock %}
@ -53,8 +55,11 @@
{% endif %}
{% block domain_content %}
{% if request.path|endswith:"renewal"%}
<h1>Renew {{domain.name}} </h1>
{%else%}
<h1 class="break-word">Domain Overview</h1>
{% endif%}
{% endblock %} {# domain_content #}
{% endif %}
@ -62,4 +67,4 @@
</div>
</div>
</div>
{% endblock %} {# content #}
{% endblock %} {# content #}

View file

@ -49,10 +49,18 @@
</span>
{% if domain.get_state_help_text %}
<div class="padding-top-1 text-primary-darker">
{% if has_domain_renewal_flag and domain.is_expiring and is_domain_manager %}
This domain will expire soon. <a href="/not-available-yet">Renew to maintain access.</a>
{% if has_domain_renewal_flag and 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 }}">Renew to maintain access.</a>
{% elif has_domain_renewal_flag and domain.is_expiring and is_domain_manager %}
This domain will expire soon.
{% url 'domain-renewal' pk=domain.id as url %}
<a href="{{ url }}">Renew to maintain access.</a>
{% elif has_domain_renewal_flag and 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 %}
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 }}
{% endif %}

View file

@ -0,0 +1,140 @@
{% extends "domain_base.html" %}
{% load static url_helpers %}
{% load custom_filters %}
{% block domain_content %}
{% block breadcrumb %}
<!-- Banner for if_policy_acknowledged -->
{% if form.is_policy_acknowledged.errors %}
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2">
<div class="usa-alert__body">
{% for error in form.is_policy_acknowledged.errors %}
<p class="usa-alert__text">{{ error }}</p>
{% endfor %}
</div>
</div>
{% endif %}
{% if portfolio %}
<!-- Navigation breadcrumbs -->
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain breadcrumb">
<ol class="usa-breadcrumb__list">
<li class="usa-breadcrumb__list-item">
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
</li>
<li class="usa-breadcrumb__list-item">
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{domain.name}}</span></a>
</li>
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
<span>Renewal Form</span>
</li>
</ol>
</nav>
{% endif %}
{% endblock breadcrumb %}
{{ block.super }}
<div class="margin-top-4 tablet:grid-col-10">
<h2 class="text-bold text-primary-dark domain-name-wrap">Confirm the following information for accuracy</h2>
<p>Review these details below. We <a href="https://get.gov/domains/requirements/#what-.gov-domain-registrants-must-do" class="usa-link">
require</a> that you maintain accurate information for the domain.
The details you provide will only be used to support the administration of .gov and won't be made public.
</p>
<p>If you would like to retire your domain instead, please <a href="https://get.gov/contact/" class="usa-link">
contact us</a>. </p>
<p><em>Required fields are marked with an asterisk (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
</p>
{% url 'user-profile' as url %}
{% include "includes/summary_item.html" with title='Your contact information' value=request.user edit_link=url editable=is_editable contact='true' %}
{% if analyst_action != 'edit' or analyst_action_location != domain.pk %}
{% if is_portfolio_user and not is_domain_manager %}
<div class="usa-alert usa-alert--info usa-alert--slim">
<div class="usa-alert__body">
<p class="usa-alert__text ">
You don't have access to manage {{domain.name}}. If you need to make updates, contact one of the listed domain managers.
</p>
</div>
</div>
{% endif %}
{% endif %}
{% url 'domain-security-email' pk=domain.id as url %}
{% if security_email is not None and security_email not in hidden_security_emails%}
{% include "includes/summary_item.html" with title='Security email' value=security_email custom_text_for_value_none='We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain.' edit_link=url editable=is_editable %}
{% else %}
{% include "includes/summary_item.html" with title='Security email' value='None provided' custom_text_for_value_none='We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain.' edit_link=url editable=is_editable %}
{% endif %}
{% url 'domain-users' pk=domain.id as url %}
{% if portfolio %}
{% include "includes/summary_item.html" with title='Domain managers' domain_permissions=True value=domain edit_link=url editable=is_editable %}
{% else %}
{% include "includes/summary_item.html" with title='Domain managers' list=True users=True value=domain.permissions.all edit_link=url editable=is_editable %}
{% endif %}
<div class="border-top-1px border-primary-dark padding-top-1 margin-top-3 margin-bottom-2">
<fieldset class="usa-fieldset">
<legend>
<h3 class="summary-item__title
font-sans-md
text-primary-dark
text-semibold
margin-top-0
margin-bottom-05
padding-right-1">
Acknowledgement of .gov domain requirements </h3>
</legend>
<form method="post" action="{% url 'domain-renewal' pk=domain.id %}">
{% csrf_token %}
<div class="usa-checkbox">
{% if form.is_policy_acknowledged.errors %}
{% for error in form.is_policy_acknowledged.errors %}
<div class="usa-error-message display-flex" role="alert">
<svg class="usa-icon usa-icon--large" focusable="true" role="img" aria-label="Error">
<use xlink:href="{%static 'img/sprite.svg'%}#error"></use>
</svg>
<span class="margin-left-05">{{ error }}</span>
</div>
{% endfor %}
</div>
{% endif %}
<input type="hidden" name="is_policy_acknowledged" value="False">
<input
class="usa-checkbox__input"
id="renewal-checkbox"
type="checkbox"
name="is_policy_acknowledged"
value="True"
{% if form.is_policy_acknowledged.value %}checked{% endif %}
>
<label class="usa-checkbox__label" for="renewal-checkbox">
I read and agree to the
<a href="https://get.gov/domains/requirements/" class="usa-link">
requirements for operating a .gov domain
</a>.
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
</label>
</div>
<button
type="submit"
name="submit_button"
value="next"
class="usa-button margin-top-3"
> Submit
</button>
</form>
</fieldset>
</div> <!-- End of the acknowledgement section div -->
</div>
{% endblock %} {# domain_content #}

View file

@ -80,7 +80,16 @@
{% include "includes/domain_sidenav_item.html" with item_text="Domain managers" %}
{% endwith %}
{% if has_domain_renewal_flag and 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" %}
{% endwith %}
{% endif %}
{% endif %}
{% endif %}
</ul>
</nav>
</div>
</div>

View file

@ -172,7 +172,7 @@
>Deleted</label
>
</div>
{% if has_domain_renewal_flag and num_expiring_domains > 0 %}
{% if has_domain_renewal_flag %}
<div class="usa-checkbox">
<input
class="usa-checkbox__input"

View file

@ -127,15 +127,15 @@
</ul>
{% endif %}
{% else %}
<p class="margin-top-0 margin-bottom-0">
{% if custom_text_for_value_none %}
<p class="margin-top-0 text-base-dark">{{ custom_text_for_value_none }}</p>
{% endif %}
{% if value %}
{{ value }}
{% elif custom_text_for_value_none %}
{{ custom_text_for_value_none }}
{% else %}
{% endif %}
{% if not value %}
None
{% endif %}
</p>
{% endif %}
</div>

View file

@ -200,6 +200,7 @@ def is_domain_subpage(path):
"domain-users-add",
"domain-request-delete",
"domain-user-delete",
"domain-renewal",
"invitation-cancel",
]
return get_url_name(path) in url_names

View file

@ -1039,6 +1039,8 @@ def completed_domain_request( # noqa
federal_agency=None,
federal_type=None,
action_needed_reason=None,
city=None,
state_territory=None,
portfolio=None,
organization_name=None,
sub_organization=None,
@ -1081,7 +1083,7 @@ def completed_domain_request( # noqa
organization_name=organization_name if organization_name else "Testorg",
address_line1="address 1",
address_line2="address 2",
state_territory="NY",
state_territory="NY" if not state_territory else state_territory,
zipcode="10002",
senior_official=so,
requested_domain=domain,
@ -1090,6 +1092,10 @@ def completed_domain_request( # noqa
investigator=investigator,
federal_agency=federal_agency,
)
if city:
domain_request_kwargs["city"] = city
if has_about_your_organization:
domain_request_kwargs["about_your_organization"] = "e-Government"
if has_anything_else:

View file

@ -1516,6 +1516,91 @@ class TestCreateFederalPortfolio(TestCase):
):
call_command("create_federal_portfolio", **kwargs)
@less_console_noise_decorator
def test_post_process_started_domain_requests_existing_portfolio(self):
"""Ensures that federal agency is cleared when agency name matches portfolio name.
As the name implies, this implicitly tests the "post_process_started_domain_requests" function.
"""
federal_agency_2 = FederalAgency.objects.create(agency="Sugarcane", federal_type=BranchChoices.EXECUTIVE)
# Test records with portfolios and no org names
# Create a portfolio. This script skips over "started"
portfolio = Portfolio.objects.create(organization_name="Sugarcane", creator=self.user)
# Create a domain request with matching org name
matching_request = completed_domain_request(
name="matching.gov",
status=DomainRequest.DomainRequestStatus.STARTED,
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
federal_agency=federal_agency_2,
user=self.user,
portfolio=portfolio,
)
# Create a request not in started (no change should occur)
matching_request_in_wrong_status = completed_domain_request(
name="kinda-matching.gov",
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
federal_agency=self.federal_agency,
user=self.user,
)
self.run_create_federal_portfolio(agency_name="Sugarcane", parse_requests=True)
self.run_create_federal_portfolio(agency_name="Test Federal Agency", parse_requests=True)
# Refresh from db
matching_request.refresh_from_db()
matching_request_in_wrong_status.refresh_from_db()
# Request with matching name should have federal_agency cleared
self.assertIsNone(matching_request.federal_agency)
self.assertIsNotNone(matching_request.portfolio)
self.assertEqual(matching_request.portfolio.organization_name, "Sugarcane")
# Request with matching name but wrong state should keep its federal agency
self.assertEqual(matching_request_in_wrong_status.federal_agency, self.federal_agency)
self.assertIsNotNone(matching_request_in_wrong_status.portfolio)
self.assertEqual(matching_request_in_wrong_status.portfolio.organization_name, "Test Federal Agency")
@less_console_noise_decorator
def test_post_process_started_domain_requests(self):
"""Tests that federal agency is cleared when agency name
matches an existing portfolio's name, even if the domain request isn't
directly on that portfolio."""
federal_agency_2 = FederalAgency.objects.create(agency="Sugarcane", federal_type=BranchChoices.EXECUTIVE)
# Create a request with matching federal_agency name but no direct portfolio association
matching_agency_request = completed_domain_request(
name="agency-match.gov",
status=DomainRequest.DomainRequestStatus.STARTED,
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
federal_agency=federal_agency_2,
user=self.user,
)
# Create a control request that shouldn't match
non_matching_request = completed_domain_request(
name="no-match.gov",
status=DomainRequest.DomainRequestStatus.STARTED,
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
federal_agency=self.federal_agency,
user=self.user,
)
# We expect the matching agency to have its fed agency cleared.
self.run_create_federal_portfolio(agency_name="Sugarcane", parse_requests=True)
matching_agency_request.refresh_from_db()
non_matching_request.refresh_from_db()
# Request with matching agency name should have federal_agency cleared
self.assertIsNone(matching_agency_request.federal_agency)
# Non-matching request should keep its federal_agency
self.assertIsNotNone(non_matching_request.federal_agency)
self.assertEqual(non_matching_request.federal_agency, self.federal_agency)
@less_console_noise_decorator
def test_create_single_portfolio(self):
"""Test portfolio creation with suborg and senior official."""
self.run_create_federal_portfolio(agency_name="Test Federal Agency", parse_requests=True)
@ -1588,6 +1673,34 @@ class TestCreateFederalPortfolio(TestCase):
self.assertTrue(all([creator == User.get_default_user() for creator in creators]))
self.assertTrue(all([note == "Auto-generated record" for note in notes]))
def test_script_adds_requested_suborganization_information(self):
"""Tests that the script adds the requested suborg fields for domain requests"""
# Create a new domain request with some errant spacing
custom_suborg_request = completed_domain_request(
name="custom_org.gov",
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
federal_agency=self.executive_agency_2,
user=self.user,
organization_name=" requested org name ",
city="Austin ",
state_territory=DomainRequest.StateTerritoryChoices.TEXAS,
)
self.assertIsNone(custom_suborg_request.requested_suborganization)
self.assertIsNone(custom_suborg_request.suborganization_city)
self.assertIsNone(custom_suborg_request.suborganization_state_territory)
# Run the script and test it
self.run_create_federal_portfolio(branch="executive", parse_requests=True)
custom_suborg_request.refresh_from_db()
self.assertEqual(custom_suborg_request.requested_suborganization, "requested org name")
self.assertEqual(custom_suborg_request.suborganization_city, "Austin")
self.assertEqual(
custom_suborg_request.suborganization_state_territory, DomainRequest.StateTerritoryChoices.TEXAS
)
def test_create_multiple_portfolios_for_branch_executive(self):
"""Tests creating all portfolios under a given branch"""
federal_choice = DomainRequest.OrganizationChoices.FEDERAL

View file

@ -439,35 +439,47 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
username="usertest",
)
self.expiringdomain, _ = Domain.objects.get_or_create(
name="expiringdomain.gov",
self.domaintorenew, _ = Domain.objects.get_or_create(
name="domainrenewal.gov",
)
UserDomainRole.objects.get_or_create(
user=self.user, domain=self.expiringdomain, role=UserDomainRole.Roles.MANAGER
user=self.user, domain=self.domaintorenew, role=UserDomainRole.Roles.MANAGER
)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.expiringdomain)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domaintorenew)
self.portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org", creator=self.user)
self.user.save()
def custom_is_expired(self):
def expiration_date_one_year_out(self):
todays_date = datetime.today()
new_expiration_date = todays_date.replace(year=todays_date.year + 1)
return new_expiration_date
def custom_is_expired_false(self):
return False
def custom_is_expired_true(self):
return True
def custom_is_expiring(self):
return True
def custom_renew_domain(self):
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):
self.client.force_login(self.user)
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
Domain, "is_expired", self.custom_is_expired
Domain, "is_expired", self.custom_is_expired_false
):
self.assertEquals(self.expiringdomain.state, Domain.State.UNKNOWN)
self.assertEquals(self.domaintorenew.state, Domain.State.UNKNOWN)
detail_page = self.client.get(
reverse("domain", kwargs={"pk": self.expiringdomain.id}),
reverse("domain", kwargs={"pk": self.domaintorenew.id}),
)
self.assertContains(detail_page, "Expiring soon")
@ -498,17 +510,17 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
],
)
expiringdomain2, _ = Domain.objects.get_or_create(name="bogusdomain2.gov")
domaintorenew2, _ = Domain.objects.get_or_create(name="bogusdomain2.gov")
DomainInformation.objects.get_or_create(
creator=non_dom_manage_user, domain=expiringdomain2, portfolio=self.portfolio
creator=non_dom_manage_user, domain=domaintorenew2, portfolio=self.portfolio
)
non_dom_manage_user.refresh_from_db()
self.client.force_login(non_dom_manage_user)
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
Domain, "is_expired", self.custom_is_expired
Domain, "is_expired", self.custom_is_expired_false
):
detail_page = self.client.get(
reverse("domain", kwargs={"pk": expiringdomain2.id}),
reverse("domain", kwargs={"pk": domaintorenew2.id}),
)
self.assertContains(detail_page, "Contact one of the listed domain managers to renew the domain.")
@ -517,20 +529,164 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
def test_expiring_domain_on_detail_page_in_org_model_as_a_domain_manager(self):
portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test org2", creator=self.user)
expiringdomain3, _ = Domain.objects.get_or_create(name="bogusdomain3.gov")
domaintorenew3, _ = Domain.objects.get_or_create(name="bogusdomain3.gov")
UserDomainRole.objects.get_or_create(user=self.user, domain=expiringdomain3, role=UserDomainRole.Roles.MANAGER)
DomainInformation.objects.get_or_create(creator=self.user, domain=expiringdomain3, portfolio=portfolio)
UserDomainRole.objects.get_or_create(user=self.user, domain=domaintorenew3, role=UserDomainRole.Roles.MANAGER)
DomainInformation.objects.get_or_create(creator=self.user, domain=domaintorenew3, portfolio=portfolio)
self.user.refresh_from_db()
self.client.force_login(self.user)
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
Domain, "is_expired", self.custom_is_expired
Domain, "is_expired", self.custom_is_expired_false
):
detail_page = self.client.get(
reverse("domain", kwargs={"pk": expiringdomain3.id}),
reverse("domain", kwargs={"pk": domaintorenew3.id}),
)
self.assertContains(detail_page, "Renew to maintain access")
@override_flag("domain_renewal", active=True)
def test_domain_renewal_form_and_sidebar_expiring(self):
self.client.force_login(self.user)
with patch.object(Domain, "is_expiring", self.custom_is_expiring), patch.object(
Domain, "is_expiring", self.custom_is_expiring
):
# Grab the detail page
detail_page = self.client.get(
reverse("domain", kwargs={"pk": self.domaintorenew.id}),
)
# Make sure we see the link as a domain manager
self.assertContains(detail_page, "Renew to maintain access")
# Make sure we can see Renewal form on the sidebar since it's expiring
self.assertContains(detail_page, "Renewal form")
# Grab link to the renewal page
renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domaintorenew.id})
self.assertContains(detail_page, f'href="{renewal_form_url}"')
# Simulate clicking the link
response = self.client.get(renewal_form_url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, f"Renew {self.domaintorenew.name}")
@override_flag("domain_renewal", active=True)
def test_domain_renewal_form_and_sidebar_expired(self):
self.client.force_login(self.user)
with patch.object(Domain, "is_expired", self.custom_is_expired_true), patch.object(
Domain, "is_expired", self.custom_is_expired_true
):
# Grab the detail page
detail_page = self.client.get(
reverse("domain", kwargs={"pk": self.domaintorenew.id}),
)
print("puglesss", self.domaintorenew.is_expired)
# Make sure we see the link as a domain manager
self.assertContains(detail_page, "Renew to maintain access")
# Make sure we can see Renewal form on the sidebar since it's expired
self.assertContains(detail_page, "Renewal form")
# Grab link to the renewal page
renewal_form_url = reverse("domain-renewal", kwargs={"pk": self.domaintorenew.id})
self.assertContains(detail_page, f'href="{renewal_form_url}"')
# Simulate clicking the link
response = self.client.get(renewal_form_url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, f"Renew {self.domaintorenew.name}")
@override_flag("domain_renewal", active=True)
def test_domain_renewal_form_your_contact_info_edit(self):
with less_console_noise():
# Start on the Renewal page for the domain
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
# Verify we see "Your contact information" on the renewal form
self.assertContains(renewal_page, "Your contact information")
# Verify that the "Edit" button for Your contact is there and links to correct URL
edit_button_url = reverse("user-profile")
self.assertContains(renewal_page, f'href="{edit_button_url}"')
# Simulate clicking on edit button
edit_page = renewal_page.click(href=edit_button_url, index=1)
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):
with less_console_noise():
# Start on the Renewal page for the domain
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
# Verify we see "Security email" on the renewal form
self.assertContains(renewal_page, "Security email")
# Verify we see "strong recommend" blurb
self.assertContains(renewal_page, "We strongly recommend that you provide a security email.")
# Verify that the "Edit" button for Security email is there and links to correct URL
edit_button_url = reverse("domain-security-email", kwargs={"pk": self.domain_with_ip.id})
self.assertContains(renewal_page, f'href="{edit_button_url}"')
# Simulate clicking on edit button
edit_page = renewal_page.click(href=edit_button_url, index=1)
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):
with less_console_noise():
# Start on the Renewal page for the domain
renewal_page = self.app.get(reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id}))
# Verify we see "Domain managers" on the renewal form
self.assertContains(renewal_page, "Domain managers")
# Verify that the "Edit" button for Domain managers is there and links to correct URL
edit_button_url = reverse("domain-users", kwargs={"pk": self.domain_with_ip.id})
self.assertContains(renewal_page, f'href="{edit_button_url}"')
# Simulate clicking on edit button
edit_page = renewal_page.click(href=edit_button_url, index=1)
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_ack_checkbox_not_checked(self):
# Grab the renewal URL
renewal_url = reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id})
# Test that the checkbox is not checked
response = self.client.post(renewal_url, data={"submit_button": "next"})
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):
# Grab the renewal URL
with patch.object(Domain, "renew_domain", self.custom_renew_domain):
renewal_url = reverse("domain-renewal", kwargs={"pk": self.domain_with_ip.id})
# Click the check, and submit
response = self.client.post(renewal_url, data={"is_policy_acknowledged": "on", "submit_button": "next"})
# Check that it redirects after a successfully submits
self.assertRedirects(response, reverse("domain", kwargs={"pk": self.domain_with_ip.id}))
# Check for the updated expiration
formatted_new_expiration_date = self.expiration_date_one_year_out().strftime("%b. %-d, %Y")
redirect_response = self.client.get(reverse("domain", kwargs={"pk": self.domain_with_ip.id}), follow=True)
self.assertContains(redirect_response, formatted_new_expiration_date)
class TestDomainManagers(TestDomainOverview):
@classmethod
@ -2660,7 +2816,6 @@ class TestDomainRenewal(TestWithUser):
UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expiring_soon_date).delete()
self.client.force_login(self.user)
domains_page = self.client.get("/")
self.assertNotContains(domains_page, "Expiring soon")
self.assertNotContains(domains_page, "will expire soon")
@less_console_noise_decorator
@ -2698,5 +2853,4 @@ class TestDomainRenewal(TestWithUser):
UserDomainRole.objects.filter(user=self.user, domain=self.domain_with_expiring_soon_date).delete()
self.client.force_login(self.user)
domains_page = self.client.get("/")
self.assertNotContains(domains_page, "Expiring soon")
self.assertNotContains(domains_page, "will expire soon")

View file

@ -14,6 +14,7 @@ from .domain import (
DomainInvitationCancelView,
DomainDeleteUserView,
PrototypeDomainDNSRecordView,
DomainRenewalView,
)
from .user_profile import UserProfileView, FinishProfileSetupView
from .health import *

View file

@ -12,11 +12,11 @@ from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.db import IntegrityError
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.shortcuts import redirect, render, get_object_or_404
from django.urls import reverse
from django.views.generic.edit import FormMixin
from django.conf import settings
from registrar.forms.domain import DomainSuborganizationForm
from registrar.forms.domain import DomainSuborganizationForm, DomainRenewalForm
from registrar.models import (
Domain,
DomainRequest,
@ -311,6 +311,47 @@ class DomainView(DomainBaseView):
self._update_session_with_domain()
class DomainRenewalView(DomainView):
"""Domain detail overview page."""
template_name = "domain_renewal.html"
def post(self, request, pk):
domain = get_object_or_404(Domain, id=pk)
form = DomainRenewalForm(request.POST)
if form.is_valid():
# check for key in the post request data
if "submit_button" in request.POST:
try:
domain.renew_domain()
messages.success(request, "This domain has been renewed for one year.")
except Exception:
messages.error(
request,
"This domain has not been renewed for one year, "
"please email help@get.gov if this problem persists.",
)
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
return render(
request,
"domain_renewal.html",
{
"domain": domain,
"form": form,
"is_editable": True,
"has_domain_renewal_flag": True,
"is_domain_manager": True,
},
)
class DomainOrgNameAddressView(DomainFormBaseView):
"""Organization view"""