Merge branch 'za/2595-update-domain-request-view-only-pages' into za/2596-view-only-domain-request-page

This commit is contained in:
zandercymatics 2024-09-17 13:02:59 -06:00
commit 2012ac0108
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
7 changed files with 248 additions and 42 deletions

View file

@ -11,6 +11,7 @@ from registrar.models.federal_agency import FederalAgency
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
from registrar.utility.constants import BranchChoices from registrar.utility.constants import BranchChoices
from auditlog.models import LogEntry
from .utility.time_stamped_model import TimeStampedModel from .utility.time_stamped_model import TimeStampedModel
from ..utility.email import send_templated_email, EmailSendingError from ..utility.email import send_templated_email, EmailSendingError
@ -576,11 +577,25 @@ class DomainRequest(TimeStampedModel):
verbose_name="last updated on", verbose_name="last updated on",
help_text="Date of the last status update", help_text="Date of the last status update",
) )
notes = models.TextField( notes = models.TextField(
null=True, null=True,
blank=True, blank=True,
) )
def get_first_status_set_date(self, status):
"""Returns the date when the domain request was first set to the given status."""
log_entry = (
LogEntry.objects.filter(content_type__model="domainrequest", object_pk=self.pk, changes__status__1=status)
.order_by("-timestamp")
.first()
)
return log_entry.timestamp.date() if log_entry else None
def get_first_status_started_date(self):
"""Returns the date when the domain request was put into the status "started" for the first time"""
return self.get_first_status_set_date(DomainRequest.DomainRequestStatus.STARTED)
@classmethod @classmethod
def get_statuses_that_send_emails(cls): def get_statuses_that_send_emails(cls):
"""Returns a list of statuses that send an email to the user""" """Returns a list of statuses that send an email to the user"""
@ -1138,6 +1153,11 @@ class DomainRequest(TimeStampedModel):
data[field.name] = field.value_from_object(self) data[field.name] = field.value_from_object(self)
return data return data
def get_formatted_cisa_rep_name(self):
"""Returns the cisa representatives name in Western order."""
names = [n for n in [self.cisa_representative_first_name, self.cisa_representative_last_name] if n]
return " ".join(names) if names else "Unknown"
def _is_federal_complete(self): def _is_federal_complete(self):
# Federal -> "Federal government branch" page can't be empty + Federal Agency selection can't be None # Federal -> "Federal government branch" page can't be empty + Federal Agency selection can't be None
return not (self.federal_type is None or self.federal_agency is None) return not (self.federal_type is None or self.federal_agency is None)

View file

@ -3,6 +3,7 @@
import time import time
import logging import logging
from urllib.parse import urlparse, urlunparse, urlencode from urllib.parse import urlparse, urlunparse, urlencode
from django.urls import resolve, Resolver404
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -315,3 +316,21 @@ def convert_queryset_to_dict(queryset, is_model=True, key="id"):
request_dict = {value[key]: value for value in queryset} request_dict = {value[key]: value for value in queryset}
return request_dict return request_dict
def get_url_name(path):
"""
Given a URL path, returns the corresponding URL name defined in urls.py.
Args:
path (str): The URL path to resolve.
Returns:
str or None: The URL name if it exists, otherwise None.
"""
try:
match = resolve(path)
return match.url_name
except Resolver404:
logger.error(f"No matching URL name found for path: {path}")
return None

View file

@ -8,33 +8,30 @@
{% block content %} {% block content %}
<main id="main-content" class="grid-container"> <main id="main-content" class="grid-container">
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8"> <div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
{% comment %}
TODO: Uncomment in #2596
{% if portfolio %} {% if portfolio %}
{% url 'domain-requests' as url %} {% url 'domain-requests' as url %}
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb"> {% else %}
<ol class="usa-breadcrumb__list"> {% url 'home' as url %}
<li class="usa-breadcrumb__list-item"> {% endif %}
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
<ol class="usa-breadcrumb__list">
<li class="usa-breadcrumb__list-item">
{% if portfolio %}
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain requests</span></a> <a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
</li> {% else %}
<li class="usa-breadcrumb__list-item usa-current" aria-current="page"> <a href="{{ url }}" class="usa-breadcrumb__link"><span>Manage your domains</span></a>
<span>{{ DomainRequest.requested_domain.name }}</span {% endif %}
> </li>
</li> <li class="usa-breadcrumb__list-item usa-current" aria-current="page">
</ol> {% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
</nav> <span>New domain request</span>
{% else %}{% endcomment %} {% else %}
{% url 'home' as url %} <span>{{ DomainRequest.requested_domain.name }}</span>
<a href="{{ url }}" class="breadcrumb__back"> {% endif %}
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img"> </li>
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use> </ol>
</svg> </nav>
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
Back to manage your domains
</p>
</a>
{% comment %} {% endif %}{% endcomment %}
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1> <h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
<div <div
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2" class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
@ -48,25 +45,71 @@
<span class="text-bold text-primary-darker"> <span class="text-bold text-primary-darker">
Status: Status:
</span> </span>
{% if DomainRequest.status == 'approved' %} Approved {{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
{% elif DomainRequest.status == 'in review' %} In review
{% elif DomainRequest.status == 'rejected' %} Rejected
{% elif DomainRequest.status == 'submitted' %} Submitted
{% elif DomainRequest.status == 'ineligible' %} Ineligible
{% else %}ERROR Please contact technical support/dev
{% endif %}
</p> </p>
</div> </div>
</div> </div>
<br> <br>
<p><b class="review__step__name">Last updated:</b> {{DomainRequest.updated_at|date:"F j, Y"}}</p>
{% with statuses=DomainRequest.DomainRequestStatus last_submitted=DomainRequest.last_submitted_date|date:"F j, Y" first_submitted=DomainRequest.first_submitted_date|date:"F j, Y" last_status_update=DomainRequest.last_status_update|date:"F j, Y" %}
{% comment %}
These are intentionally seperated this way.
There is some code repetition, but it gives us more flexibility rather than a dense reduction.
Leave it this way until we've solidified our requirements.
{% endcomment %}
{% if DomainRequest.status == statuses.STARTED %}
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
<p class="margin-top-1">
{% comment %}
A newly created domain request will not have a value for last_status update.
This is because the status never really updated.
However, if this somehow goes back to started we can default to displaying that new date.
{% endcomment %}
<b class="review__step__name">Started on:</b> {{last_status_update|default:first_started_date}}
</p>
{% endwith %}
{% elif DomainRequest.status == statuses.SUBMITTED %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
</p>
{% elif DomainRequest.status == statuses.ACTION_NEEDED %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
</p>
{% elif DomainRequest.status == statuses.REJECTED %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Rejected on:</b> {{last_status_update}}
</p>
{% elif DomainRequest.status == statuses.WITHDRAWN %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Withdrawn on:</b> {{last_status_update}}
</p>
{% else %}
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
<p class="margin-top-1">
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
</p>
{% endif %}
{% if DomainRequest.status != 'rejected' %} {% if DomainRequest.status != 'rejected' %}
<p>{% include "includes/domain_request.html" %}</p> <p>{% include "includes/domain_request.html" %}</p>
<p><a href="{% url 'domain-request-withdraw-confirmation' pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline"> <p><a href="{% url 'domain-request-withdraw-confirmation' pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
Withdraw request</a> Withdraw request</a>
</p> </p>
{% endif %} {% endif %}
{% endwith %}
</div> </div>
<div class="grid-col desktop:grid-offset-2 maxw-tablet"> <div class="grid-col desktop:grid-offset-2 maxw-tablet">
@ -141,8 +184,8 @@
{% if DomainRequest %} {% if DomainRequest %}
<h3 class="register-form-review-header">CISA Regional Representative</h3> <h3 class="register-form-review-header">CISA Regional Representative</h3>
<ul class="usa-list usa-list--unstyled margin-top-0"> <ul class="usa-list usa-list--unstyled margin-top-0">
{% if domain_request.cisa_representative_first_name %} {% if DomainRequest.cisa_representative_first_name %}
{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}} {{ DomainRequest.get_formatted_cisa_rep_name }}
{% else %} {% else %}
No No
{% endif %} {% endif %}

View file

@ -42,7 +42,7 @@
{% else %} {% else %}
{% url 'no-portfolio-domains' as url %} {% url 'no-portfolio-domains' as url %}
{% endif %} {% endif %}
<a href="{{ url }}" class="usa-nav-link{% if 'domain'|in_path:request.path %} usa-current{% endif %}"> <a href="{{ url }}" class="usa-nav-link{% if path|is_domain_subpage %} usa-current{% endif %}">
Domains Domains
</a> </a>
</li> </li>
@ -59,7 +59,7 @@
{% url 'domain-requests' as url %} {% url 'domain-requests' as url %}
<button <button
type="button" type="button"
class="usa-accordion__button usa-nav__link{% if 'request'|in_path:request.path %} usa-current{% endif %}" class="usa-accordion__button usa-nav__link{% if path|is_domain_request_subpage %} usa-current{% endif %}"
aria-expanded="false" aria-expanded="false"
aria-controls="basic-nav-section-two" aria-controls="basic-nav-section-two"
> >
@ -80,13 +80,13 @@
<!-- user has view but no edit permissions --> <!-- user has view but no edit permissions -->
{% elif has_any_requests_portfolio_permission %} {% elif has_any_requests_portfolio_permission %}
{% url 'domain-requests' as url %} {% url 'domain-requests' as url %}
<a href="{{ url }}" class="usa-nav-link{% if 'request'|in_path:request.path %} usa-current{% endif %}"> <a href="{{ url }}" class="usa-nav-link{% if path|is_domain_request_subpage %} usa-current{% endif %}">
Domain requests Domain requests
</a> </a>
<!-- user does not have permissions --> <!-- user does not have permissions -->
{% else %} {% else %}
{% url 'no-portfolio-requests' as url %} {% url 'no-portfolio-requests' as url %}
<a href="{{ url }}" class="usa-nav-link{% if 'request'|in_path:request.path %} usa-current{% endif %}"> <a href="{{ url }}" class="usa-nav-link{% if path|is_domain_request_subpage %} usa-current{% endif %}">
Domain requests Domain requests
</a> </a>
{% endif %} {% endif %}
@ -104,7 +104,7 @@
<li class="usa-nav__primary-item"> <li class="usa-nav__primary-item">
{% url 'organization' as url %} {% url 'organization' as url %}
<!-- Move the padding from the a to the span so that the descenders do not get cut off --> <!-- Move the padding from the a to the span so that the descenders do not get cut off -->
<a href="{{ url }}" class="usa-nav-link padding-y-0 {% if request.path == '/organization/' %} usa-current{% endif %}"> <a href="{{ url }}" class="usa-nav-link padding-y-0 {% if path|is_portfolio_subpage %} usa-current{% endif %}">
<span class="ellipsis ellipsis--23 ellipsis--desktop-50 padding-y-1 desktop:padding-y-2"> <span class="ellipsis ellipsis--23 ellipsis--desktop-50 padding-y-1 desktop:padding-y-2">
{{ portfolio.organization_name }} {{ portfolio.organization_name }}
</span> </span>

View file

@ -3,6 +3,9 @@ from django import template
import re import re
from registrar.models.domain_request import DomainRequest from registrar.models.domain_request import DomainRequest
from phonenumber_field.phonenumber import PhoneNumber from phonenumber_field.phonenumber import PhoneNumber
from registrar.views.domain_request import DomainRequestWizard
from registrar.models.utility.generic_helper import get_url_name
register = template.Library() register = template.Library()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -174,3 +177,65 @@ def has_contact_info(user):
@register.filter @register.filter
def model_name_lowercase(instance): def model_name_lowercase(instance):
return instance.__class__.__name__.lower() return instance.__class__.__name__.lower()
@register.filter(name="is_domain_subpage")
def is_domain_subpage(path):
"""Checks if the given page is a subpage of domains.
Takes a path name, like '/domains/'."""
# Since our pages aren't unified under a common path, we need this approach for now.
url_names = [
"domains",
"no-portfolio-domains",
"domain",
"domain-users",
"domain-dns",
"domain-dns-nameservers",
"domain-dns-dnssec",
"domain-dns-dnssec-dsdata",
"domain-your-contact-information",
"domain-org-name-address",
"domain-senior-official",
"domain-security-email",
"domain-users-add",
"domain-request-delete",
"domain-user-delete",
"invitation-delete",
]
return get_url_name(path) in url_names
@register.filter(name="is_domain_request_subpage")
def is_domain_request_subpage(path):
"""Checks if the given page is a subpage of domain requests.
Takes a path name, like '/requests/'."""
# Since our pages aren't unified under a common path, we need this approach for now.
url_names = [
"domain-requests",
"no-portfolio-requests",
"domain-request-status",
"domain-request-withdraw-confirmation",
"domain-request-withdrawn",
"domain-request-delete",
]
# The domain request wizard pages don't have a defined path,
# so we need to check directly on it.
wizard_paths = [
DomainRequestWizard.EDIT_URL_NAME,
DomainRequestWizard.URL_NAMESPACE,
DomainRequestWizard.NEW_URL_NAME,
]
return get_url_name(path) in url_names or any(wizard in path for wizard in wizard_paths)
@register.filter(name="is_portfolio_subpage")
def is_portfolio_subpage(path):
"""Checks if the given page is a subpage of portfolio.
Takes a path name, like '/organization/'."""
# Since our pages aren't unified under a common path, we need this approach for now.
url_names = [
"organization",
"senior-official",
]
return get_url_name(path) in url_names

View file

@ -9,6 +9,9 @@ from registrar.templatetags.custom_filters import (
find_index, find_index,
slice_after, slice_after,
contains_checkbox, contains_checkbox,
is_domain_request_subpage,
is_domain_subpage,
is_portfolio_subpage,
) )
@ -90,3 +93,18 @@ class CustomFiltersTestCase(TestCase):
] ]
result = contains_checkbox(html_list) result = contains_checkbox(html_list)
self.assertFalse(result) # Expecting False self.assertFalse(result) # Expecting False
def test_is_domain_subpage(self):
"""Tests if the path is recognized as a domain subpage."""
self.assertTrue(is_domain_subpage("/domains/"))
self.assertFalse(is_domain_subpage("/"))
def test_is_domain_request_subpage(self):
"""Tests if the path is recognized as a domain request subpage."""
self.assertTrue(is_domain_request_subpage("/requests/"))
self.assertFalse(is_domain_request_subpage("/"))
def test_is_portfolio_subpage(self):
"""Tests if the path is recognized as a portfolio subpage."""
self.assertTrue(is_portfolio_subpage("/organization/"))
self.assertFalse(is_portfolio_subpage("/"))

View file

@ -1,6 +1,7 @@
from unittest import skip from unittest import skip
from unittest.mock import Mock from unittest.mock import Mock, patch
from datetime import datetime
from django.utils import timezone
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
from api.tests.common import less_console_noise_decorator from api.tests.common import less_console_noise_decorator
@ -56,6 +57,46 @@ class DomainRequestTests(TestWithUser, WebTest):
intro_page = self.app.get(reverse("domain-request:")) intro_page = self.app.get(reverse("domain-request:"))
self.assertContains(intro_page, "Youre about to start your .gov domain request") self.assertContains(intro_page, "Youre about to start your .gov domain request")
@less_console_noise_decorator
def test_template_status_display(self):
"""Tests the display of status-related information in the template."""
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
domain_request.last_submitted_date = datetime.now()
domain_request.save()
response = self.app.get(f"/domain-request/{domain_request.id}")
self.assertContains(response, "Submitted on:")
self.assertContains(response, domain_request.last_submitted_date.strftime("%B %-d, %Y"))
@patch.object(DomainRequest, "get_first_status_set_date")
def test_get_first_status_started_date(self, mock_get_first_status_set_date):
"""Tests retrieval of the first date the status was set to 'started'."""
# Set the mock to return a fixed date
fixed_date = timezone.datetime(2023, 1, 1).date()
mock_get_first_status_set_date.return_value = fixed_date
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED, user=self.user)
domain_request.last_status_update = None
domain_request.save()
response = self.app.get(f"/domain-request/{domain_request.id}")
# Ensure that the date is still set to None
self.assertIsNone(domain_request.last_status_update)
print(response)
# We should still grab a date for this field in this event - but it should come from the audit log instead
self.assertContains(response, "Started on:")
self.assertContains(response, fixed_date.strftime("%B %-d, %Y"))
# If a status date is set, we display that instead
domain_request.last_status_update = datetime.now()
domain_request.save()
response = self.app.get(f"/domain-request/{domain_request.id}")
# We should still grab a date for this field in this event - but it should come from the audit log instead
self.assertContains(response, "Started on:")
self.assertContains(response, domain_request.last_status_update.strftime("%B %-d, %Y"))
@less_console_noise_decorator @less_console_noise_decorator
def test_domain_request_form_intro_is_skipped_when_edit_access(self): def test_domain_request_form_intro_is_skipped_when_edit_access(self):
"""Tests that user is NOT presented with intro acknowledgement page when accessed through 'edit'""" """Tests that user is NOT presented with intro acknowledgement page when accessed through 'edit'"""