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

This commit is contained in:
zandercymatics 2024-09-19 11:15:04 -06:00
commit 999ed2f2e5
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
64 changed files with 2077 additions and 1884 deletions

View file

@ -42,7 +42,6 @@ class DomainRequest {
creator (User)
investigator (User)
senior_official (Contact)
submitter (Contact)
other_contacts (Contacts)
approved_domain (Domain)
requested_domain (DraftDomain)
@ -80,7 +79,7 @@ class Contact {
--
}
DomainRequest *-r-* Contact : senior_official, submitter, other_contacts
DomainRequest *-r-* Contact : senior_official, other_contacts
class DraftDomain {
Requested domain

View file

@ -38,7 +38,6 @@ class "registrar.Contact <Registrar>" as registrar.Contact #d6f4e9 {
+ id (BigAutoField)
+ created_at (DateTimeField)
+ updated_at (DateTimeField)
~ user (OneToOneField)
+ first_name (CharField)
+ middle_name (CharField)
+ last_name (CharField)
@ -47,7 +46,6 @@ class "registrar.Contact <Registrar>" as registrar.Contact #d6f4e9 {
+ phone (PhoneNumberField)
--
}
registrar.Contact -- registrar.User
class "registrar.Host <Registrar>" as registrar.Host #d6f4e9 {
@ -143,6 +141,8 @@ class "registrar.FederalAgency <Registrar>" as registrar.FederalAgency #d6f4e9 {
+ updated_at (DateTimeField)
+ agency (CharField)
+ federal_type (CharField)
+ initials (CharField)
+ is_fceb (BooleanField)
--
}
@ -159,6 +159,7 @@ class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
+ action_needed_reason_email (TextField)
~ federal_agency (ForeignKey)
~ portfolio (ForeignKey)
~ sub_organization (ForeignKey)
~ creator (ForeignKey)
~ investigator (ForeignKey)
+ generic_org_type (CharField)
@ -179,7 +180,6 @@ class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
~ senior_official (ForeignKey)
~ approved_domain (OneToOneField)
~ requested_domain (OneToOneField)
~ submitter (ForeignKey)
+ purpose (TextField)
+ no_other_contacts_rationale (TextField)
+ anything_else (TextField)
@ -198,12 +198,12 @@ class "registrar.DomainRequest <Registrar>" as registrar.DomainRequest #d6f4e9 {
}
registrar.DomainRequest -- registrar.FederalAgency
registrar.DomainRequest -- registrar.Portfolio
registrar.DomainRequest -- registrar.Suborganization
registrar.DomainRequest -- registrar.User
registrar.DomainRequest -- registrar.User
registrar.DomainRequest -- registrar.Contact
registrar.DomainRequest -- registrar.Domain
registrar.DomainRequest -- registrar.DraftDomain
registrar.DomainRequest -- registrar.Contact
registrar.DomainRequest *--* registrar.Website
registrar.DomainRequest *--* registrar.Website
registrar.DomainRequest *--* registrar.Contact
@ -218,6 +218,7 @@ class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #
~ federal_agency (ForeignKey)
~ creator (ForeignKey)
~ portfolio (ForeignKey)
~ sub_organization (ForeignKey)
~ domain_request (OneToOneField)
+ generic_org_type (CharField)
+ organization_type (CharField)
@ -236,7 +237,6 @@ class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #
+ about_your_organization (TextField)
~ senior_official (ForeignKey)
~ domain (OneToOneField)
~ submitter (ForeignKey)
+ purpose (TextField)
+ no_other_contacts_rationale (TextField)
+ anything_else (TextField)
@ -253,10 +253,10 @@ class "registrar.DomainInformation <Registrar>" as registrar.DomainInformation #
registrar.DomainInformation -- registrar.FederalAgency
registrar.DomainInformation -- registrar.User
registrar.DomainInformation -- registrar.Portfolio
registrar.DomainInformation -- registrar.Suborganization
registrar.DomainInformation -- registrar.DomainRequest
registrar.DomainInformation -- registrar.Contact
registrar.DomainInformation -- registrar.Domain
registrar.DomainInformation -- registrar.Contact
registrar.DomainInformation *--* registrar.Contact
@ -285,6 +285,38 @@ class "registrar.DomainInvitation <Registrar>" as registrar.DomainInvitation #d6
registrar.DomainInvitation -- registrar.Domain
class "registrar.UserPortfolioPermission <Registrar>" as registrar.UserPortfolioPermission #d6f4e9 {
user portfolio permission
--
+ id (BigAutoField)
+ created_at (DateTimeField)
+ updated_at (DateTimeField)
~ user (ForeignKey)
~ portfolio (ForeignKey)
+ roles (ArrayField)
+ additional_permissions (ArrayField)
--
}
registrar.UserPortfolioPermission -- registrar.User
registrar.UserPortfolioPermission -- registrar.Portfolio
class "registrar.PortfolioInvitation <Registrar>" as registrar.PortfolioInvitation #d6f4e9 {
portfolio invitation
--
+ id (BigAutoField)
+ created_at (DateTimeField)
+ updated_at (DateTimeField)
+ email (EmailField)
~ portfolio (ForeignKey)
+ portfolio_roles (ArrayField)
+ portfolio_additional_permissions (ArrayField)
+ status (FSMField)
--
}
registrar.PortfolioInvitation -- registrar.Portfolio
class "registrar.TransitionDomain <Registrar>" as registrar.TransitionDomain #d6f4e9 {
transition domain
--
@ -409,10 +441,11 @@ class "registrar.Portfolio <Registrar>" as registrar.Portfolio #d6f4e9 {
+ created_at (DateTimeField)
+ updated_at (DateTimeField)
~ creator (ForeignKey)
+ organization_name (CharField)
+ organization_type (CharField)
+ notes (TextField)
~ federal_agency (ForeignKey)
+ organization_type (CharField)
+ organization_name (CharField)
~ senior_official (ForeignKey)
+ address_line1 (CharField)
+ address_line2 (CharField)
+ city (CharField)
@ -424,6 +457,7 @@ class "registrar.Portfolio <Registrar>" as registrar.Portfolio #d6f4e9 {
}
registrar.Portfolio -- registrar.User
registrar.Portfolio -- registrar.FederalAgency
registrar.Portfolio -- registrar.SeniorOfficial
class "registrar.DomainGroup <Registrar>" as registrar.DomainGroup #d6f4e9 {
@ -454,7 +488,21 @@ class "registrar.Suborganization <Registrar>" as registrar.Suborganization #d6f4
registrar.Suborganization -- registrar.Portfolio
@enduml
```
class "registrar.SeniorOfficial <Registrar>" as registrar.SeniorOfficial #d6f4e9 {
senior official
--
+ id (BigAutoField)
+ created_at (DateTimeField)
+ updated_at (DateTimeField)
+ first_name (CharField)
+ last_name (CharField)
+ title (CharField)
+ phone (PhoneNumberField)
+ email (EmailField)
~ federal_agency (ForeignKey)
--
}
registrar.SeniorOfficial -- registrar.FederalAgency
</details>
@enduml

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Before After
Before After

View file

@ -9,10 +9,10 @@ We use [django-waffle](https://waffle.readthedocs.io/en/stable/) for our feature
3. Click `Add waffle flag`.
4. Add the model as you would normally. Refer to waffle's documentation [regarding attributes](https://waffle.readthedocs.io/en/stable/types/flag.html#flag-attributes) for more information on them.
### Enabling the profile_feature flag
### Enabling a feature flag
1. On the app, navigate to `\admin`.
2. Under models, click `Waffle flags`.
3. Click the `profile_feature` record. This should exist by default, if not - create one with that name.
3. Click the featue flag record. This should exist by default, if not - create one with that name.
4. (Important) Set the field `Everyone` to `Unknown`. This field overrides all other settings when set to anything else.
5. Configure the settings as you see fit.

View file

@ -14,7 +14,7 @@
- Starting Location: Home page
- Workflow: (Domain requests Table) Manage domain
- Workflow Step: Click "Manage" -> Click "Withdraw request" -> (confirmation prompt) -> Click "Withdraw request" (inside prompt)
- Notes: You can also do this through Django Admin by switching a domain of status "submitted" to "withdrawn", but you need to be the submitter (email listed on Your Contact Information).
- Notes: You can also do this through Django Admin by switching a domain of status "submitted" to "withdrawn", but you need to be the creator.
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/domain_request_withdrawn.txt)
### Domain Request Withdrawn Subject
@ -25,7 +25,7 @@
- Starting Location: Django Admin
- Workflow: Analyst Admin
- Workflow Step: Click "domain requests" -> Click a domain request in a status of "submitted", "In review", "rejected", or "ineligible" -> Click status dropdown -> (select "approved") -> click "Save"
- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information). To test this with your own email, you need to create a domain request, then set the status to "approved". This will send you an email.
- Notes: Note that this will send an email to the creator. To test this with your own email, you need to create a domain request, then set the status to "approved". This will send you an email.
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_approved.txt)
### Status Change Approved Subject
@ -36,7 +36,7 @@
- Starting Location: Django Admin
- Workflow: Analyst Admin
- Workflow Step: Click "domain requests" -> Click a domain request in a status of "In review", or "approved" -> Click status dropdown -> (select "rejected") -> click "Save"
- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information). To test this with your own email, you need to create a domain request, then set the status to "in review" (and click save). Then, go back to the same application and set the status to "rejected". This will send you an email.
- Notes: Note that this will send an email to the creator. To test this with your own email, you need to create a domain request, then set the status to "in review" (and click save). Then, go back to the same application and set the status to "rejected". This will send you an email.
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/status_change_rejected.txt)
### Status Change Rejected Subject
@ -47,7 +47,7 @@
- Starting Location: Home Page
- Workflow: Start domain request
- Workflow Step: Click "Start a new domain request" -> (fill out the form) -> On the last step ("Review and submit your domain request "), click "Submit your domain request"
- Notes: Note that this will send an email to the submitter (email listed on Your Contact Information)
- Notes: Note that this will send an email to the creator.
- [Email Content](https://github.com/cisagov/manage.get.gov/blob/main/src/registrar/templates/emails/submission_confirmation.txt)
### Submission Confirmation Subject

View file

@ -126,7 +126,15 @@ class AvailableAPITest(MockEppLib):
def setUp(self):
super().setUp()
self.user = get_user_model().objects.create(username="username")
username = "test_user"
first_name = "First"
last_name = "Last"
email = "info@example.com"
title = "title"
phone = "8080102431"
self.user = get_user_model().objects.create(
username=username, title=title, first_name=first_name, last_name=last_name, email=email, phone=phone
)
def test_available_get(self):
self.client.force_login(self.user)

View file

@ -536,7 +536,6 @@ class AdminSortFields:
sort_mapping = {
# == Contact == #
"other_contacts": (Contact, _name_sort),
"submitter": (Contact, _name_sort),
# == Senior Official == #
"senior_official": (SeniorOfficial, _name_sort),
# == User == #
@ -1443,13 +1442,9 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"domain",
"generic_org_type",
"created_at",
"submitter",
]
orderable_fk_fields = [
("domain", "name"),
("submitter", ["first_name", "last_name"]),
]
orderable_fk_fields = [("domain", "name")]
# Filters
list_filter = ["generic_org_type"]
@ -1461,7 +1456,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
search_help_text = "Search by domain."
fieldsets = [
(None, {"fields": ["portfolio", "sub_organization", "creator", "submitter", "domain_request", "notes"]}),
(None, {"fields": ["portfolio", "sub_organization", "creator", "domain_request", "notes"]}),
(".gov domain", {"fields": ["domain"]}),
("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}),
("Background info", {"fields": ["anything_else"]}),
@ -1525,7 +1520,6 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"more_organization_information",
"domain",
"domain_request",
"submitter",
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
@ -1540,7 +1534,6 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"domain_request",
"senior_official",
"domain",
"submitter",
"portfolio",
"sub_organization",
]
@ -1713,13 +1706,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"custom_election_board",
"city",
"state_territory",
"submitter",
"investigator",
]
orderable_fk_fields = [
("requested_domain", "name"),
("submitter", ["first_name", "last_name"]),
("investigator", ["first_name", "last_name"]),
]
@ -1749,11 +1740,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# Search
search_fields = [
"requested_domain__name",
"submitter__email",
"submitter__first_name",
"submitter__last_name",
"creator__email",
"creator__first_name",
"creator__last_name",
]
search_help_text = "Search by domain or submitter."
search_help_text = "Search by domain or creator."
fieldsets = [
(
@ -1769,7 +1760,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"action_needed_reason_email",
"investigator",
"creator",
"submitter",
"approved_domain",
"notes",
]
@ -1857,7 +1847,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"approved_domain",
"alternative_domains",
"purpose",
"submitter",
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
@ -1868,7 +1857,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
autocomplete_fields = [
"approved_domain",
"requested_domain",
"submitter",
"creator",
"senior_official",
"investigator",
@ -1989,15 +1977,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
so we should display that information using this function.
"""
# TODO 2574: remove lines 1977-1978 (refactor as needed)
profile_flag = flag_is_active(request, "profile_feature")
if profile_flag and hasattr(obj, "creator"):
recipient = obj.creator
elif not profile_flag and hasattr(obj, "submitter"):
recipient = obj.submitter
else:
recipient = None
# Displays a warning in admin when an email cannot be sent
if recipient and recipient.email:
@ -2202,7 +2182,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
emails = self.get_all_action_needed_reason_emails(obj)
extra_context["action_needed_reason_emails"] = json.dumps(emails)
extra_context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
# Denote if an action needed email was sent or not
email_sent = request.session.get("action_needed_email_sent", False)
@ -2231,10 +2210,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
if not action_needed_reason or action_needed_reason == DomainRequest.ActionNeededReasons.OTHER:
return None
if flag_is_active(None, "profile_feature"): # type: ignore
recipient = domain_request.creator
else:
recipient = domain_request.submitter
# Return the context of the rendered views
context = {"domain_request": domain_request, "recipient": recipient}

View file

@ -33,6 +33,14 @@ const showElement = (element) => {
element.classList.remove('display-none');
};
/**
* Helper function to get the CSRF token from the cookie
*
*/
function getCsrfToken() {
return document.querySelector('input[name="csrfmiddlewaretoken"]').value;
}
/**
* Helper function that scrolls to an element
* @param {string} attributeName - The string "class" or "id"
@ -994,40 +1002,79 @@ function unloadModals() {
window.modal.off();
}
class LoadTableBase {
constructor(tableSelector, tableWrapperSelector, searchFieldId, searchSubmitId, resetSearchBtn, resetFiltersBtn, noDataDisplay, noSearchresultsDisplay) {
this.tableWrapper = document.querySelector(tableWrapperSelector);
this.tableHeaders = document.querySelectorAll(`${tableSelector} th[data-sortable]`);
this.currentSortBy = 'id';
this.currentOrder = 'asc';
this.currentStatus = [];
this.currentSearchTerm = '';
this.scrollToTable = false;
this.searchInput = document.querySelector(searchFieldId);
this.searchSubmit = document.querySelector(searchSubmitId);
this.tableAnnouncementRegion = document.querySelector(`${tableWrapperSelector} .usa-table__announcement-region`);
this.resetSearchButton = document.querySelector(resetSearchBtn);
this.resetFiltersButton = document.querySelector(resetFiltersBtn);
// NOTE: these 3 can't be used if filters are active on a page with more than 1 table
this.statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
this.statusIndicator = document.querySelector('.filter-indicator');
this.statusToggle = document.querySelector('.usa-button--filter');
this.noTableWrapper = document.querySelector(noDataDisplay);
this.noSearchResultsWrapper = document.querySelector(noSearchresultsDisplay);
this.portfolioElement = document.getElementById('portfolio-js-value');
this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null;
this.initializeTableHeaders();
this.initializeSearchHandler();
this.initializeStatusToggleHandler();
this.initializeFilterCheckboxes();
this.initializeResetSearchButton();
this.initializeResetFiltersButton();
this.initializeAccordionAccessibilityListeners();
}
/**
* Generalized function to update pagination for a list.
* @param {string} itemName - The name displayed in the counter
* @param {string} paginationSelector - CSS selector for the pagination container.
* @param {string} counterSelector - CSS selector for the pagination counter.
* @param {string} linkAnchor - CSS selector for the header element to anchor the links to.
* @param {Function} loadPageFunction - Function to call when a page link is clicked.
* @param {string} tableSelector - CSS selector for the header element to anchor the links to.
* @param {number} currentPage - The current page number (starting with 1).
* @param {number} numPages - The total number of pages.
* @param {boolean} hasPrevious - Whether there is a page before the current page.
* @param {boolean} hasNext - Whether there is a page after the current page.
* @param {number} totalItems - The total number of items.
* @param {string} searchTerm - The search term
* @param {number} total - The total number of items.
*/
function updatePagination(itemName, paginationSelector, counterSelector, linkAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) {
const paginationContainer = document.querySelector(paginationSelector);
const paginationCounter = document.querySelector(counterSelector);
updatePagination(
itemName,
paginationSelector,
counterSelector,
parentTableSelector,
currentPage,
numPages,
hasPrevious,
hasNext,
totalItems,
) {
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
paginationCounter.innerHTML = '';
const counterSelectorEl = document.querySelector(counterSelector);
const paginationSelectorEl = document.querySelector(paginationSelector);
counterSelectorEl.innerHTML = '';
paginationButtons.innerHTML = '';
// Buttons should only be displayed if there are more than one pages of results
paginationButtons.classList.toggle('display-none', numPages <= 1);
// Counter should only be displayed if there is more than 1 item
paginationContainer.classList.toggle('display-none', totalItems < 1);
paginationSelectorEl.classList.toggle('display-none', totalItems < 1);
paginationCounter.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${searchTerm ? ' for ' + '"' + searchTerm + '"' : ''}`;
counterSelectorEl.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`;
if (hasPrevious) {
const prevPageItem = document.createElement('li');
prevPageItem.className = 'usa-pagination__item usa-pagination__arrow';
prevPageItem.innerHTML = `
<a href="${linkAnchor}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page">
<a href="${parentTableSelector}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page">
<svg class="usa-icon" aria-hidden="true" role="img">
<use xlink:href="/public/img/sprite.svg#navigate_before"></use>
</svg>
@ -1036,32 +1083,14 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
`;
prevPageItem.querySelector('a').addEventListener('click', (event) => {
event.preventDefault();
loadPageFunction(currentPage - 1);
this.loadTable(currentPage - 1);
});
paginationButtons.appendChild(prevPageItem);
}
// Helper function to create a page item
function createPageItem(page) {
const pageItem = document.createElement('li');
pageItem.className = 'usa-pagination__item usa-pagination__page-no';
pageItem.innerHTML = `
<a href="${linkAnchor}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
`;
if (page === currentPage) {
pageItem.querySelector('a').classList.add('usa-current');
pageItem.querySelector('a').setAttribute('aria-current', 'page');
}
pageItem.querySelector('a').addEventListener('click', (event) => {
event.preventDefault();
loadPageFunction(page);
});
return pageItem;
}
// Add first page and ellipsis if necessary
if (currentPage > 2) {
paginationButtons.appendChild(createPageItem(1));
paginationButtons.appendChild(this.createPageItem(1, parentTableSelector, currentPage));
if (currentPage > 3) {
const ellipsis = document.createElement('li');
ellipsis.className = 'usa-pagination__item usa-pagination__overflow';
@ -1073,7 +1102,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
// Add pages around the current page
for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) {
paginationButtons.appendChild(createPageItem(i));
paginationButtons.appendChild(this.createPageItem(i, parentTableSelector, currentPage));
}
// Add last page and ellipsis if necessary
@ -1085,14 +1114,14 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
ellipsis.innerHTML = '<span>…</span>';
paginationButtons.appendChild(ellipsis);
}
paginationButtons.appendChild(createPageItem(numPages));
paginationButtons.appendChild(this.createPageItem(numPages, parentTableSelector, currentPage));
}
if (hasNext) {
const nextPageItem = document.createElement('li');
nextPageItem.className = 'usa-pagination__item usa-pagination__arrow';
nextPageItem.innerHTML = `
<a href="${linkAnchor}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page">
<a href="${parentTableSelector}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page">
<span class="usa-pagination__link-text">Next</span>
<svg class="usa-icon" aria-hidden="true" role="img">
<use xlink:href="/public/img/sprite.svg#navigate_next"></use>
@ -1101,7 +1130,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
`;
nextPageItem.querySelector('a').addEventListener('click', (event) => {
event.preventDefault();
loadPageFunction(currentPage + 1);
this.loadTable(currentPage + 1);
});
paginationButtons.appendChild(nextPageItem);
}
@ -1111,7 +1140,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
* A helper that toggles content/ no content/ no search results
*
*/
const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
const { unfiltered_total, total } = data;
if (unfiltered_total) {
if (total) {
@ -1130,11 +1159,29 @@ const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper)
}
};
// Helper function to create a page item
createPageItem(page, parentTableSelector, currentPage) {
const pageItem = document.createElement('li');
pageItem.className = 'usa-pagination__item usa-pagination__page-no';
pageItem.innerHTML = `
<a href="${parentTableSelector}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
`;
if (page === currentPage) {
pageItem.querySelector('a').classList.add('usa-current');
pageItem.querySelector('a').setAttribute('aria-current', 'page');
}
pageItem.querySelector('a').addEventListener('click', (event) => {
event.preventDefault();
this.loadTable(page);
});
return pageItem;
}
/**
* A helper that resets sortable table headers
*
*/
const unsetHeader = (header) => {
unsetHeader = (header) => {
header.removeAttribute('aria-sort');
let headerName = header.innerText;
const headerLabel = `${headerName}, sortable column, currently unsorted"`;
@ -1143,34 +1190,181 @@ const unsetHeader = (header) => {
header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel);
};
/**
* An IIFE that listens for DOM Content to be loaded, then executes. This function
* initializes the domains list and associated functionality on the home page of the app.
*
*/
document.addEventListener('DOMContentLoaded', function() {
const domainsWrapper = document.querySelector('.domains__table-wrapper');
// Abstract method (to be implemented in the child class)
loadTable(page, sortBy, order) {
throw new Error('loadData() must be implemented in a subclass');
}
if (domainsWrapper) {
let currentSortBy = 'id';
let currentOrder = 'asc';
const noDomainsWrapper = document.querySelector('.domains__no-data');
const noSearchResultsWrapper = document.querySelector('.domains__no-search-results');
let scrollToTable = false;
let currentStatus = [];
let currentSearchTerm = '';
const domainsSearchInput = document.getElementById('domains__search-field');
const domainsSearchSubmit = document.getElementById('domains__search-field-submit');
const tableHeaders = document.querySelectorAll('.domains__table th[data-sortable]');
const tableAnnouncementRegion = document.querySelector('.domains__table-wrapper .usa-table__announcement-region');
const resetSearchButton = document.querySelector('.domains__reset-search');
const resetFiltersButton = document.querySelector('.domains__reset-filters');
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
const statusIndicator = document.querySelector('.domain__filter-indicator');
const statusToggle = document.querySelector('.usa-button--filter');
const portfolioElement = document.getElementById('portfolio-js-value');
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
// Add event listeners to table headers for sorting
initializeTableHeaders() {
this.tableHeaders.forEach(header => {
header.addEventListener('click', () => {
const sortBy = header.getAttribute('data-sortable');
let order = 'asc';
// sort order will be ascending, unless the currently sorted column is ascending, and the user
// is selecting the same column to sort in descending order
if (sortBy === this.currentSortBy) {
order = this.currentOrder === 'asc' ? 'desc' : 'asc';
}
// load the results with the updated sort
this.loadTable(1, sortBy, order);
});
});
}
initializeSearchHandler() {
this.searchSubmit.addEventListener('click', (e) => {
e.preventDefault();
this.currentSearchTerm = this.searchInput.value;
// If the search is blank, we match the resetSearch functionality
if (this.currentSearchTerm) {
showElement(this.resetSearchButton);
} else {
hideElement(this.resetSearchButton);
}
this.loadTable(1, 'id', 'asc');
this.resetHeaders();
});
}
initializeStatusToggleHandler() {
if (this.statusToggle) {
this.statusToggle.addEventListener('click', () => {
toggleCaret(this.statusToggle);
});
}
}
// Add event listeners to status filter checkboxes
initializeFilterCheckboxes() {
this.statusCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', () => {
const checkboxValue = checkbox.value;
// Update currentStatus array based on checkbox state
if (checkbox.checked) {
this.currentStatus.push(checkboxValue);
} else {
const index = this.currentStatus.indexOf(checkboxValue);
if (index > -1) {
this.currentStatus.splice(index, 1);
}
}
// Manage visibility of reset filters button
if (this.currentStatus.length == 0) {
hideElement(this.resetFiltersButton);
} else {
showElement(this.resetFiltersButton);
}
// Disable the auto scroll
this.scrollToTable = false;
// Call loadTable with updated status
this.loadTable(1, 'id', 'asc');
this.resetHeaders();
this.updateStatusIndicator();
});
});
}
// Reset UI and accessibility
resetHeaders() {
this.tableHeaders.forEach(header => {
// Unset sort UI in headers
this.unsetHeader(header);
});
// Reset the announcement region
this.tableAnnouncementRegion.innerHTML = '';
}
resetSearch() {
this.searchInput.value = '';
this.currentSearchTerm = '';
hideElement(this.resetSearchButton);
this.loadTable(1, 'id', 'asc');
this.resetHeaders();
}
initializeResetSearchButton() {
if (this.resetSearchButton) {
this.resetSearchButton.addEventListener('click', () => {
this.resetSearch();
});
}
}
resetFilters() {
this.currentStatus = [];
this.statusCheckboxes.forEach(checkbox => {
checkbox.checked = false;
});
hideElement(this.resetFiltersButton);
// Disable the auto scroll
this.scrollToTable = false;
this.loadTable(1, 'id', 'asc');
this.resetHeaders();
this.updateStatusIndicator();
// No need to toggle close the filters. The focus shift will trigger that for us.
}
initializeResetFiltersButton() {
if (this.resetFiltersButton) {
this.resetFiltersButton.addEventListener('click', () => {
this.resetFilters();
});
}
}
updateStatusIndicator() {
this.statusIndicator.innerHTML = '';
// Even if the element is empty, it'll mess up the flex layout unless we set display none
hideElement(this.statusIndicator);
if (this.currentStatus.length)
this.statusIndicator.innerHTML = '(' + this.currentStatus.length + ')';
showElement(this.statusIndicator);
}
closeFilters() {
if (this.statusToggle.getAttribute("aria-expanded") === "true") {
this.statusToggle.click();
}
}
initializeAccordionAccessibilityListeners() {
// Instead of managing the toggle/close on the filter buttons in all edge cases (user clicks on search, user clicks on ANOTHER filter,
// user clicks on main nav...) we add a listener and close the filters whenever the focus shifts out of the dropdown menu/filter button.
// NOTE: We may need to evolve this as we add more filters.
document.addEventListener('focusin', (event) => {
const accordion = document.querySelector('.usa-accordion--select');
const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
if (accordionThatIsOpen && !accordion.contains(event.target)) {
this.closeFilters();
}
});
// Close when user clicks outside
// NOTE: We may need to evolve this as we add more filters.
document.addEventListener('click', (event) => {
const accordion = document.querySelector('.usa-accordion--select');
const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
if (accordionThatIsOpen && !accordion.contains(event.target)) {
this.closeFilters();
}
});
}
}
class DomainsTable extends LoadTableBase {
constructor() {
super('.domains__table', '.domains__table-wrapper', '#domains__search-field', '#domains__search-field-submit', '.domains__reset-search', '.domains__reset-filters', '.domains__no-data', '.domains__no-search-results');
}
/**
* Loads rows in the domains list, as well as updates pagination around the domains list
* based on the supplied attributes.
@ -1178,10 +1372,12 @@ document.addEventListener('DOMContentLoaded', function() {
* @param {*} sortBy - the sort column option
* @param {*} order - the sort order {asc, desc}
* @param {*} scroll - control for the scrollToElement functionality
* @param {*} status - control for the status filter
* @param {*} searchTerm - the search term
* @param {*} portfolio - the portfolio id
*/
function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm, portfolio = portfolioValue) {
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) {
// fetch json of page of domais, given params
let baseUrl = document.getElementById("get_domains_json_url");
if (!baseUrl) {
@ -1194,10 +1390,19 @@ document.addEventListener('DOMContentLoaded', function() {
}
// fetch json of page of domains, given params
let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}`
let searchParams = new URLSearchParams(
{
"page": page,
"sort_by": sortBy,
"order": order,
"status": status,
"search_term": searchTerm
}
);
if (portfolio)
url += `&portfolio=${portfolio}`
searchParams.append("portfolio", portfolio)
let url = `${baseUrlValue}?${searchParams.toString()}`
fetch(url)
.then(response => response.json())
.then(data => {
@ -1207,7 +1412,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
// handle the display of proper messaging in the event that no domains exist in the list or search returns no results
updateDisplay(data, domainsWrapper, noDomainsWrapper, noSearchResultsWrapper, currentSearchTerm);
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
// identify the DOM element where the domain list will be inserted into the DOM
const domainList = document.querySelector('.domains__table tbody');
@ -1225,7 +1430,7 @@ document.addEventListener('DOMContentLoaded', function() {
let markupForSuborganizationRow = '';
if (portfolioValue) {
if (this.portfolioValue) {
markupForSuborganizationRow = `
<td>
<span class="text-wrap" aria-label="${domain.suborganization ? suborganization : 'No suborganization'}">${suborganization}</span>
@ -1271,271 +1476,46 @@ document.addEventListener('DOMContentLoaded', function() {
// Do not scroll on first page load
if (scroll)
ScrollToElement('class', 'domains');
scrollToTable = true;
this.scrollToTable = true;
// update pagination
updatePagination(
this.updatePagination(
'domain',
'#domains-pagination',
'#domains-pagination .usa-pagination__counter',
'#domains',
loadDomains,
data.page,
data.num_pages,
data.has_previous,
data.has_next,
data.total,
currentSearchTerm
);
currentSortBy = sortBy;
currentOrder = order;
currentSearchTerm = searchTerm;
this.currentSortBy = sortBy;
this.currentOrder = order;
this.currentSearchTerm = searchTerm;
})
.catch(error => console.error('Error fetching domains:', error));
}
// Add event listeners to table headers for sorting
tableHeaders.forEach(header => {
header.addEventListener('click', function() {
const sortBy = this.getAttribute('data-sortable');
let order = 'asc';
// sort order will be ascending, unless the currently sorted column is ascending, and the user
// is selecting the same column to sort in descending order
if (sortBy === currentSortBy) {
order = currentOrder === 'asc' ? 'desc' : 'asc';
}
// load the results with the updated sort
loadDomains(1, sortBy, order);
});
});
domainsSearchSubmit.addEventListener('click', function(e) {
e.preventDefault();
currentSearchTerm = domainsSearchInput.value;
// If the search is blank, we match the resetSearch functionality
if (currentSearchTerm) {
showElement(resetSearchButton);
} else {
hideElement(resetSearchButton);
}
loadDomains(1, 'id', 'asc');
resetHeaders();
});
if (statusToggle) {
statusToggle.addEventListener('click', function() {
toggleCaret(statusToggle);
});
}
// Add event listeners to status filter checkboxes
statusCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const checkboxValue = this.value;
// Update currentStatus array based on checkbox state
if (this.checked) {
currentStatus.push(checkboxValue);
} else {
const index = currentStatus.indexOf(checkboxValue);
if (index > -1) {
currentStatus.splice(index, 1);
class DomainRequestsTable extends LoadTableBase {
constructor() {
super('.domain-requests__table', '.domain-requests__table-wrapper', '#domain-requests__search-field', '#domain-requests__search-field-submit', '.domain-requests__reset-search', '.domain-requests__reset-filters', '.domain-requests__no-data', '.domain-requests__no-search-results');
}
}
// Manage visibility of reset filters button
if (currentStatus.length == 0) {
hideElement(resetFiltersButton);
} else {
showElement(resetFiltersButton);
}
// Disable the auto scroll
scrollToTable = false;
// Call loadDomains with updated status
loadDomains(1, 'id', 'asc');
resetHeaders();
updateStatusIndicator();
});
});
// Reset UI and accessibility
function resetHeaders() {
tableHeaders.forEach(header => {
// Unset sort UI in headers
unsetHeader(header);
});
// Reset the announcement region
tableAnnouncementRegion.innerHTML = '';
}
function resetSearch() {
domainsSearchInput.value = '';
currentSearchTerm = '';
hideElement(resetSearchButton);
loadDomains(1, 'id', 'asc');
resetHeaders();
}
if (resetSearchButton) {
resetSearchButton.addEventListener('click', function() {
resetSearch();
});
}
function resetFilters() {
currentStatus = [];
statusCheckboxes.forEach(checkbox => {
checkbox.checked = false;
});
hideElement(resetFiltersButton);
// Disable the auto scroll
scrollToTable = false;
loadDomains(1, 'id', 'asc');
resetHeaders();
updateStatusIndicator();
// No need to toggle close the filters. The focus shift will trigger that for us.
}
if (resetFiltersButton) {
resetFiltersButton.addEventListener('click', function() {
resetFilters();
});
}
function updateStatusIndicator() {
statusIndicator.innerHTML = '';
// Even if the element is empty, it'll mess up the flex layout unless we set display none
statusIndicator.hideElement();
if (currentStatus.length)
statusIndicator.innerHTML = '(' + currentStatus.length + ')';
statusIndicator.showElement();
}
function closeFilters() {
if (statusToggle.getAttribute("aria-expanded") === "true") {
statusToggle.click();
}
}
// Instead of managing the toggle/close on the filter buttons in all edge cases (user clicks on search, user clicks on ANOTHER filter,
// user clicks on main nav...) we add a listener and close the filters whenever the focus shifts out of the dropdown menu/filter button.
// NOTE: We may need to evolve this as we add more filters.
document.addEventListener('focusin', function(event) {
const accordion = document.querySelector('.usa-accordion--select');
const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
if (accordionThatIsOpen && !accordion.contains(event.target)) {
closeFilters();
}
});
// Close when user clicks outside
// NOTE: We may need to evolve this as we add more filters.
document.addEventListener('click', function(event) {
const accordion = document.querySelector('.usa-accordion--select');
const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
if (accordionThatIsOpen && !accordion.contains(event.target)) {
closeFilters();
}
});
// Initial load
loadDomains(1);
}
});
const utcDateString = (dateString) => {
const date = new Date(dateString);
const utcYear = date.getUTCFullYear();
const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
const utcDay = date.getUTCDate().toString().padStart(2, '0');
let utcHours = date.getUTCHours();
const utcMinutes = date.getUTCMinutes().toString().padStart(2, '0');
const ampm = utcHours >= 12 ? 'PM' : 'AM';
utcHours = utcHours % 12 || 12; // Convert to 12-hour format, '0' hours should be '12'
return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} ${ampm} UTC`;
};
/**
* An IIFE that listens for DOM Content to be loaded, then executes. This function
* initializes the domain requests list and associated functionality on the home page of the app.
*
*/
document.addEventListener('DOMContentLoaded', function() {
const domainRequestsSectionWrapper = document.querySelector('.domain-requests');
const domainRequestsWrapper = document.querySelector('.domain-requests__table-wrapper');
if (domainRequestsWrapper) {
let currentSortBy = 'id';
let currentOrder = 'asc';
const noDomainRequestsWrapper = document.querySelector('.domain-requests__no-data');
const noSearchResultsWrapper = document.querySelector('.domain-requests__no-search-results');
let scrollToTable = false;
let currentSearchTerm = '';
const domainRequestsSearchInput = document.getElementById('domain-requests__search-field');
const domainRequestsSearchSubmit = document.getElementById('domain-requests__search-field-submit');
const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]');
const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region');
const resetSearchButton = document.querySelector('.domain-requests__reset-search');
const portfolioElement = document.getElementById('portfolio-js-value');
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
/**
* Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input.
* @param {*} domainRequestPk - the identifier for the request that we're deleting
* @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page
*/
function deleteDomainRequest(domainRequestPk,pageToDisplay) {
// Use to debug uswds modal issues
//console.log('deleteDomainRequest')
// Get csrf token
const csrfToken = getCsrfToken();
// Create FormData object and append the CSRF token
const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`;
fetch(`/domain-request/${domainRequestPk}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': csrfToken,
},
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Update data and UI
loadDomainRequests(pageToDisplay, currentSortBy, currentOrder, scrollToTable, currentSearchTerm);
})
.catch(error => console.error('Error fetching domain requests:', error));
}
// Helper function to get the CSRF token from the cookie
function getCsrfToken() {
return document.querySelector('input[name="csrfmiddlewaretoken"]').value;
}
/**
* Loads rows in the domain requests list, as well as updates pagination around the domain requests list
* Loads rows in the domains list, as well as updates pagination around the domains list
* based on the supplied attributes.
* @param {*} page - the page number of the results (starts with 1)
* @param {*} sortBy - the sort column option
* @param {*} order - the sort order {asc, desc}
* @param {*} scroll - control for the scrollToElement functionality
* @param {*} status - control for the status filter
* @param {*} searchTerm - the search term
* @param {*} portfolio - the portfolio id
*/
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm, portfolio = portfolioValue) {
// fetch json of page of domain requests, given params
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) {
let baseUrl = document.getElementById("get_domain_requests_json_url");
if (!baseUrl) {
return;
@ -1546,11 +1526,20 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
// fetch json of page of requests, given params
let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`
// add searchParams
let searchParams = new URLSearchParams(
{
"page": page,
"sort_by": sortBy,
"order": order,
"status": status,
"search_term": searchTerm
}
);
if (portfolio)
url += `&portfolio=${portfolio}`
searchParams.append("portfolio", portfolio)
let url = `${baseUrlValue}?${searchParams.toString()}`
fetch(url)
.then(response => response.json())
.then(data => {
@ -1560,7 +1549,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results
updateDisplay(data, domainRequestsWrapper, noDomainRequestsWrapper, noSearchResultsWrapper, currentSearchTerm);
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
// identify the DOM element where the domain request list will be inserted into the DOM
const tbody = document.querySelector('.domain-requests__table tbody');
@ -1613,7 +1602,7 @@ document.addEventListener('DOMContentLoaded', function() {
let markupCreatorRow = '';
if (portfolioValue) {
if (this.portfolioValue) {
markupCreatorRow = `
<td>
<span class="text-wrap break-word">${request.creator ? request.creator : ''}</span>
@ -1708,10 +1697,10 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
`
domainRequestsSectionWrapper.appendChild(modal);
this.tableWrapper.appendChild(modal);
// Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly
if (portfolioValue) {
if (this.portfolioValue) {
modalTrigger = `
<a
role="button"
@ -1794,8 +1783,8 @@ document.addEventListener('DOMContentLoaded', function() {
modals.forEach(modal => {
const submitButton = modal.querySelector('.usa-modal__submit');
const closeButton = modal.querySelector('.usa-modal__close');
submitButton.addEventListener('click', function() {
pk = submitButton.getAttribute('data-pk');
submitButton.addEventListener('click', () => {
let pk = submitButton.getAttribute('data-pk');
// Close the modal to remove the USWDS UI local classes
closeButton.click();
// If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page
@ -1803,90 +1792,95 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.total == 1 && data.unfiltered_total > 1) {
pageToDisplay--;
}
deleteDomainRequest(pk, pageToDisplay);
this.deleteDomainRequest(pk, pageToDisplay);
});
});
// Do not scroll on first page load
if (scroll)
ScrollToElement('class', 'domain-requests');
scrollToTable = true;
this.scrollToTable = true;
// update the pagination after the domain requests list is updated
updatePagination(
this.updatePagination(
'domain request',
'#domain-requests-pagination',
'#domain-requests-pagination .usa-pagination__counter',
'#domain-requests',
loadDomainRequests,
data.page,
data.num_pages,
data.has_previous,
data.has_next,
data.total,
currentSearchTerm
);
currentSortBy = sortBy;
currentOrder = order;
currentSearchTerm = searchTerm;
this.currentSortBy = sortBy;
this.currentOrder = order;
this.currentSearchTerm = searchTerm;
})
.catch(error => console.error('Error fetching domain requests:', error));
}
// Add event listeners to table headers for sorting
tableHeaders.forEach(header => {
header.addEventListener('click', function() {
const sortBy = this.getAttribute('data-sortable');
let order = 'asc';
// sort order will be ascending, unless the currently sorted column is ascending, and the user
// is selecting the same column to sort in descending order
if (sortBy === currentSortBy) {
order = currentOrder === 'asc' ? 'desc' : 'asc';
}
loadDomainRequests(1, sortBy, order);
});
});
/**
* Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input.
* @param {*} domainRequestPk - the identifier for the request that we're deleting
* @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page
*/
deleteDomainRequest(domainRequestPk, pageToDisplay) {
// Use to debug uswds modal issues
//console.log('deleteDomainRequest')
domainRequestsSearchSubmit.addEventListener('click', function(e) {
e.preventDefault();
currentSearchTerm = domainRequestsSearchInput.value;
// If the search is blank, we match the resetSearch functionality
if (currentSearchTerm) {
showElement(resetSearchButton);
} else {
hideElement(resetSearchButton);
}
loadDomainRequests(1, 'id', 'asc');
resetHeaders();
});
// Get csrf token
const csrfToken = getCsrfToken();
// Create FormData object and append the CSRF token
const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`;
// Reset UI and accessibility
function resetHeaders() {
tableHeaders.forEach(header => {
// unset sort UI in headers
unsetHeader(header);
});
// Reset the announcement region
tableAnnouncementRegion.innerHTML = '';
fetch(`/domain-request/${domainRequestPk}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': csrfToken,
},
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Update data and UI
this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm);
})
.catch(error => console.error('Error fetching domain requests:', error));
}
}
function resetSearch() {
domainRequestsSearchInput.value = '';
currentSearchTerm = '';
hideElement(resetSearchButton);
loadDomainRequests(1, 'id', 'asc');
resetHeaders();
}
if (resetSearchButton) {
resetSearchButton.addEventListener('click', function() {
resetSearch();
/**
* An IIFE that listens for DOM Content to be loaded, then executes. This function
* initializes the domains list and associated functionality on the home page of the app.
*
*/
document.addEventListener('DOMContentLoaded', function() {
const isDomainsPage = document.querySelector("#domains")
if (isDomainsPage){
const domainsTable = new DomainsTable();
if (domainsTable.tableWrapper) {
// Initial load
domainsTable.loadTable(1);
}
}
});
}
function closeMoreActionMenu(accordionThatIsOpen) {
if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") {
accordionThatIsOpen.click();
/**
* An IIFE that listens for DOM Content to be loaded, then executes. This function
* initializes the domain requests list and associated functionality on the home page of the app.
*
*/
document.addEventListener('DOMContentLoaded', function() {
const domainRequestsSectionWrapper = document.querySelector('.domain-requests');
if (domainRequestsSectionWrapper) {
const domainRequestsTable = new DomainRequestsTable();
if (domainRequestsTable.tableWrapper) {
domainRequestsTable.loadTable(1);
}
}
@ -1898,6 +1892,12 @@ document.addEventListener('DOMContentLoaded', function() {
closeOpenAccordions(event);
});
function closeMoreActionMenu(accordionThatIsOpen) {
if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") {
accordionThatIsOpen.click();
}
}
function closeOpenAccordions(event) {
const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]');
openAccordions.forEach((openAccordionButton) => {
@ -1909,12 +1909,22 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
}
// Initial load
loadDomainRequests(1);
}
});
const utcDateString = (dateString) => {
const date = new Date(dateString);
const utcYear = date.getUTCFullYear();
const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
const utcDay = date.getUTCDate().toString().padStart(2, '0');
let utcHours = date.getUTCHours();
const utcMinutes = date.getUTCMinutes().toString().padStart(2, '0');
const ampm = utcHours >= 12 ? 'PM' : 'AM';
utcHours = utcHours % 12 || 12; // Convert to 12-hour format, '0' hours should be '12'
return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} ${ampm} UTC`;
};
/**
* An IIFE that displays confirmation modal on the user profile page

View file

@ -1,7 +1,8 @@
@use "uswds-core" as *;
@use "cisa_colors" as *;
/* Make "placeholder" links visually obvious */
// Used on: TODO links
// Used on: NONE
a[href$="todo"]::after {
background-color: yellow;
color: color(blue-80v);
@ -9,10 +10,14 @@ a[href$="todo"]::after {
font-style: italic;
}
// Used on: profile
// Note: Is this needed?
a.usa-link.usa-link--always-blue {
color: #{$dhs-blue};
}
// Used on: breadcrumbs
// Note: This could potentially be simplified and use usa-button--with-icon
a.breadcrumb__back {
display:flex;
align-items: center;
@ -28,10 +33,18 @@ a.breadcrumb__back {
}
}
// Remove anchor buttons' underline
a.usa-button {
text-decoration: none;
}
// Unstyled anchor buttons
a.usa-button--unstyled:visited {
color: color('primary');
}
// Disabled anchor buttons
// NOTE: Not used
a.usa-button.disabled-link {
background-color: #ccc !important;
color: #454545 !important
@ -58,6 +71,8 @@ a.usa-button--unstyled.disabled-link:focus {
text-decoration: none !important;
}
// Disabled buttons
// Used on: Domain managers, disabled logo on profile
.usa-button--unstyled.disabled-button,
.usa-button--unstyled.disabled-button:hover,
.usa-button--unstyled.disabled-button:focus {
@ -66,6 +81,16 @@ a.usa-button--unstyled.disabled-link:focus {
text-decoration: none !important;
}
// Unstyled variant for reverse out?
// Used on: NONE
.usa-button--unstyled--white,
.usa-button--unstyled--white:hover,
.usa-button--unstyled--white:focus,
.usa-button--unstyled--white:active {
color: color('white');
}
// Solid anchor buttons
a.usa-button:not(.usa-button--unstyled, .usa-button--outline) {
color: color('white');
}
@ -77,6 +102,7 @@ a.usa-button:not(.usa-button--unstyled, .usa-button--outline):active {
color: color('white');
}
// Outline anchor buttons
a.usa-button--outline,
a.usa-button--outline:visited {
box-shadow: inset 0 0 0 2px color('primary');
@ -94,10 +120,22 @@ a.usa-button--outline:active {
color: color('primary-darker');
}
// Used on: Domain request withdraw confirmation
a.withdraw {
background-color: color('error');
}
a.withdraw:hover,
a.withdraw:focus {
background-color: color('error-dark');
}
a.withdraw:active {
background-color: color('error-darker');
}
// Used on: Domain request status
//NOTE: Revise to BEM convention usa-button--outline-secondary
a.withdraw_outline,
a.withdraw_outline:visited {
box-shadow: inset 0 0 0 2px color('error');
@ -115,19 +153,8 @@ a.withdraw_outline:active {
color: color('error-darker');
}
a.withdraw:hover,
a.withdraw:focus {
background-color: color('error-dark');
}
a.withdraw:active {
background-color: color('error-darker');
}
a.usa-button--unstyled:visited {
color: color('primary');
}
// Used on: Domain request submit
.dotgov-button--green {
background-color: color('success-dark');
@ -140,15 +167,8 @@ a.usa-button--unstyled:visited {
}
}
.usa-button--unstyled--white,
.usa-button--unstyled--white:hover,
.usa-button--unstyled--white:focus,
.usa-button--unstyled--white:active {
color: color('white');
}
// Cancel button used on the
// DNSSEC main page
// Cancel button
// Used on: DNSSEC main page
// We want to center this button on mobile
// and add some extra left margin on tablet+
.usa-button--cancel {
@ -175,6 +195,8 @@ a.usa-button--unstyled:visited {
}
}
// Used on: Profile page, toggleable fields
// Note: Could potentially be cleaned up by using usa-button--with-icon
// We need to deviate from some default USWDS styles here
// in this particular case, so we have to override this.
.usa-form .usa-button.readonly-edit-button {
@ -186,6 +208,7 @@ a.usa-button--unstyled:visited {
}
}
//Used on: Domains and Requests tables
.usa-button--filter {
width: auto;
// For mobile stacking
@ -201,6 +224,8 @@ a.usa-button--unstyled:visited {
}
}
// Buttons with nested icons
// Note: Can be simplified by adding usa-link--icon to anchors in tables
.dotgov-table a,
.usa-link--icon,
.usa-button--with-icon {
@ -224,6 +249,9 @@ a .usa-icon,
width: 1.5em;
}
// Red, for delete buttons
// Used on: All delete buttons
// Note: Can be simplified by adding text-secondary to delete anchors in tables
button.text-secondary,
button.text-secondary:hover,
.dotgov-table a.text-secondary {

View file

@ -85,7 +85,7 @@ legend.float-left-tablet + button.float-right-tablet {
.read-only-label {
font-size: size('body', 'sm');
color: color('primary');
color: color('primary-dark');
margin-bottom: units(0.5);
}

View file

@ -245,7 +245,6 @@ TEMPLATES = [
"registrar.context_processors.is_production",
"registrar.context_processors.org_user_status",
"registrar.context_processors.add_path_to_context",
"registrar.context_processors.add_has_profile_feature_flag_to_context",
"registrar.context_processors.portfolio_permissions",
],
},

View file

@ -53,7 +53,6 @@ for step, view in [
(Step.CURRENT_SITES, views.CurrentSites),
(Step.DOTGOV_DOMAIN, views.DotgovDomain),
(Step.PURPOSE, views.Purpose),
(Step.YOUR_CONTACT, views.YourContact),
(Step.OTHER_CONTACTS, views.OtherContacts),
(Step.ADDITIONAL_DETAILS, views.AdditionalDetails),
(Step.REQUIREMENTS, views.Requirements),
@ -213,11 +212,6 @@ urlpatterns = [
views.DomainDsDataView.as_view(),
name="domain-dns-dnssec-dsdata",
),
path(
"domain/<int:pk>/your-contact-information",
views.DomainYourContactInformationView.as_view(),
name="domain-your-contact-information",
),
path(
"domain/<int:pk>/org-name-address",
views.DomainOrgNameAddressView.as_view(),

View file

@ -1,5 +1,4 @@
from django.conf import settings
from waffle.decorators import flag_is_active
def language_code(request):
@ -54,10 +53,6 @@ def add_path_to_context(request):
return {"path": getattr(request, "path", None)}
def add_has_profile_feature_flag_to_context(request):
return {"has_profile_feature_flag": flag_is_active(request, "profile_feature")}
def portfolio_permissions(request):
"""Make portfolio permissions for the request user available in global context"""
portfolio_context = {

View file

@ -37,7 +37,6 @@ class DomainRequestFixture:
# "anything_else": None,
# "is_policy_acknowledged": None,
# "senior_official": None,
# "submitter": None,
# "other_contacts": [],
# "current_websites": [],
# "alternative_domains": [],
@ -123,12 +122,6 @@ class DomainRequestFixture:
else:
da.senior_official = Contact.objects.create(**cls.fake_contact())
if not da.submitter:
if "submitter" in app and app["submitter"] is not None:
da.submitter, _ = Contact.objects.get_or_create(**app["submitter"])
else:
da.submitter = Contact.objects.create(**cls.fake_contact())
if not da.requested_domain:
if "requested_domain" in app and app["requested_domain"] is not None:
da.requested_domain, _ = DraftDomain.objects.get_or_create(name=app["requested_domain"])

View file

@ -386,64 +386,6 @@ class PurposeForm(RegistrarForm):
)
class YourContactForm(RegistrarForm):
JOIN = "submitter"
def to_database(self, obj):
if not self.is_valid():
return
contact = getattr(obj, "submitter", None)
if contact is not None and not contact.has_more_than_one_join("submitted_domain_requests"):
# if contact exists in the database and is not joined to other entities
super().to_database(contact)
else:
# no contact exists OR contact exists which is joined also to other entities;
# in either case, create a new contact and update it
contact = Contact()
super().to_database(contact)
obj.submitter = contact
obj.save()
@classmethod
def from_database(cls, obj):
contact = getattr(obj, "submitter", None)
return super().from_database(contact)
first_name = forms.CharField(
label="First name / given name",
error_messages={"required": "Enter your first name / given name."},
)
middle_name = forms.CharField(
required=False,
label="Middle name (optional)",
)
last_name = forms.CharField(
label="Last name / family name",
error_messages={"required": "Enter your last name / family name."},
)
title = forms.CharField(
label="Title or role in your organization",
error_messages={
"required": ("Enter your title or role in your organization (e.g., Chief Information Officer).")
},
)
email = forms.EmailField(
label="Email",
max_length=None,
error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")},
validators=[
MaxLengthValidator(
320,
message="Response must be less than 320 characters.",
)
],
)
phone = PhoneNumberField(
label="Phone",
error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."},
)
class OtherContactsYesNoForm(BaseYesNoForm):
"""The yes/no field for the OtherContacts form."""

View file

@ -0,0 +1,38 @@
# Generated by Django 4.2.10 on 2024-08-29 23:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0124_alter_portfolioinvitation_portfolio_additional_permissions_and_more"),
]
operations = [
migrations.AlterField(
model_name="domaininformation",
name="submitter",
field=models.ForeignKey(
blank=True,
help_text='Person listed under "your contact information" in the request form',
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="submitted_domain_requests_information",
to="registrar.contact",
),
),
migrations.AlterField(
model_name="domainrequest",
name="submitter",
field=models.ForeignKey(
blank=True,
help_text='Person listed under "your contact information" in the request form; will receive email updates',
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="submitted_domain_requests",
to="registrar.contact",
),
),
]

View file

@ -0,0 +1,27 @@
from django.db import migrations
from django.db.models import Q
from typing import Any
# Deletes Contact objects associated with a submitter which we are deprecating
def cascade_delete_submitter_contacts(apps, schema_editor) -> Any:
contacts_model = apps.get_model("registrar", "Contact")
submitter_contacts = contacts_model.objects.filter(
Q(submitted_domain_requests__isnull=False) | Q(submitted_domain_requests_information__isnull=False)
).filter(
information_senior_official__isnull=True,
senior_official__isnull=True,
contact_domain_requests_information__isnull=True,
contact_domain_requests__isnull=True,
)
submitter_contacts.delete()
class Migration(migrations.Migration):
dependencies = [
("registrar", "0125_alter_domaininformation_submitter_and_more"),
]
operations = [
migrations.RunPython(cascade_delete_submitter_contacts, reverse_code=migrations.RunPython.noop, atomic=True),
]

View file

@ -0,0 +1,33 @@
# Generated by Django 4.2.10 on 2024-08-29 24:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0126_delete_cascade_submitter_contacts"),
]
operations = [
migrations.RemoveField(
model_name="domaininformation",
name="submitter",
),
migrations.RemoveField(
model_name="domainrequest",
name="submitter",
),
migrations.AlterField(
model_name="domainrequest",
name="creator",
field=models.ForeignKey(
help_text="Person who submitted the domain request. Will receive email updates.",
on_delete=django.db.models.deletion.PROTECT,
related_name="domain_requests_created",
to=settings.AUTH_USER_MODEL,
),
),
]

View file

@ -0,0 +1,229 @@
# Generated by Django 4.2.10 on 2024-09-11 21:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0127_remove_domaininformation_submitter_and_more"),
]
operations = [
migrations.AlterField(
model_name="domaininformation",
name="state_territory",
field=models.CharField(
blank=True,
choices=[
("AL", "Alabama (AL)"),
("AK", "Alaska (AK)"),
("AS", "American Samoa (AS)"),
("AZ", "Arizona (AZ)"),
("AR", "Arkansas (AR)"),
("CA", "California (CA)"),
("CO", "Colorado (CO)"),
("CT", "Connecticut (CT)"),
("DE", "Delaware (DE)"),
("DC", "District of Columbia (DC)"),
("FL", "Florida (FL)"),
("GA", "Georgia (GA)"),
("GU", "Guam (GU)"),
("HI", "Hawaii (HI)"),
("ID", "Idaho (ID)"),
("IL", "Illinois (IL)"),
("IN", "Indiana (IN)"),
("IA", "Iowa (IA)"),
("KS", "Kansas (KS)"),
("KY", "Kentucky (KY)"),
("LA", "Louisiana (LA)"),
("ME", "Maine (ME)"),
("MD", "Maryland (MD)"),
("MA", "Massachusetts (MA)"),
("MI", "Michigan (MI)"),
("MN", "Minnesota (MN)"),
("MS", "Mississippi (MS)"),
("MO", "Missouri (MO)"),
("MT", "Montana (MT)"),
("NE", "Nebraska (NE)"),
("NV", "Nevada (NV)"),
("NH", "New Hampshire (NH)"),
("NJ", "New Jersey (NJ)"),
("NM", "New Mexico (NM)"),
("NY", "New York (NY)"),
("NC", "North Carolina (NC)"),
("ND", "North Dakota (ND)"),
("MP", "Northern Mariana Islands (MP)"),
("OH", "Ohio (OH)"),
("OK", "Oklahoma (OK)"),
("OR", "Oregon (OR)"),
("PA", "Pennsylvania (PA)"),
("PR", "Puerto Rico (PR)"),
("RI", "Rhode Island (RI)"),
("SC", "South Carolina (SC)"),
("SD", "South Dakota (SD)"),
("TN", "Tennessee (TN)"),
("TX", "Texas (TX)"),
("UM", "United States Minor Outlying Islands (UM)"),
("UT", "Utah (UT)"),
("VT", "Vermont (VT)"),
("VI", "Virgin Islands (VI)"),
("VA", "Virginia (VA)"),
("WA", "Washington (WA)"),
("WV", "West Virginia (WV)"),
("WI", "Wisconsin (WI)"),
("WY", "Wyoming (WY)"),
("AA", "Armed Forces Americas (AA)"),
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
("AP", "Armed Forces Pacific (AP)"),
],
max_length=2,
null=True,
verbose_name="state, territory, or military post",
),
),
migrations.AlterField(
model_name="domainrequest",
name="state_territory",
field=models.CharField(
blank=True,
choices=[
("AL", "Alabama (AL)"),
("AK", "Alaska (AK)"),
("AS", "American Samoa (AS)"),
("AZ", "Arizona (AZ)"),
("AR", "Arkansas (AR)"),
("CA", "California (CA)"),
("CO", "Colorado (CO)"),
("CT", "Connecticut (CT)"),
("DE", "Delaware (DE)"),
("DC", "District of Columbia (DC)"),
("FL", "Florida (FL)"),
("GA", "Georgia (GA)"),
("GU", "Guam (GU)"),
("HI", "Hawaii (HI)"),
("ID", "Idaho (ID)"),
("IL", "Illinois (IL)"),
("IN", "Indiana (IN)"),
("IA", "Iowa (IA)"),
("KS", "Kansas (KS)"),
("KY", "Kentucky (KY)"),
("LA", "Louisiana (LA)"),
("ME", "Maine (ME)"),
("MD", "Maryland (MD)"),
("MA", "Massachusetts (MA)"),
("MI", "Michigan (MI)"),
("MN", "Minnesota (MN)"),
("MS", "Mississippi (MS)"),
("MO", "Missouri (MO)"),
("MT", "Montana (MT)"),
("NE", "Nebraska (NE)"),
("NV", "Nevada (NV)"),
("NH", "New Hampshire (NH)"),
("NJ", "New Jersey (NJ)"),
("NM", "New Mexico (NM)"),
("NY", "New York (NY)"),
("NC", "North Carolina (NC)"),
("ND", "North Dakota (ND)"),
("MP", "Northern Mariana Islands (MP)"),
("OH", "Ohio (OH)"),
("OK", "Oklahoma (OK)"),
("OR", "Oregon (OR)"),
("PA", "Pennsylvania (PA)"),
("PR", "Puerto Rico (PR)"),
("RI", "Rhode Island (RI)"),
("SC", "South Carolina (SC)"),
("SD", "South Dakota (SD)"),
("TN", "Tennessee (TN)"),
("TX", "Texas (TX)"),
("UM", "United States Minor Outlying Islands (UM)"),
("UT", "Utah (UT)"),
("VT", "Vermont (VT)"),
("VI", "Virgin Islands (VI)"),
("VA", "Virginia (VA)"),
("WA", "Washington (WA)"),
("WV", "West Virginia (WV)"),
("WI", "Wisconsin (WI)"),
("WY", "Wyoming (WY)"),
("AA", "Armed Forces Americas (AA)"),
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
("AP", "Armed Forces Pacific (AP)"),
],
max_length=2,
null=True,
verbose_name="state, territory, or military post",
),
),
migrations.AlterField(
model_name="portfolio",
name="state_territory",
field=models.CharField(
blank=True,
choices=[
("AL", "Alabama (AL)"),
("AK", "Alaska (AK)"),
("AS", "American Samoa (AS)"),
("AZ", "Arizona (AZ)"),
("AR", "Arkansas (AR)"),
("CA", "California (CA)"),
("CO", "Colorado (CO)"),
("CT", "Connecticut (CT)"),
("DE", "Delaware (DE)"),
("DC", "District of Columbia (DC)"),
("FL", "Florida (FL)"),
("GA", "Georgia (GA)"),
("GU", "Guam (GU)"),
("HI", "Hawaii (HI)"),
("ID", "Idaho (ID)"),
("IL", "Illinois (IL)"),
("IN", "Indiana (IN)"),
("IA", "Iowa (IA)"),
("KS", "Kansas (KS)"),
("KY", "Kentucky (KY)"),
("LA", "Louisiana (LA)"),
("ME", "Maine (ME)"),
("MD", "Maryland (MD)"),
("MA", "Massachusetts (MA)"),
("MI", "Michigan (MI)"),
("MN", "Minnesota (MN)"),
("MS", "Mississippi (MS)"),
("MO", "Missouri (MO)"),
("MT", "Montana (MT)"),
("NE", "Nebraska (NE)"),
("NV", "Nevada (NV)"),
("NH", "New Hampshire (NH)"),
("NJ", "New Jersey (NJ)"),
("NM", "New Mexico (NM)"),
("NY", "New York (NY)"),
("NC", "North Carolina (NC)"),
("ND", "North Dakota (ND)"),
("MP", "Northern Mariana Islands (MP)"),
("OH", "Ohio (OH)"),
("OK", "Oklahoma (OK)"),
("OR", "Oregon (OR)"),
("PA", "Pennsylvania (PA)"),
("PR", "Puerto Rico (PR)"),
("RI", "Rhode Island (RI)"),
("SC", "South Carolina (SC)"),
("SD", "South Dakota (SD)"),
("TN", "Tennessee (TN)"),
("TX", "Texas (TX)"),
("UM", "United States Minor Outlying Islands (UM)"),
("UT", "Utah (UT)"),
("VT", "Vermont (VT)"),
("VI", "Virgin Islands (VI)"),
("VA", "Virginia (VA)"),
("WA", "Washington (WA)"),
("WV", "West Virginia (WV)"),
("WI", "Wisconsin (WI)"),
("WY", "Wyoming (WY)"),
("AA", "Armed Forces Americas (AA)"),
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
("AP", "Armed Forces Pacific (AP)"),
],
max_length=2,
null=True,
verbose_name="state, territory, or military post",
),
),
]

View file

@ -48,8 +48,7 @@ class DomainInformation(TimeStampedModel):
null=True,
)
# This is the domain request user who created this domain request. The contact
# information that they gave is in the `submitter` field
# This is the domain request user who created this domain request.
creator = models.ForeignKey(
"registrar.User",
on_delete=models.PROTECT,
@ -160,7 +159,7 @@ class DomainInformation(TimeStampedModel):
choices=StateTerritoryChoices.choices,
null=True,
blank=True,
verbose_name="state / territory",
verbose_name="state, territory, or military post",
)
zipcode = models.CharField(
max_length=10,
@ -197,17 +196,6 @@ class DomainInformation(TimeStampedModel):
related_name="domain_info",
)
# This is the contact information provided by the domain requestor. The
# user who created the domain request is in the `creator` field.
submitter = models.ForeignKey(
"registrar.Contact",
null=True,
blank=True,
related_name="submitted_domain_requests_information",
on_delete=models.PROTECT,
help_text='Person listed under "your contact information" in the request form',
)
purpose = models.TextField(
null=True,
blank=True,

View file

@ -6,7 +6,6 @@ from django.conf import settings
from django.db import models
from django_fsm import FSMField, transition # type: ignore
from django.utils import timezone
from waffle import flag_is_active
from registrar.models.domain import Domain
from registrar.models.federal_agency import FederalAgency
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
@ -340,13 +339,12 @@ class DomainRequest(TimeStampedModel):
help_text="The suborganization that this domain request is included under",
)
# This is the domain request user who created this domain request. The contact
# information that they gave is in the `submitter` field
# This is the domain request user who created this domain request.
creator = models.ForeignKey(
"registrar.User",
on_delete=models.PROTECT,
related_name="domain_requests_created",
help_text="Person who submitted the domain request; will not receive email updates",
help_text="Person who submitted the domain request. Will receive email updates.",
)
investigator = models.ForeignKey(
@ -425,7 +423,7 @@ class DomainRequest(TimeStampedModel):
choices=StateTerritoryChoices.choices,
null=True,
blank=True,
verbose_name="state / territory",
verbose_name="state, territory, or military post",
)
zipcode = models.CharField(
max_length=10,
@ -484,17 +482,6 @@ class DomainRequest(TimeStampedModel):
help_text="Other domain names the creator provided for consideration",
)
# This is the contact information provided by the domain requestor. The
# user who created the domain request is in the `creator` field.
submitter = models.ForeignKey(
"registrar.Contact",
null=True,
blank=True,
related_name="submitted_domain_requests",
on_delete=models.PROTECT,
help_text='Person listed under "your contact information" in the request form; will receive email updates',
)
purpose = models.TextField(
null=True,
blank=True,
@ -756,9 +743,6 @@ class DomainRequest(TimeStampedModel):
contact information. If there is not creator information, then do
nothing.
If the waffle flag "profile_feature" is active, then this email will be sent to the
domain request creator rather than the submitter
Optional args:
bcc_address: str -> the address to bcc to
@ -773,7 +757,7 @@ class DomainRequest(TimeStampedModel):
custom_email_content: str -> Renders an email with the content of this string as its body text.
"""
recipient = self.creator if flag_is_active(None, "profile_feature") else self.submitter
recipient = self.creator
if recipient is None or recipient.email is None:
logger.warning(
f"Cannot send {new_status} email, no creator email address for domain request with pk: {self.pk}."
@ -1202,6 +1186,10 @@ class DomainRequest(TimeStampedModel):
# Special District -> "Election office" and "About your organization" page can't be empty
return self.is_election_board is not None and self.about_your_organization is not None
# Do we still want to test this after creator is autogenerated? Currently it went back to being selectable
def _is_creator_complete(self):
return self.creator is not None
def _is_organization_name_and_address_complete(self):
return not (
self.organization_name is None
@ -1220,9 +1208,6 @@ class DomainRequest(TimeStampedModel):
def _is_purpose_complete(self):
return self.purpose is not None
def _is_submitter_complete(self):
return self.submitter is not None
def _has_other_contacts_and_filled(self):
# Other Contacts Radio button is Yes and if all required fields are filled
return (
@ -1271,14 +1256,12 @@ class DomainRequest(TimeStampedModel):
return self.is_policy_acknowledged is not None
def _is_general_form_complete(self, request):
has_profile_feature_flag = flag_is_active(request, "profile_feature")
return (
self._is_organization_name_and_address_complete()
self._is_creator_complete()
and self._is_organization_name_and_address_complete()
and self._is_senior_official_complete()
and self._is_requested_domain_complete()
and self._is_purpose_complete()
# NOTE: This flag leaves submitter as empty (request wont submit) hence set to True
and (self._is_submitter_complete() if not has_profile_feature_flag else True)
and self._is_other_contacts_complete()
and self._is_additional_details_complete()
and self._is_policy_acknowledgement_complete()

View file

@ -89,7 +89,7 @@ class Portfolio(TimeStampedModel):
choices=StateTerritoryChoices.choices,
null=True,
blank=True,
verbose_name="state / territory",
verbose_name="state, territory, or military post",
)
zipcode = models.CharField(

View file

@ -9,7 +9,7 @@ class WaffleFlag(AbstractUserFlag):
Custom implementation of django-waffles 'Flag' object.
Read more here: https://waffle.readthedocs.io/en/stable/types/flag.html
Use this class when dealing with feature flags, such as profile_feature.
Use this class when dealing with feature flags.
"""
class Meta:

View file

@ -72,12 +72,6 @@ class CheckUserProfileMiddleware:
"""Runs pre-processing logic for each view. Checks for the
finished_setup flag on the current user. If they haven't done so,
then we redirect them to the finish setup page."""
# Check that the user is "opted-in" to the profile feature flag
has_profile_feature_flag = flag_is_active(request, "profile_feature")
# If they aren't, skip this check entirely
if not has_profile_feature_flag:
return None
if request.user.is_authenticated:
profile_page = self.profile_page

View file

@ -70,7 +70,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
<div class="readonly textarea-wrapper">
<div id="action_needed_reason_email_readonly" class="dja-readonly-textarea-container padding-1 margin-top-0 padding-top-0 margin-bottom-1 thin-border collapse--dgsimple collapsed">
<label class="max-full" for="action_needed_reason_email_view_more">
<strong>Sent to {% if has_profile_feature_flag %}creator{%else%}submitter{%endif%}</strong>
<strong>Sent to creator</strong>
</label>
<textarea id="action_needed_reason_email_view_more" cols="40" rows="20" class="{% if not original_object.action_needed_reason %}display-none{% endif %}" readonly>
{{ original_object.action_needed_reason_email }}
@ -287,11 +287,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% if not skip_additional_contact_info %}
{% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
{% endif%}
{% elif field.field.name == "submitter" %}
<div class="flex-container tablet:margin-top-2">
<label aria-label="Submitter contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.submitter no_title_top_padding=field.is_readonly %}
</div>
{% elif field.field.name == "senior_official" %}
<div class="flex-container">
<label aria-label="Senior official contact details"></label>

View file

@ -77,19 +77,12 @@
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization_portfolio_permission %}
{% else %}
{% url 'domain-org-name-address' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
{% include "includes/summary_item.html" with title='Organization' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
{% url 'domain-senior-official' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=is_editable %}
{% endif %}
{# Conditionally display profile #}
{% if not has_profile_feature_flag %}
{% url 'domain-your-contact-information' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Your contact information' value=request.user contact='true' edit_link=url editable=is_editable %}
{% 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 edit_link=url editable=is_editable %}

View file

@ -7,7 +7,7 @@
{# this is right after the messages block in the parent template #}
{% include "includes/form_errors.html" with form=form %}
<h1>Organization name and mailing address </h1>
<h1>Organization</h1>
<p>The name of your organization will be publicly listed as the domain registrant.</p>

View file

@ -16,11 +16,9 @@
<h2>Time to complete the form</h2>
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
completing your domain request might take around 15 minutes.</p>
{% if has_profile_feature_flag %}
<h2>How well reach you</h2>
<p>While reviewing your domain request, we may need to reach out with questions. Well also email you when we complete our review. If the contact information below is not correct, visit <a href="{% url 'user-profile' %}?redirect=domain-request:" class="usa-link">your profile</a> to make updates.</p>
{% include "includes/profile_information.html" with user=user%}
{% endif %}
{% block form_buttons %}

View file

@ -130,8 +130,8 @@
{% if step == Step.YOUR_CONTACT %}
{% namespaced_url 'domain-request' step as domain_request_url %}
{% if domain_request.submitter is not None %}
{% with title=form_titles|get_item:step value=domain_request.submitter %}
{% if domain_request.creator is not None %}
{% with title=form_titles|get_item:step value=domain_request.creator %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %}
{% endwith %}
{% else %}

View file

@ -143,7 +143,7 @@
{% endif %}
{% if DomainRequest.organization_name %}
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=DomainRequest address='true' heading_level=heading_level %}
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.about_your_organization %}
@ -170,10 +170,9 @@
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
{% endif %}
{% if DomainRequest.submitter and not has_profile_feature_flag %}
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.submitter contact='true' heading_level=heading_level %}
{% if DomainRequest.creator %}
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.other_contacts.all %}
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %}
{% else %}

View file

@ -12,7 +12,7 @@
{% if not portfolio %}
{% with url_name="domain-org-name-address" %}
{% include "includes/domain_sidenav_item.html" with item_text="Organization name and mailing address" %}
{% include "includes/domain_sidenav_item.html" with item_text="Organization" %}
{% endwith %}
{% endif %}
@ -72,13 +72,6 @@
{% endwith %}
{% endif %}
{% if not has_profile_feature_flag %}
{# Conditionally display profile link in main nav #}
{% with url_name="domain-your-contact-information" %}
{% include "includes/domain_sidenav_item.html" with item_text="Your contact information" %}
{% endwith %}
{% endif %}
{% with url_name="domain-security-email" %}
{% include "includes/domain_sidenav_item.html" with item_text="Security email" %}
{% endwith %}

View file

@ -1,37 +0,0 @@
{% extends "domain_base.html" %}
{% load static field_helpers %}
{% block title %}Your contact information | {{ domain.name }} | {% endblock %}
{% block domain_content %}
{% include "includes/form_errors.html" with form=form %}
<h1>Your contact information</h1>
<p>If youd like us to use a different name, email, or phone number you can make those changes below. <strong>Updating your contact information here will update the contact information for all domains in your account.</strong> Changing your information here wont affect your Login.gov account information. The contact information you provide here wont be made public and will only be used for the .gov program.
</p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% csrf_token %}
{% input_with_errors form.first_name %}
{% input_with_errors form.middle_name %}
{% input_with_errors form.last_name %}
{% input_with_errors form.title %}
{% input_with_errors form.email %}
{% input_with_errors form.phone %}
<button
type="submit"
class="usa-button"
>Save</button>
</form>
{% endblock %} {# domain_content #}

View file

@ -15,7 +15,7 @@ State-recognized tribe
Election office:
{{ domain_request.is_election_board|yesno:"Yes,No,Incomplete" }}
{% endif %}
Organization name and mailing address:
Organization:
{% spaceless %}{{ domain_request.federal_agency }}
{{ domain_request.organization_name }}
{{ domain_request.address_line1 }}{% if domain_request.address_line2 %}

View file

@ -23,13 +23,21 @@
</svg>
Reset
</button>
{% if portfolio %}
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name or creator</label>
{% else %}
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>
{% endif %}
<input
class="usa-input"
id="domain-requests__search-field"
type="search"
name="search"
{% if portfolio %}
placeholder="Search by domain name or creator"
{% else %}
placeholder="Search by domain name"
{% endif %}
/>
<button class="usa-button" type="submit" id="domain-requests__search-field-submit">
<img
@ -42,6 +50,125 @@
</section>
</div>
</div>
{% if portfolio %}
<div class="display-flex flex-align-center">
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
<div class="usa-accordion usa-accordion--select margin-right-2">
<div class="usa-accordion__heading">
<button
type="button"
class="usa-button usa-button--small padding--8-8-9 usa-button--outline usa-button--filter usa-accordion__button"
aria-expanded="false"
aria-controls="filter-status"
>
<span class="filter-indicator text-bold display-none"></span> Status
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
</svg>
</button>
</div>
<div id="filter-status" class="usa-accordion__content usa-prose shadow-1">
<h2>Status</h2>
<fieldset class="usa-fieldset margin-top-0">
<legend class="usa-legend">Select to apply <span class="sr-only">status</span> filter</legend>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-started"
type="checkbox"
name="filter-status"
value="started"
/>
<label class="usa-checkbox__label" for="filter-status-started"
>Started</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-submitted"
type="checkbox"
name="filter-status"
value="submitted"
/>
<label class="usa-checkbox__label" for="filter-status-submitted"
>Submitted</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-in-review"
type="checkbox"
name="filter-status"
value="in review"
/>
<label class="usa-checkbox__label" for="filter-status-in-review"
>In review</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-action-needed"
type="checkbox"
name="filter-status"
value="action needed"
/>
<label class="usa-checkbox__label" for="filter-status-action-needed"
>Action needed</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-rejected"
type="checkbox"
name="filter-status"
value="rejected"
/>
<label class="usa-checkbox__label" for="filter-status-rejected"
>Rejected</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-withdrawn"
type="checkbox"
name="filter-status"
value="withdrawn"
/>
<label class="usa-checkbox__label" for="filter-status-withdrawn"
>Withdrawn</label
>
</div>
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-ineligible"
type="checkbox"
name="filter-status"
value="ineligible"
/>
<label class="usa-checkbox__label" for="filter-status-ineligible"
>Ineligible</label
>
</div>
</fieldset>
</div>
</div>
<button
type="button"
class="usa-button usa-button--small padding--8-12-9-12 usa-button--outline usa-button--filter domain-requests__reset-filters display-none"
>
Clear filters
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#close"></use>
</svg>
</button>
</div>
{% endif %}
<div class="domain-requests__table-wrapper display-none usa-table-container--scrollable margin-top-0" tabindex="0">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
<caption class="sr-only">Your domain requests</caption>

View file

@ -64,7 +64,7 @@
aria-expanded="false"
aria-controls="filter-status"
>
<span class="domain__filter-indicator text-bold display-none"></span> Status
<span class="filter-indicator text-bold display-none"></span> Status
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
</svg>

View file

@ -16,7 +16,6 @@
{% if user.is_authenticated %}
<span class="usa-nav__username ellipsis">{{ user.email }}</span>
</li>
{% if has_profile_feature_flag %}
<li class="usa-nav__primary-item">
{% url 'user-profile' as user_profile_url %}
{% url 'finish-user-profile-setup' as finish_setup_url %}
@ -24,7 +23,6 @@
<span class="text-primary">Your profile</span>
</a>
</li>
{% endif %}
<li class="usa-nav__primary-item">
<a href="{% url 'logout' %}"><span class="text-primary">Sign out</span></a>
{% else %}

View file

@ -18,7 +18,6 @@
{% if user.is_authenticated %}
<span class="ellipsis usa-nav__username">{{ user.email }}</span>
</li>
{% if has_profile_feature_flag %}
<li class="usa-nav__secondary-item">
{% url 'user-profile' as user_profile_url %}
{% url 'finish-user-profile-setup' as finish_setup_url %}
@ -26,7 +25,6 @@
Your profile
</a>
</li>
{% endif %}
<li class="usa-nav__secondary-item">
<a class="usa-nav-link" href="{% url 'logout' %}">Sign out</a>
{% else %}

View file

@ -2,9 +2,6 @@
<div class="usa-alert">
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %}">
<b>Attention:</b> You are on a test site.
{% if has_profile_feature_flag %}
The profile_feature flag is active.
{% endif %}
</div>
</div>
</div>

View file

@ -16,15 +16,7 @@
{% if can_edit %}
{% include "includes/required_fields.html" %}
{% else %}
<p>
The senior official for your organization cant be updated here.
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
</p>
{% endif %}
{% if can_edit %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
<form class="usa-form usa-form--large desktop:margin-top-4" method="post" novalidate id="form-container">
{% csrf_token %}
{% input_with_errors form.first_name %}
{% input_with_errors form.last_name %}
@ -33,8 +25,16 @@
<button type="submit" class="usa-button">Save</button>
</form>
{% elif not form.full_name.value and not form.title.value and not form.email.value %}
<h4>No senior official was found.</h4>
<p>
Your senior official is a person within your organization who can authorize domain requests.
We don't have information about your organization's senior official. To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
</p>
{% else %}
<p>
The senior official for your organization cant be updated here.
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
</p>
<div class="desktop:margin-top-4">
{% if form.full_name.value is not None %}
{% include "includes/input_read_only.html" with field=form.full_name %}
{% endif %}
@ -46,4 +46,5 @@
{% if form.email.value is not None %}
{% include "includes/input_read_only.html" with field=form.email %}
{% endif %}
</div>
{% endif %}

View file

@ -9,9 +9,6 @@
{# the entire logged in page goes here #}
<div class="tablet:grid-col-12">
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
{% block portfolio_content %}{% endblock %}

View file

@ -9,6 +9,10 @@
{% endblock %}
{% block portfolio_content %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
<div id="main-content">
<h1 id="domains-header">Domains</h1>
{% include "includes/domains_table.html" with portfolio=portfolio user_domain_count=user_domain_count %}

View file

@ -5,6 +5,12 @@
{% block title %} Domains | {% endblock %}
{% block portfolio_content %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
<div id="main-content">
<h1 id="domains-header">Domains</h1>
<section class="section-outlined">

View file

@ -1,7 +1,7 @@
{% extends 'portfolio_base.html' %}
{% load static field_helpers%}
{% block title %}Organization mailing address | {{ portfolio.name }}{% endblock %}
{% block title %}Organization name and mailing address | {{ portfolio.name }}{% endblock %}
{% load static %}
@ -19,21 +19,25 @@
<div class="tablet:grid-col-9" id="main-content">
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
<h1>Organization</h1>
<p>The name of your federal agency will be publicly listed as the domain registrant.</p>
<p>The name of your organization will be publicly listed as the domain registrant.</p>
{% if has_edit_org_portfolio_permission %}
<p>
The federal agency for your organization cant be updated here.
Your organization name cant be updated here.
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
</p>
{% include "includes/form_errors.html" with form=form %}
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate>
<form class="usa-form usa-form--large desktop:margin-top-4" method="post" novalidate>
{% csrf_token %}
<h4 class="read-only-label">Federal agency</h4>
<h4 class="read-only-label">Organization name</h4>
<p class="read-only-value">
{{ portfolio.federal_agency }}
</p>
@ -49,7 +53,7 @@
</button>
</form>
{% else %}
<h4 class="read-only-label">Federal agency</h4>
<h4 class="read-only-label">Organization name</h4>
<p class="read-only-value">
{{ portfolio.federal_agency }}
</p>

View file

@ -9,13 +9,18 @@
{% endblock %}
{% block portfolio_content %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
<div id="main-content">
<h1 id="domain-requests-header">Domain requests</h1>
<div class="grid-row grid-gap">
{% if has_edit_request_portfolio_permission %}
<div class="mobile:grid-col-12 tablet:grid-col-6">
<p class="margin-y-0">Domain requests can only be modified by the person who created the request.</p>
</div>
{% if has_edit_request_portfolio_permission %}
<div class="mobile:grid-col-12 tablet:grid-col-6">
{% comment %}
IMPORTANT:
@ -29,6 +34,8 @@
</a>
</p>
</div>
{% else %}
<p class="margin-y-0">Domain requests can only be modified by the person who created the request.</p>
{% endif %}
</div>

View file

@ -6,6 +6,10 @@
{% load static %}
{% block portfolio_content %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
<div class="grid-row grid-gap">
<div class="tablet:grid-col-3">
<p class="font-body-md margin-top-0 margin-bottom-2

View file

@ -394,7 +394,6 @@ class AuditedAdminMockData:
about_your_organization: str = "e-Government",
anything_else: str = "There is more",
senior_official: Contact = self.dummy_contact(item_name, "senior_official"),
submitter: Contact = self.dummy_contact(item_name, "submitter"),
creator: User = self.dummy_user(item_name, "creator"),
}
""" # noqa
@ -412,7 +411,6 @@ class AuditedAdminMockData:
about_your_organization="e-Government",
anything_else="There is more",
senior_official=self.dummy_contact(item_name, "senior_official"),
submitter=self.dummy_contact(item_name, "submitter"),
creator=creator,
)
return common_args
@ -537,8 +535,10 @@ class MockDb(TestCase):
first_name = "First"
last_name = "Last"
email = "info@example.com"
title = "title"
phone = "8080102431"
cls.user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, email=email
username=username, first_name=first_name, last_name=last_name, email=email, title=title, phone=phone
)
current_date = get_time_aware_date(datetime(2024, 4, 2))
@ -847,6 +847,7 @@ def create_superuser():
last_name="last",
is_staff=True,
password=p,
phone="8003111234",
)
# Retrieve the group or create it if it doesn't exist
group, _ = UserGroup.objects.get_or_create(name="full_access_group")
@ -864,7 +865,9 @@ def create_user():
first_name="first",
last_name="last",
is_staff=True,
title="title",
password=p,
phone="8003111234",
)
# Retrieve the group or create it if it doesn't exist
group, _ = UserGroup.objects.get_or_create(name="cisa_analysts_group")
@ -881,7 +884,12 @@ def create_test_user():
phone = "8003111234"
title = "test title"
user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone, title=title
username=username,
first_name=first_name,
last_name=last_name,
email=email,
phone=phone,
title=title,
)
return user
@ -901,7 +909,6 @@ def completed_domain_request( # noqa
has_cisa_representative=True,
status=DomainRequest.DomainRequestStatus.STARTED,
user=False,
submitter=False,
name="city.gov",
investigator=None,
generic_org_type="federal",
@ -926,14 +933,6 @@ def completed_domain_request( # noqa
domain, _ = DraftDomain.objects.get_or_create(name=name)
alt, _ = Website.objects.get_or_create(website="city1.gov")
current, _ = Website.objects.get_or_create(website="city.com")
if not submitter:
submitter, _ = Contact.objects.get_or_create(
first_name="Testy2",
last_name="Tester2",
title="Admin Tester",
email="mayor@igorville.gov",
phone="(555) 555 5556",
)
other, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",
@ -962,7 +961,6 @@ def completed_domain_request( # noqa
zipcode="10002",
senior_official=so,
requested_domain=domain,
submitter=submitter,
creator=user,
status=status,
investigator=investigator,

View file

@ -63,6 +63,7 @@ from .common import (
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from django.contrib.sessions.backends.db import SessionStore
from django.contrib.auth import get_user_model
from unittest.mock import ANY, patch, Mock
@ -384,7 +385,7 @@ class TestDomainInformationAdmin(TestCase):
contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson")
domain_request = completed_domain_request(
submitter=contact, name="city1244.gov", status=DomainRequest.DomainRequestStatus.IN_REVIEW
name="city1244.gov", status=DomainRequest.DomainRequestStatus.IN_REVIEW
)
domain_request.approve()
@ -509,7 +510,6 @@ class TestDomainInformationAdmin(TestCase):
# These should exist in the response
expected_values = [
("creator", "Person who submitted the domain request"),
("submitter", 'Person listed under "your contact information" in the request form'),
("domain_request", "Request associated with this domain"),
("no_other_contacts_rationale", "Required if creator does not list other employees"),
("urbanization", "Required for Puerto Rico only"),
@ -633,16 +633,6 @@ class TestDomainInformationAdmin(TestCase):
# Check for the field itself
self.assertContains(response, "Meoward Jones")
# == Check for the submitter == #
self.assertContains(response, "mayor@igorville.gov", count=2)
expected_submitter_fields = [
# Field, expected value
("title", "Admin Tester"),
("phone", "(555) 555 5556"),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
self.assertContains(response, "Testy2 Tester2")
# == Check for the senior_official == #
self.assertContains(response, "testy@town.com", count=2)
expected_so_fields = [
@ -664,7 +654,7 @@ class TestDomainInformationAdmin(TestCase):
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
# Test for the copy link
self.assertContains(response, "button--clipboard", count=4)
self.assertContains(response, "button--clipboard", count=3)
# cleanup this test
domain_info.delete()
@ -688,7 +678,6 @@ class TestDomainInformationAdmin(TestCase):
"more_organization_information",
"domain",
"domain_request",
"submitter",
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
@ -707,19 +696,19 @@ class TestDomainInformationAdmin(TestCase):
# Assert that sorting in reverse works correctly
self.test_helper.assert_table_sorted("-1", ("-domain__name",))
def test_submitter_sortable(self):
"""Tests if DomainInformation sorts by submitter correctly"""
def test_creator_sortable(self):
"""Tests if DomainInformation sorts by creator correctly"""
with less_console_noise():
self.client.force_login(self.superuser)
# Assert that our sort works correctly
self.test_helper.assert_table_sorted(
"4",
("submitter__first_name", "submitter__last_name"),
("creator__first_name", "creator__last_name"),
)
# Assert that sorting in reverse works correctly
self.test_helper.assert_table_sorted("-4", ("-submitter__first_name", "-submitter__last_name"))
self.test_helper.assert_table_sorted("-4", ("-creator__first_name", "-creator__last_name"))
class TestUserDomainRoleAdmin(TestCase):
@ -1317,7 +1306,6 @@ class AuditedAdminTest(TestCase):
# Senior offical is commented out for now - this is alphabetized
# and this test does not accurately reflect that.
# DomainRequest.senior_official.field,
DomainRequest.submitter.field,
# DomainRequest.investigator.field,
DomainRequest.creator.field,
DomainRequest.requested_domain.field,
@ -1377,7 +1365,6 @@ class AuditedAdminTest(TestCase):
# Senior offical is commented out for now - this is alphabetized
# and this test does not accurately reflect that.
# DomainInformation.senior_official.field,
DomainInformation.submitter.field,
# DomainInformation.creator.field,
(DomainInformation.domain.field, ["name"]),
(DomainInformation.domain_request.field, ["requested_domain__name"]),
@ -1686,91 +1673,6 @@ class TestContactAdmin(TestCase):
self.assertEqual(readonly_fields, expected_fields)
def test_change_view_for_joined_contact_five_or_less(self):
"""Create a contact, join it to 4 domain requests.
Assert that the warning on the contact form lists 4 joins."""
with less_console_noise():
self.client.force_login(self.superuser)
# Create an instance of the model
contact, _ = Contact.objects.get_or_create(
first_name="Henry",
last_name="McFakerson",
)
# join it to 4 domain requests.
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
domain_request3 = completed_domain_request(submitter=contact, name="city3.gov")
domain_request4 = completed_domain_request(submitter=contact, name="city4.gov")
with patch("django.contrib.messages.warning") as mock_warning:
# Use the test client to simulate the request
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
# Assert that the error message was called with the correct argument
# Note: The 5th join will be a user.
mock_warning.assert_called_once_with(
response.wsgi_request,
"<ul class='messagelist_content-list--unstyled'>"
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
f"domainrequest/{domain_request1.pk}/change/'>city1.gov</a></li>"
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
f"domainrequest/{domain_request2.pk}/change/'>city2.gov</a></li>"
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
f"domainrequest/{domain_request3.pk}/change/'>city3.gov</a></li>"
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
f"domainrequest/{domain_request4.pk}/change/'>city4.gov</a></li>"
"</ul>",
)
# cleanup this test
DomainRequest.objects.all().delete()
contact.delete()
def test_change_view_for_joined_contact_five_or_more(self):
"""Create a contact, join it to 6 domain requests.
Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis."""
with less_console_noise():
self.client.force_login(self.superuser)
# Create an instance of the model
# join it to 6 domain requests.
contact, _ = Contact.objects.get_or_create(
first_name="Henry",
last_name="McFakerson",
)
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
domain_request3 = completed_domain_request(submitter=contact, name="city3.gov")
domain_request4 = completed_domain_request(submitter=contact, name="city4.gov")
domain_request5 = completed_domain_request(submitter=contact, name="city5.gov")
completed_domain_request(submitter=contact, name="city6.gov")
with patch("django.contrib.messages.warning") as mock_warning:
# Use the test client to simulate the request
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
logger.debug(mock_warning)
# Assert that the error message was called with the correct argument
# Note: The 6th join will be a user.
mock_warning.assert_called_once_with(
response.wsgi_request,
"<ul class='messagelist_content-list--unstyled'>"
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
f"domainrequest/{domain_request1.pk}/change/'>city1.gov</a></li>"
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
f"domainrequest/{domain_request2.pk}/change/'>city2.gov</a></li>"
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
f"domainrequest/{domain_request3.pk}/change/'>city3.gov</a></li>"
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
f"domainrequest/{domain_request4.pk}/change/'>city4.gov</a></li>"
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
f"domainrequest/{domain_request5.pk}/change/'>city5.gov</a></li>"
"</ul>"
"<p class='font-sans-3xs'>And 1 more...</p>",
)
# cleanup this test
DomainRequest.objects.all().delete()
contact.delete()
class TestVerifiedByStaffAdmin(TestCase):

View file

@ -427,13 +427,6 @@ class TestDomainAdminWithClient(TestCase):
# Check for the field itself
self.assertContains(response, "Meoward Jones")
# == Check for the submitter == #
self.assertContains(response, "mayor@igorville.gov")
self.assertContains(response, "Admin Tester")
self.assertContains(response, "(555) 555 5556")
self.assertContains(response, "Testy2 Tester2")
# == Check for the senior_official == #
self.assertContains(response, "testy@town.com")
self.assertContains(response, "Chief Tester")

View file

@ -100,7 +100,7 @@ class TestDomainRequestAdmin(MockEppLib):
SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title")
contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson")
domain_request = completed_domain_request(submitter=contact, name="city1.gov")
domain_request = completed_domain_request(name="city1.gov")
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
model_admin = AuditedAdmin(DomainRequest, self.site)
@ -155,11 +155,7 @@ class TestDomainRequestAdmin(MockEppLib):
# These should exist in the response
expected_values = [
("creator", "Person who submitted the domain request; will not receive email updates"),
(
"submitter",
'Person listed under "your contact information" in the request form; will receive email updates',
),
("creator", "Person who submitted the domain request. Will receive email updates"),
("approved_domain", "Domain associated with this request; will be blank until request is approved"),
("no_other_contacts_rationale", "Required if creator does not list other employees"),
("alternative_domains", "Other domain names the creator provided for consideration"),
@ -442,8 +438,8 @@ class TestDomainRequestAdmin(MockEppLib):
self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",))
@less_console_noise_decorator
def test_submitter_sortable(self):
"""Tests if the DomainRequest sorts by submitter correctly"""
def test_creator_sortable(self):
"""Tests if the DomainRequest sorts by creator correctly"""
self.client.force_login(self.superuser)
multiple_unalphabetical_domain_objects("domain_request")
@ -457,8 +453,8 @@ class TestDomainRequestAdmin(MockEppLib):
self.test_helper.assert_table_sorted(
"13",
(
"submitter__first_name",
"submitter__last_name",
"creator__first_name",
"creator__last_name",
),
)
@ -466,8 +462,8 @@ class TestDomainRequestAdmin(MockEppLib):
self.test_helper.assert_table_sorted(
"-13",
(
"-submitter__first_name",
"-submitter__last_name",
"-creator__first_name",
"-creator__last_name",
),
)
@ -665,15 +661,24 @@ class TestDomainRequestAdmin(MockEppLib):
def test_action_needed_sends_reason_email_prod_bcc(self):
"""When an action needed reason is set, an email is sent out and help@get.gov
is BCC'd in production"""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
# Create fake creator
EMAIL = "meoward.jones@igorville.gov"
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email=EMAIL,
phone="(555) 123 12345",
title="Treat inspector",
)
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
User.objects.filter(email=EMAIL).delete()
in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
action_needed = DomainRequest.DomainRequestStatus.ACTION_NEEDED
# Create a sample domain request
domain_request = completed_domain_request(status=in_review)
domain_request = completed_domain_request(status=in_review, user=_creator)
# Test the email sent out for already_has_domains
already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS
@ -702,7 +707,7 @@ class TestDomainRequestAdmin(MockEppLib):
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so)
self.assert_email_is_accurate(
"SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, EMAIL, bcc_email_address=BCC_EMAIL
"SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, _creator.email, bcc_email_address=BCC_EMAIL
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
@ -723,7 +728,7 @@ class TestDomainRequestAdmin(MockEppLib):
)
domain_request.refresh_from_db()
self.assert_email_is_accurate("custom email content", 4, EMAIL, bcc_email_address=BCC_EMAIL)
self.assert_email_is_accurate("custom email content", 4, _creator.email, bcc_email_address=BCC_EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
# Tests if a new email gets sent when just the email is changed.
@ -747,7 +752,9 @@ class TestDomainRequestAdmin(MockEppLib):
action_needed_reason=eligibility_unclear,
action_needed_reason_email="custom content when starting anew",
)
self.assert_email_is_accurate("custom content when starting anew", 5, EMAIL, bcc_email_address=BCC_EMAIL)
self.assert_email_is_accurate(
"custom content when starting anew", 5, _creator.email, bcc_email_address=BCC_EMAIL
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 6)
# def test_action_needed_sends_reason_email_prod_bcc(self):
@ -811,22 +818,31 @@ class TestDomainRequestAdmin(MockEppLib):
Also test that the default email set in settings is NOT BCCd on non-prod whenever
an email does go out."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
EMAIL = "meoward.jones@igorville.gov"
# Create a sample domain request
domain_request = completed_domain_request()
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email=EMAIL,
phone="(555) 123 12345",
title="Treat inspector",
)
# Create a sample domain request and whitelist user email
domain_request = completed_domain_request(user=_creator)
AllowedEmail.objects.get_or_create(email=_creator.email)
# Test Submitted Status from started
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, True)
self.assert_email_is_accurate("We received your .gov domain request.", 0, _creator.email, True)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Test Withdrawn Status
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN)
self.assert_email_is_accurate(
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL, True
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, _creator.email, True
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
@ -885,30 +901,37 @@ class TestDomainRequestAdmin(MockEppLib):
Also test that the default email set in settings IS BCCd on prod whenever
an email does go out."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email="meoward.jones@igorville.gov",
phone="(555) 123 12345",
title="Treat inspector",
)
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
# Create a sample domain request
domain_request = completed_domain_request()
# Create a sample domain request and whitelist user email
domain_request = completed_domain_request(user=_creator)
AllowedEmail.objects.get_or_create(email=_creator.email)
# Test Submitted Status from started
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL)
self.assert_email_is_accurate("We received your .gov domain request.", 0, _creator.email, False, BCC_EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Test Withdrawn Status
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN)
self.assert_email_is_accurate(
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, _creator.email
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (from withdrawn)
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL)
self.assert_email_is_accurate("We received your .gov domain request.", 0, _creator.email, False, BCC_EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to IN_REVIEW
@ -938,16 +961,23 @@ class TestDomainRequestAdmin(MockEppLib):
"""When transitioning to approved on a domain request,
an email is sent out every time."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email="meoward.jones@igorville.gov",
phone="(555) 123 12345",
title="Treat inspector",
)
# Create a sample domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
# Create a sample domain request and whitelist user email
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
AllowedEmail.objects.get_or_create(email=_creator.email)
# Test Submitted Status
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, EMAIL)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, _creator.email)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Test Withdrawn Status
@ -956,7 +986,7 @@ class TestDomainRequestAdmin(MockEppLib):
DomainRequest.DomainRequestStatus.REJECTED,
DomainRequest.RejectionReasons.DOMAIN_PURPOSE,
)
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL)
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, _creator.email)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (No new email should be sent)
@ -968,12 +998,19 @@ class TestDomainRequestAdmin(MockEppLib):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is domain purpose."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email="meoward.jones@igorville.gov",
phone="(555) 123 12345",
title="Treat inspector",
)
# Create a sample domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
# Create a sample domain request and whitelist user email
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
AllowedEmail.objects.get_or_create(email=_creator.email)
# Reject for reason DOMAIN_PURPOSE and test email
self.transition_state_and_send_email(
@ -984,13 +1021,13 @@ class TestDomainRequestAdmin(MockEppLib):
self.assert_email_is_accurate(
"Your domain request was rejected because the purpose you provided did not meet our \nrequirements.",
0,
EMAIL,
_creator.email,
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
@less_console_noise_decorator
@ -998,12 +1035,19 @@ class TestDomainRequestAdmin(MockEppLib):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is requestor."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email="meoward.jones@igorville.gov",
phone="(555) 123 12345",
title="Treat inspector",
)
# Create a sample domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
# Create a sample domain request and whitelist user email
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
AllowedEmail.objects.get_or_create(email=_creator.email)
# Reject for reason REQUESTOR and test email including dynamic organization name
self.transition_state_and_send_email(
@ -1013,13 +1057,13 @@ class TestDomainRequestAdmin(MockEppLib):
"Your domain request was rejected because we dont believe youre eligible to request a \n.gov "
"domain on behalf of Testorg",
0,
EMAIL,
_creator.email,
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
@less_console_noise_decorator
@ -1027,12 +1071,19 @@ class TestDomainRequestAdmin(MockEppLib):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is second domain."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email="meoward.jones@igorville.gov",
phone="(555) 123 12345",
title="Treat inspector",
)
# Create a sample domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
# Create a sample domain request and whitelist user email
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
AllowedEmail.objects.get_or_create(email=_creator.email)
# Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name
self.transition_state_and_send_email(
@ -1040,25 +1091,35 @@ class TestDomainRequestAdmin(MockEppLib):
DomainRequest.DomainRequestStatus.REJECTED,
DomainRequest.RejectionReasons.SECOND_DOMAIN_REASONING,
)
self.assert_email_is_accurate("Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL)
self.assert_email_is_accurate(
"Your domain request was rejected because Testorg has a .gov domain.", 0, _creator.email
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
@less_console_noise_decorator
def test_save_model_sends_rejected_email_contacts_or_org_legitimacy(self):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is contacts or org legitimacy."""
# Create fake creator
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
EMAIL = "meoward.jones@igorville.gov"
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email=EMAIL,
phone="(555) 123 12345",
title="Treat inspector",
)
# Create a sample domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
# Create a sample domain request and whitelist user email
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
AllowedEmail.objects.get_or_create(email=_creator.email)
# Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name
self.transition_state_and_send_email(
@ -1070,13 +1131,13 @@ class TestDomainRequestAdmin(MockEppLib):
"Your domain request was rejected because we could not verify the organizational \n"
"contacts you provided. If you have questions or comments, reply to this email.",
0,
EMAIL,
_creator.email,
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
@less_console_noise_decorator
@ -1084,12 +1145,19 @@ class TestDomainRequestAdmin(MockEppLib):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is org eligibility."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email="meoward.jones@igorville.gov",
phone="(555) 123 12345",
title="Treat inspector",
)
# Create a sample domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
# Create a sample domain request and whitelist user email
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
AllowedEmail.objects.get_or_create(email=_creator.email)
# Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name
self.transition_state_and_send_email(
@ -1101,26 +1169,32 @@ class TestDomainRequestAdmin(MockEppLib):
"Your domain request was rejected because we determined that Testorg is not \neligible for "
"a .gov domain.",
0,
EMAIL,
_creator.email,
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
@less_console_noise_decorator
def test_save_model_sends_rejected_email_naming(self):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is naming."""
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email="meoward.jones@igorville.gov",
phone="(555) 123 12345",
title="Treat inspector",
)
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
# Create a sample domain request and whitelist user email
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
AllowedEmail.objects.get_or_create(email=_creator.email)
# Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name
self.transition_state_and_send_email(
@ -1129,13 +1203,13 @@ class TestDomainRequestAdmin(MockEppLib):
DomainRequest.RejectionReasons.NAMING_REQUIREMENTS,
)
self.assert_email_is_accurate(
"Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL
"Your domain request was rejected because it does not meet our naming requirements.", 0, _creator.email
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
@less_console_noise_decorator
@ -1143,12 +1217,19 @@ class TestDomainRequestAdmin(MockEppLib):
"""When transitioning to rejected on a domain request, an email is sent
explaining why when the reason is other."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email="meoward.jones@igorville.gov",
phone="(555) 123 12345",
title="Treat inspector",
)
# Create a sample domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
# Create a sample domain request and whitelist user email
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
AllowedEmail.objects.get_or_create(email=_creator.email)
# Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name
self.transition_state_and_send_email(
@ -1156,12 +1237,12 @@ class TestDomainRequestAdmin(MockEppLib):
DomainRequest.DomainRequestStatus.REJECTED,
DomainRequest.RejectionReasons.OTHER,
)
self.assert_email_is_accurate("Choosing a .gov domain name", 0, EMAIL)
self.assert_email_is_accurate("Choosing a .gov domain name", 0, _creator.email)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Approve
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
@less_console_noise_decorator
@ -1225,23 +1306,30 @@ class TestDomainRequestAdmin(MockEppLib):
"""When transitioning to withdrawn on a domain request,
an email is sent out every time."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email="meoward.jones@igorville.gov",
phone="(555) 123 12345",
title="Treat inspector",
)
# Create a sample domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
# Create a sample domain request and whitelists user email
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
AllowedEmail.objects.get_or_create(email=_creator.email)
# Test Submitted Status
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN)
self.assert_email_is_accurate(
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, EMAIL
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, _creator.email
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Test Withdrawn Status
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
self.assert_email_is_accurate("We received your .gov domain request.", 1, EMAIL)
self.assert_email_is_accurate("We received your .gov domain request.", 1, _creator.email)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (No new email should be sent)
@ -1420,16 +1508,6 @@ class TestDomainRequestAdmin(MockEppLib):
# Check for the field itself
self.assertContains(response, "Meoward Jones")
# == Check for the submitter == #
self.assertContains(response, "mayor@igorville.gov", count=2)
expected_submitter_fields = [
# Field, expected value
("title", "Admin Tester"),
("phone", "(555) 555 5556"),
]
self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
self.assertContains(response, "Testy2 Tester2")
# == Check for the senior_official == #
self.assertContains(response, "testy@town.com", count=2)
expected_so_fields = [
@ -1450,7 +1528,7 @@ class TestDomainRequestAdmin(MockEppLib):
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
# Test for the copy link
self.assertContains(response, "button--clipboard", count=5)
self.assertContains(response, "button--clipboard", count=4)
# Test that Creator counts display properly
self.assertNotContains(response, "Approved domains")
@ -1585,7 +1663,6 @@ class TestDomainRequestAdmin(MockEppLib):
"senior_official",
"approved_domain",
"requested_domain",
"submitter",
"purpose",
"no_other_contacts_rationale",
"anything_else",
@ -1624,7 +1701,6 @@ class TestDomainRequestAdmin(MockEppLib):
"approved_domain",
"alternative_domains",
"purpose",
"submitter",
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",

View file

@ -7,7 +7,7 @@ from waffle.testutils import override_flag
from registrar.utility import email
from registrar.utility.email import send_templated_email
from .common import completed_domain_request
from registrar.models import AllowedEmail
from registrar.models import AllowedEmail, User
from api.tests.common import less_console_noise_decorator
from datetime import datetime
@ -93,7 +93,6 @@ class TestEmails(TestCase):
# check for optional things
self.assertIn("Other employees from your organization:", body)
self.assertIn("Testy2 Tester2", body)
self.assertIn("Current websites:", body)
self.assertIn("city.com", body)
self.assertIn("About your organization:", body)
@ -130,14 +129,20 @@ class TestEmails(TestCase):
@less_console_noise_decorator
def test_submission_confirmation_other_contacts_spacing(self):
"""Test line spacing with other contacts."""
domain_request = completed_domain_request(has_other_contacts=True)
# Create fake creator
_creator = User.objects.create(
username="MrMeoward", first_name="Meoward", last_name="Jones", phone="(888) 888 8888"
)
# Create a fake domain request
domain_request = completed_domain_request(has_other_contacts=True, user=_creator)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
self.assertIn("Other employees from your organization:", body)
# spacing should be right between adjacent elements
self.assertRegex(body, r"5556\n\nOther employees")
self.assertRegex(body, r"8888\n\nOther employees")
self.assertRegex(body, r"5557\n\nAnything else")
@boto3_mocking.patching
@ -150,7 +155,6 @@ class TestEmails(TestCase):
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
# spacing should be right between adjacent elements
self.assertRegex(body, r"5556\n\nOther employees")
self.assertRegex(body, r"None\n\nAnything else")
@boto3_mocking.patching

View file

@ -10,7 +10,6 @@ from registrar.forms.domain_request_wizard import (
DotGovDomainForm,
SeniorOfficialForm,
OrganizationContactForm,
YourContactForm,
OtherContactsForm,
RequirementsForm,
TribalGovernmentForm,
@ -366,19 +365,6 @@ class TestFormValidation(MockEppLib):
["Response must be less than 2000 characters."],
)
def test_your_contact_email_invalid(self):
"""must be a valid email address."""
form = YourContactForm(data={"email": "boss@boss"})
self.assertEqual(
form.errors["email"],
["Enter your email address in the required format, like name@example.com."],
)
def test_your_contact_phone_invalid(self):
"""Must be a valid phone number."""
form = YourContactForm(data={"phone": "boss@boss"})
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number."))
def test_other_contact_email_invalid(self):
"""must be a valid email address."""
form = OtherContactsForm(data={"email": "splendid@boss"})

View file

@ -170,7 +170,6 @@ class TestDomainRequest(TestCase):
zipcode="12345-6789",
senior_official=contact,
requested_domain=domain,
submitter=contact,
purpose="Igorville rules!",
anything_else="All of Igorville loves the dotgov program.",
is_policy_acknowledged=True,
@ -197,7 +196,6 @@ class TestDomainRequest(TestCase):
state_territory="CA",
zipcode="12345-6789",
senior_official=contact,
submitter=contact,
purpose="Igorville rules!",
anything_else="All of Igorville loves the dotgov program.",
is_policy_acknowledged=True,
@ -225,7 +223,7 @@ class TestDomainRequest(TestCase):
site = DraftDomain.objects.create(name="igorville.gov")
domain_request = DomainRequest.objects.create(creator=user, requested_domain=site)
# no submitter email so this emits a log warning
# no email sent to creator so this emits a log warning
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
@ -258,19 +256,11 @@ class TestDomainRequest(TestCase):
email_allowed.delete()
@override_flag("profile_feature", active=False)
@less_console_noise_decorator
def test_submit_from_started_sends_email(self):
msg = "Create a domain request and submit it and see if email was sent."
domain_request = completed_domain_request(submitter=self.dummy_user, user=self.dummy_user_2)
self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hello")
@override_flag("profile_feature", active=True)
@less_console_noise_decorator
def test_submit_from_started_sends_email_to_creator(self):
"""Tests if, when the profile feature flag is on, we send an email to the creator"""
"""tests that we send an email to the creator"""
msg = "Create a domain request and submit it and see if email was sent when the feature flag is on."
domain_request = completed_domain_request(submitter=self.dummy_user, user=self.dummy_user_2)
domain_request = completed_domain_request(user=self.dummy_user_2)
self.check_email_sent(
domain_request, msg, "submit", 1, expected_content="Lava", expected_email="intern@igorville.com"
)
@ -278,10 +268,9 @@ class TestDomainRequest(TestCase):
@less_console_noise_decorator
def test_submit_from_withdrawn_sends_email(self):
msg = "Create a withdrawn domain request and submit it and see if email was sent."
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.WITHDRAWN, submitter=self.dummy_user
)
self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hello")
user, _ = User.objects.get_or_create(username="testy")
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.WITHDRAWN, user=user)
self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hi", expected_email=user.email)
@less_console_noise_decorator
def test_submit_from_action_needed_does_not_send_email(self):
@ -298,26 +287,25 @@ class TestDomainRequest(TestCase):
@less_console_noise_decorator
def test_approve_sends_email(self):
msg = "Create a domain request and approve it and see if email was sent."
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW, submitter=self.dummy_user
)
self.check_email_sent(domain_request, msg, "approve", 1, expected_content="Hello")
user, _ = User.objects.get_or_create(username="testy")
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=user)
self.check_email_sent(domain_request, msg, "approve", 1, expected_content="approved", expected_email=user.email)
@less_console_noise_decorator
def test_withdraw_sends_email(self):
msg = "Create a domain request and withdraw it and see if email was sent."
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW, submitter=self.dummy_user
user, _ = User.objects.get_or_create(username="testy")
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=user)
self.check_email_sent(
domain_request, msg, "withdraw", 1, expected_content="withdrawn", expected_email=user.email
)
self.check_email_sent(domain_request, msg, "withdraw", 1, expected_content="Hello")
@less_console_noise_decorator
def test_reject_sends_email(self):
msg = "Create a domain request and reject it and see if email was sent."
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.APPROVED, submitter=self.dummy_user
)
self.check_email_sent(domain_request, msg, "reject", 1, expected_content="Hello")
user, _ = User.objects.get_or_create(username="testy")
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED, user=user)
self.check_email_sent(domain_request, msg, "reject", 1, expected_content="Hi", expected_email=user.email)
@less_console_noise_decorator
def test_reject_with_prejudice_does_not_send_email(self):
@ -2139,7 +2127,6 @@ class TestDomainRequestIncomplete(TestCase):
senior_official=so,
requested_domain=draft_domain,
purpose="Some purpose",
submitter=you,
no_other_contacts_rationale=None,
has_cisa_representative=True,
cisa_representative_email="somerep@cisa.com",
@ -2268,13 +2255,6 @@ class TestDomainRequestIncomplete(TestCase):
self.domain_request.save()
self.assertFalse(self.domain_request._is_purpose_complete())
@less_console_noise_decorator
def test_is_submitter_complete(self):
self.assertTrue(self.domain_request._is_submitter_complete())
self.domain_request.submitter = None
self.domain_request.save()
self.assertFalse(self.domain_request._is_submitter_complete())
@less_console_noise_decorator
def test_is_other_contacts_complete_missing_one_field(self):
self.assertTrue(self.domain_request._is_other_contacts_complete())

View file

@ -369,19 +369,12 @@ class HomeTests(TestWithUser):
last_name="Mars",
)
# Attach a user object to a contact (should not be deleted)
contact_user, _ = Contact.objects.get_or_create(
first_name="Hank",
last_name="McFakey",
)
site = DraftDomain.objects.create(name="igorville.gov")
domain_request = DomainRequest.objects.create(
creator=self.user,
requested_domain=site,
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
senior_official=contact,
submitter=contact_user,
)
domain_request.other_contacts.set([contact_2])
@ -392,7 +385,6 @@ class HomeTests(TestWithUser):
requested_domain=site_2,
status=DomainRequest.DomainRequestStatus.STARTED,
senior_official=contact_2,
submitter=contact_shared,
)
domain_request_2.other_contacts.set([contact_shared])
@ -409,8 +401,6 @@ class HomeTests(TestWithUser):
# Check if the orphaned contacts were deleted
orphan = Contact.objects.filter(id=contact.id)
self.assertFalse(orphan.exists())
orphan = Contact.objects.filter(id=contact_user.id)
self.assertFalse(orphan.exists())
try:
edge_case = Contact.objects.filter(id=contact_2.id).get()
@ -455,7 +445,6 @@ class HomeTests(TestWithUser):
requested_domain=site,
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
senior_official=contact,
submitter=contact_user,
)
domain_request.other_contacts.set([contact_2])
@ -466,7 +455,6 @@ class HomeTests(TestWithUser):
requested_domain=site_2,
status=DomainRequest.DomainRequestStatus.STARTED,
senior_official=contact_2,
submitter=contact_shared,
)
domain_request_2.other_contacts.set([contact_shared])
@ -506,7 +494,13 @@ class HomeTests(TestWithUser):
phone = "8003111234"
status = User.RESTRICTED
restricted_user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone, status=status
username=username,
first_name=first_name,
last_name=last_name,
email=email,
phone=phone,
status=status,
title="title",
)
self.client.force_login(restricted_user)
response = self.client.get("/request/", follow=True)
@ -558,7 +552,6 @@ class FinishUserProfileTests(TestWithUser, WebTest):
return page.follow() if follow else page
@less_console_noise_decorator
@override_flag("profile_feature", active=True)
def test_full_name_initial_value(self):
"""Test that full_name initial value is empty when first_name or last_name is empty.
This will later be displayed as "unknown" using javascript."""
@ -612,8 +605,8 @@ class FinishUserProfileTests(TestWithUser, WebTest):
incomplete_regular_user.delete()
@less_console_noise_decorator
def test_new_user_with_profile_feature_on(self):
"""Tests that a new user is redirected to the profile setup page when profile_feature is on"""
def test_new_user(self):
"""Tests that a new user is redirected to the profile setup page"""
username_regular_incomplete = "test_regular_user_incomplete"
first_name_2 = "Incomplete"
email_2 = "unicorn@igorville.com"
@ -626,12 +619,10 @@ class FinishUserProfileTests(TestWithUser, WebTest):
)
self.app.set_user(incomplete_regular_user.username)
with override_flag("profile_feature", active=True):
# This will redirect the user to the setup page.
# Follow implicity checks if our redirect is working.
finish_setup_page = self.app.get(reverse("home")).follow()
self._set_session_cookie()
# Assert that we're on the right page
self.assertContains(finish_setup_page, "Finish setting up your profile")
@ -675,7 +666,6 @@ class FinishUserProfileTests(TestWithUser, WebTest):
verification_type=User.VerificationTypeChoices.REGULAR,
)
self.app.set_user(incomplete_regular_user.username)
with override_flag("profile_feature", active=True):
# This will redirect the user to the setup page.
# Follow implicity checks if our redirect is working.
finish_setup_page = self.app.get(reverse("home")).follow()
@ -713,8 +703,8 @@ class FinishUserProfileTests(TestWithUser, WebTest):
incomplete_regular_user.delete()
@less_console_noise_decorator
def test_new_user_goes_to_domain_request_with_profile_feature_on(self):
"""Tests that a new user is redirected to the domain request page when profile_feature is on"""
def test_new_user_goes_to_domain_request(self):
"""Tests that a new user is redirected to the domain request page"""
username_regular_incomplete = "test_regular_user_incomplete"
first_name_2 = "Incomplete"
email_2 = "unicorn@igorville.com"
@ -726,7 +716,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
verification_type=User.VerificationTypeChoices.REGULAR,
)
self.app.set_user(incomplete_regular_user.username)
with override_flag("profile_feature", active=True):
with override_flag("", active=True):
# This will redirect the user to the setup page
finish_setup_page = self.app.get(reverse("domain-request:")).follow()
self._set_session_cookie()
@ -770,25 +760,6 @@ class FinishUserProfileTests(TestWithUser, WebTest):
self.assertContains(completed_setup_page, "Youre about to start your .gov domain request")
incomplete_regular_user.delete()
@less_console_noise_decorator
def test_new_user_with_profile_feature_off(self):
"""Tests that a new user is not redirected to the profile setup page when profile_feature is off"""
with override_flag("profile_feature", active=False):
response = self.client.get("/")
self.assertNotContains(response, "Finish setting up your profile")
@less_console_noise_decorator
def test_new_user_goes_to_domain_request_with_profile_feature_off(self):
"""Tests that a new user is redirected to the domain request page
when profile_feature is off but not the setup page"""
with override_flag("profile_feature", active=False):
response = self.client.get("/request/")
self.assertNotContains(response, "Finish setting up your profile")
self.assertNotContains(response, "What contact information should we use to reach you?")
self.assertContains(response, "Youre about to start your .gov domain request")
class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
"""A series of tests that target the user profile page intercept for incomplete IAL1 user profiles."""
@ -828,8 +799,8 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
return page.follow() if follow else page
@less_console_noise_decorator
def test_new_user_with_profile_feature_on(self):
"""Tests that a new user is redirected to the profile setup page when profile_feature is on,
def test_new_user(self):
"""Tests that a new user is redirected to the profile setup page,
and testing that the confirmation modal is present"""
username_other_incomplete = "test_other_user_incomplete"
first_name_2 = "Incomplete"
@ -843,7 +814,6 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
verification_type=User.VerificationTypeChoices.VERIFIED_BY_STAFF,
)
self.app.set_user(incomplete_other_user.username)
with override_flag("profile_feature", active=True):
# This will redirect the user to the user profile page.
# Follow implicity checks if our redirect is working.
user_profile_page = self.app.get(reverse("home")).follow()
@ -893,9 +863,7 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
# NOTE: "anage" is not a typo. It is to accomodate the fact that the "m" is uppercase in one
# instance and lowercase in the other.
self.assertContains(save_page, "anage your domains", count=2)
self.assertNotContains(
save_page, "Before you can manage your domains, we need you to add contact information"
)
self.assertNotContains(save_page, "Before you can manage your domains, we need you to add contact information")
# Assert that modal does not appear on subsequent submits
self.assertNotContains(save_page, "domain registrants must maintain accurate contact information")
@ -927,106 +895,59 @@ class UserProfileTests(TestWithUser, WebTest):
DomainInformation.objects.all().delete()
@less_console_noise_decorator
def error_500_main_nav_with_profile_feature_turned_on(self):
"""test that Your profile is in main nav of 500 error page when profile_feature is on.
def error_500_main_nav(self):
"""test that Your profile is in main nav of 500 error page.
Our treatment of 401 and 403 error page handling with that waffle feature is similar, so we
assume that the same test results hold true for 401 and 403."""
with override_flag("profile_feature", active=True):
with self.assertRaises(Exception):
response = self.client.get(reverse("home"), follow=True)
self.assertEqual(response.status_code, 500)
self.assertContains(response, "Your profile")
@less_console_noise_decorator
def error_500_main_nav_with_profile_feature_turned_off(self):
"""test that Your profile is not in main nav of 500 error page when profile_feature is off.
Our treatment of 401 and 403 error page handling with that waffle feature is similar, so we
assume that the same test results hold true for 401 and 403."""
with override_flag("profile_feature", active=False):
with self.assertRaises(Exception):
response = self.client.get(reverse("home"), follow=True)
self.assertEqual(response.status_code, 500)
self.assertNotContains(response, "Your profile")
@less_console_noise_decorator
def test_home_page_main_nav_with_profile_feature_on(self):
"""test that Your profile is in main nav of home page when profile_feature is on"""
with override_flag("profile_feature", active=True):
def test_home_page_main_nav(self):
"""test that Your profile is in main nav of home page"""
response = self.client.get("/", follow=True)
self.assertContains(response, "Your profile")
@less_console_noise_decorator
def test_home_page_main_nav_with_profile_feature_off(self):
"""test that Your profile is not in main nav of home page when profile_feature is off"""
with override_flag("profile_feature", active=False):
response = self.client.get("/", follow=True)
self.assertNotContains(response, "Your profile")
@less_console_noise_decorator
def test_new_request_main_nav_with_profile_feature_on(self):
"""test that Your profile is in main nav of new request when profile_feature is on"""
with override_flag("profile_feature", active=True):
def test_new_request_main_nav(self):
"""test that Your profile is in main nav of new request"""
response = self.client.get("/request/", follow=True)
self.assertContains(response, "Your profile")
@less_console_noise_decorator
def test_new_request_main_nav_with_profile_feature_off(self):
"""test that Your profile is not in main nav of new request when profile_feature is off"""
with override_flag("profile_feature", active=False):
response = self.client.get("/request/", follow=True)
self.assertNotContains(response, "Your profile")
@less_console_noise_decorator
def test_user_profile_main_nav_with_profile_feature_on(self):
"""test that Your profile is in main nav of user profile when profile_feature is on"""
with override_flag("profile_feature", active=True):
def test_user_profile_main_nav(self):
"""test that Your profile is in main nav of user profile"""
response = self.client.get("/user-profile", follow=True)
self.assertContains(response, "Your profile")
@less_console_noise_decorator
def test_user_profile_returns_404_when_feature_off(self):
"""test that Your profile returns 404 when profile_feature is off"""
with override_flag("profile_feature", active=False):
response = self.client.get("/user-profile", follow=True)
self.assertEqual(response.status_code, 404)
@less_console_noise_decorator
def test_user_profile_back_button_when_coming_from_domain_request(self):
"""tests user profile when profile_feature is on,
"""tests user profile,
and when they are redirected from the domain request page"""
with override_flag("profile_feature", active=True):
response = self.client.get("/user-profile?redirect=domain-request:")
self.assertContains(response, "Your profile")
self.assertContains(response, "Go back to your domain request")
self.assertNotContains(response, "Back to manage your domains")
@less_console_noise_decorator
def test_domain_detail_profile_feature_on(self):
"""test that domain detail view when profile_feature is on"""
with override_flag("profile_feature", active=True):
def test_domain_detail_contains_your_profile(self):
"""Tests that the domain detail view contains 'your profile' rather than 'your contact information'"""
response = self.client.get(reverse("domain", args=[self.domain.pk]))
self.assertContains(response, "Your profile")
self.assertNotContains(response, "Your contact information")
@less_console_noise_decorator
def test_domain_your_contact_information_when_profile_feature_off(self):
"""test that Your contact information is accessible when profile_feature is off"""
with override_flag("profile_feature", active=False):
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True)
self.assertContains(response, "Your contact information")
@less_console_noise_decorator
def test_domain_your_contact_information_when_profile_feature_on(self):
"""test that Your contact information is not accessible when profile feature is on"""
with override_flag("profile_feature", active=True):
def test_domain_your_contact_information(self):
"""test that your contact information is not accessible"""
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True)
self.assertEqual(response.status_code, 404)
@less_console_noise_decorator
def test_request_when_profile_feature_on(self):
"""test that Your profile is in request page when profile feature is on"""
def test_profile_request_page(self):
"""test that your profile is in request"""
contact_user, _ = Contact.objects.get_or_create(
first_name="Hank",
@ -1038,44 +959,17 @@ class UserProfileTests(TestWithUser, WebTest):
requested_domain=site,
status=DomainRequest.DomainRequestStatus.SUBMITTED,
senior_official=contact_user,
submitter=contact_user,
)
with override_flag("profile_feature", active=True):
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
self.assertContains(response, "Your profile")
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True)
self.assertContains(response, "Your profile")
@less_console_noise_decorator
def test_request_when_profile_feature_off(self):
"""test that Your profile is not in request page when profile feature is off"""
contact_user, _ = Contact.objects.get_or_create(
first_name="Hank",
last_name="McFakerson",
)
site = DraftDomain.objects.create(name="igorville.gov")
domain_request = DomainRequest.objects.create(
creator=self.user,
requested_domain=site,
status=DomainRequest.DomainRequestStatus.SUBMITTED,
senior_official=contact_user,
submitter=contact_user,
)
with override_flag("profile_feature", active=False):
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
self.assertNotContains(response, "Your profile")
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True)
self.assertNotContains(response, "Your profile")
# cleanup
domain_request.delete()
site.delete()
@less_console_noise_decorator
def test_user_profile_form_submission(self):
"""test user profile form submission"""
self.app.set_user(self.user.username)
with override_flag("profile_feature", active=True):
profile_page = self.app.get(reverse("user-profile"))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)

View file

@ -161,7 +161,6 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain-dns-nameservers",
"domain-org-name-address",
"domain-senior-official",
"domain-your-contact-information",
"domain-security-email",
]:
with self.subTest(view_name=view_name):
@ -181,7 +180,6 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain-dns-nameservers",
"domain-org-name-address",
"domain-senior-official",
"domain-your-contact-information",
"domain-security-email",
]:
with self.subTest(view_name=view_name):
@ -202,7 +200,6 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain-dns-dnssec-dsdata",
"domain-org-name-address",
"domain-senior-official",
"domain-your-contact-information",
"domain-security-email",
]:
for domain in [
@ -726,7 +723,7 @@ class TestDomainManagers(TestDomainOverview):
email_address = "mayor@igorville.gov"
invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
other_user = User()
other_user = create_user()
other_user.save()
self.client.force_login(other_user)
mock_client = MagicMock()
@ -740,6 +737,12 @@ class TestDomainManagers(TestDomainOverview):
def test_domain_invitation_flow(self):
"""Send an invitation to a new user, log in and load the dashboard."""
email_address = "mayor@igorville.gov"
username = "mayor"
first_name = "First"
last_name = "Last"
title = "title"
phone = "8080102431"
title = "title"
User.objects.filter(email=email_address).delete()
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
@ -755,7 +758,9 @@ class TestDomainManagers(TestDomainOverview):
add_page.form.submit()
# user was invited, create them
new_user = User.objects.create(username=email_address, email=email_address)
new_user = User.objects.create(
username=username, email=email_address, first_name=first_name, last_name=last_name, title=title, phone=phone
)
# log them in to `self.app`
self.app.set_user(new_user.username)
# and manually call the on each login callback
@ -1301,7 +1306,9 @@ class TestDomainOrganization(TestDomainOverview):
"""Can load domain's org name and mailing address page."""
page = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
# once on the sidebar, once in the page title, once as H1
self.assertContains(page, "Organization name and mailing address", count=4)
self.assertContains(page, "/org-name-address")
self.assertContains(page, "Organization name and mailing address")
self.assertContains(page, "Organization</h1>")
@less_console_noise_decorator
def test_domain_org_name_address_content(self):
@ -1610,7 +1617,7 @@ class TestDomainSuborganization(TestDomainOverview):
# Test for the title change
self.assertContains(page, "Suborganization")
self.assertNotContains(page, "Organization name")
self.assertNotContains(page, "Organization")
# Test for the good value
self.assertContains(page, "Ice Cream")
@ -1624,22 +1631,6 @@ class TestDomainSuborganization(TestDomainOverview):
portfolio.delete()
class TestDomainContactInformation(TestDomainOverview):
@less_console_noise_decorator
def test_domain_your_contact_information(self):
"""Can load domain's your contact information page."""
page = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}))
self.assertContains(page, "Your contact information")
@less_console_noise_decorator
def test_domain_your_contact_information_content(self):
"""Logged-in user's contact information appears on the page."""
self.user.first_name = "Testy"
self.user.save()
page = self.app.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}))
self.assertContains(page, "Testy")
class TestDomainSecurityEmail(TestDomainOverview):
def test_domain_security_email_existing_security_contact(self):
"""Can load domain's security email page."""

View file

@ -200,7 +200,7 @@ class TestPortfolio(WebTest):
# Assert the response is a 200
self.assertEqual(response.status_code, 200)
# The label for Federal agency will always be a h4
self.assertContains(response, '<h4 class="read-only-label">Federal agency</h4>')
self.assertContains(response, '<h4 class="read-only-label">Organization name</h4>')
# The read only label for city will be a h4
self.assertContains(response, '<h4 class="read-only-label">City</h4>')
self.assertNotContains(response, 'for="id_city"')
@ -225,10 +225,10 @@ class TestPortfolio(WebTest):
# Assert the response is a 200
self.assertEqual(response.status_code, 200)
# The label for Federal agency will always be a h4
self.assertContains(response, '<h4 class="read-only-label">Federal agency</h4>')
self.assertContains(response, '<h4 class="read-only-label">Organization name</h4>')
# The read only label for city will be a h4
self.assertNotContains(response, '<h4 class="read-only-label">City</h4>')
self.assertNotContains(response, '<p class="read-only-value">Los Angeles</p>>')
self.assertNotContains(response, '<p class="read-only-value">Los Angeles</p>')
self.assertContains(response, 'for="id_city"')
@less_console_noise_decorator
@ -342,9 +342,7 @@ class TestPortfolio(WebTest):
user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
)
page = self.app.get(reverse("organization"))
self.assertContains(
page, "The name of your federal agency will be publicly listed as the domain registrant."
)
self.assertContains(page, "The name of your organization will be publicly listed as the domain registrant.")
@less_console_noise_decorator
def test_domain_org_name_address_content(self):
@ -837,3 +835,106 @@ class TestPortfolio(WebTest):
response = self.client.get(reverse("home"))
self.assertIsNone(self.client.session.get("portfolio"))
self.assertNotContains(response, "Hotel California")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_org_user_can_delete_own_domain_request_with_permission(self):
"""Test that an org user with edit permission can delete their own DomainRequest with a deletable status."""
# Assign the user to a portfolio with edit permission
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
)
# Create a domain request with status WITHDRAWN
domain_request = completed_domain_request(
name="test-domain.gov",
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
portfolio=self.portfolio,
)
domain_request.creator = self.user
domain_request.save()
self.client.force_login(self.user)
# Perform delete
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
# Check that the response is 200
self.assertEqual(response.status_code, 200)
# Check that the domain request no longer exists
self.assertFalse(DomainRequest.objects.filter(pk=domain_request.pk).exists())
domain_request.delete()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_delete_domain_request_as_org_user_without_permission_with_deletable_status(self):
"""Test that an org user without edit permission cant delete their DomainRequest even if status is deletable."""
# Assign the user to a portfolio without edit permission
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[],
)
# Create a domain request with status STARTED
domain_request = completed_domain_request(
name="test-domain.gov",
status=DomainRequest.DomainRequestStatus.STARTED,
portfolio=self.portfolio,
)
domain_request.creator = self.user
domain_request.save()
self.client.force_login(self.user)
# Attempt to delete
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
# Check response is 403 Forbidden
self.assertEqual(response.status_code, 403)
# Check that the domain request still exists
self.assertTrue(DomainRequest.objects.filter(pk=domain_request.pk).exists())
domain_request.delete()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_org_user_cannot_delete_others_domain_requests(self):
"""Test that an org user with edit permission cannot delete DomainRequests they did not create."""
# Assign the user to a portfolio with edit permission
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
)
# Create another user and a domain request
other_user = User.objects.create(username="other_user")
domain_request = completed_domain_request(
name="test-domain.gov",
status=DomainRequest.DomainRequestStatus.STARTED,
portfolio=self.portfolio,
)
domain_request.creator = other_user
domain_request.save()
self.client.force_login(self.user)
# Perform delete as self.user
response = self.client.post(reverse("domain-request-delete", kwargs={"pk": domain_request.pk}), follow=True)
# Check response is 403 Forbidden
self.assertEqual(response.status_code, 403)
# Check that the domain request still exists
self.assertTrue(DomainRequest.objects.filter(pk=domain_request.pk).exists())
domain_request.delete()

View file

@ -9,6 +9,7 @@ from .common import MockSESClient, completed_domain_request # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
from waffle.testutils import override_flag
from registrar.models import (
DomainRequest,
DraftDomain,
@ -391,41 +392,13 @@ class DomainRequestTests(TestWithUser, WebTest):
# the post request should return a redirect to the next form in
# the domain request page
self.assertEqual(purpose_result.status_code, 302)
self.assertEqual(purpose_result["Location"], "/request/your_contact/")
num_pages_tested += 1
# ---- YOUR CONTACT INFO PAGE ----
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_page = purpose_result.follow()
your_contact_form = your_contact_page.forms[0]
your_contact_form["your_contact-first_name"] = "Testy you"
your_contact_form["your_contact-last_name"] = "Tester you"
your_contact_form["your_contact-title"] = "Admin Tester"
your_contact_form["your_contact-email"] = "testy-admin@town.com"
your_contact_form["your_contact-phone"] = "(201) 555 5556"
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_result = your_contact_form.submit()
# validate that data from this step are being saved
domain_request = DomainRequest.objects.get() # there's only one
self.assertEqual(domain_request.submitter.first_name, "Testy you")
self.assertEqual(domain_request.submitter.last_name, "Tester you")
self.assertEqual(domain_request.submitter.title, "Admin Tester")
self.assertEqual(domain_request.submitter.email, "testy-admin@town.com")
self.assertEqual(domain_request.submitter.phone, "(201) 555 5556")
# the post request should return a redirect to the next form in
# the domain request page
self.assertEqual(your_contact_result.status_code, 302)
self.assertEqual(your_contact_result["Location"], "/request/other_contacts/")
self.assertEqual(purpose_result["Location"], "/request/other_contacts/")
num_pages_tested += 1
# ---- OTHER CONTACTS PAGE ----
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_page = your_contact_result.follow()
other_contacts_page = purpose_result.follow()
# This page has 3 forms in 1.
# Let's set the yes/no radios to enable the other contacts fieldsets
@ -535,11 +508,6 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(review_page, "city.gov")
self.assertContains(review_page, "city1.gov")
self.assertContains(review_page, "For all kinds of things.")
self.assertContains(review_page, "Testy you")
self.assertContains(review_page, "Tester you")
self.assertContains(review_page, "Admin Tester")
self.assertContains(review_page, "testy-admin@town.com")
self.assertContains(review_page, "(201) 555-5556")
self.assertContains(review_page, "Testy2")
self.assertContains(review_page, "Tester2")
self.assertContains(review_page, "Another Tester")
@ -747,41 +715,13 @@ class DomainRequestTests(TestWithUser, WebTest):
# the post request should return a redirect to the next form in
# the domain request page
self.assertEqual(purpose_result.status_code, 302)
self.assertEqual(purpose_result["Location"], "/request/your_contact/")
num_pages_tested += 1
# ---- YOUR CONTACT INFO PAGE ----
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_page = purpose_result.follow()
your_contact_form = your_contact_page.forms[0]
your_contact_form["your_contact-first_name"] = "Testy you"
your_contact_form["your_contact-last_name"] = "Tester you"
your_contact_form["your_contact-title"] = "Admin Tester"
your_contact_form["your_contact-email"] = "testy-admin@town.com"
your_contact_form["your_contact-phone"] = "(201) 555 5556"
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_result = your_contact_form.submit()
# validate that data from this step are being saved
domain_request = DomainRequest.objects.get() # there's only one
self.assertEqual(domain_request.submitter.first_name, "Testy you")
self.assertEqual(domain_request.submitter.last_name, "Tester you")
self.assertEqual(domain_request.submitter.title, "Admin Tester")
self.assertEqual(domain_request.submitter.email, "testy-admin@town.com")
self.assertEqual(domain_request.submitter.phone, "(201) 555 5556")
# the post request should return a redirect to the next form in
# the domain request page
self.assertEqual(your_contact_result.status_code, 302)
self.assertEqual(your_contact_result["Location"], "/request/other_contacts/")
self.assertEqual(purpose_result["Location"], "/request/other_contacts/")
num_pages_tested += 1
# ---- OTHER CONTACTS PAGE ----
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_page = your_contact_result.follow()
other_contacts_page = purpose_result.follow()
# This page has 3 forms in 1.
# Let's set the yes/no radios to enable the other contacts fieldsets
@ -1686,7 +1626,6 @@ class DomainRequestTests(TestWithUser, WebTest):
state_territory="NY",
zipcode="10002",
senior_official=so,
submitter=you,
creator=self.user,
status="started",
)
@ -1823,7 +1762,6 @@ class DomainRequestTests(TestWithUser, WebTest):
state_territory="NY",
zipcode="10002",
senior_official=so,
submitter=you,
creator=self.user,
status="started",
)
@ -1898,7 +1836,6 @@ class DomainRequestTests(TestWithUser, WebTest):
state_territory="NY",
zipcode="10002",
senior_official=so,
submitter=you,
creator=self.user,
status="started",
)
@ -1976,7 +1913,6 @@ class DomainRequestTests(TestWithUser, WebTest):
state_territory="NY",
zipcode="10002",
senior_official=so,
submitter=you,
creator=self.user,
status="started",
)
@ -2053,7 +1989,6 @@ class DomainRequestTests(TestWithUser, WebTest):
state_territory="NY",
zipcode="10002",
senior_official=so,
submitter=you,
creator=self.user,
status="started",
)
@ -2129,7 +2064,6 @@ class DomainRequestTests(TestWithUser, WebTest):
state_territory="NY",
zipcode="10002",
senior_official=so,
submitter=you,
creator=self.user,
status="started",
)
@ -2314,22 +2248,13 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertEquals("Testy2", senior_official.first_name)
@less_console_noise_decorator
def test_edit_submitter_in_place(self):
def test_edit_creator_in_place(self):
"""When you:
1. edit a submitter (your contact) which is not joined to another model,
1. edit a your user profile information,
2. then submit,
the domain request is linked to the existing submitter, and the submitter updated."""
the domain request also updates its creator data to reflect user profile changes."""
# Populate the database with a domain request that
# has a submitter
# We'll do it from scratch
you, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",
title="Chief Tester",
email="testy@town.com",
phone="(201) 555 5555",
)
# Populate the database with a domain request
domain_request, _ = DomainRequest.objects.get_or_create(
generic_org_type="federal",
federal_type="executive",
@ -2340,14 +2265,11 @@ class DomainRequestTests(TestWithUser, WebTest):
address_line1="address 1",
state_territory="NY",
zipcode="10002",
submitter=you,
creator=self.user,
status="started",
)
# submitter_pk is the initial pk of the submitter. set it before update
# to be able to verify after update that the same contact object is in place
submitter_pk = you.id
creator_pk = self.user.id
# prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
@ -2358,98 +2280,25 @@ class DomainRequestTests(TestWithUser, WebTest):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_page = self.app.get(reverse("domain-request:your_contact"))
profile_page = self.app.get("/user-profile")
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_form = your_contact_page.forms[0]
profile_form = profile_page.forms[0]
# Minimal check to ensure the form is loaded
self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy")
self.assertEqual(profile_form["first_name"].value, self.user.first_name)
# update the first name of the contact
your_contact_form["your_contact-first_name"] = "Testy2"
profile_form["first_name"] = "Testy2"
# Submit the updated form
your_contact_form.submit()
profile_form.submit()
domain_request.refresh_from_db()
updated_submitter = domain_request.submitter
self.assertEquals(submitter_pk, updated_submitter.id)
self.assertEquals("Testy2", updated_submitter.first_name)
@less_console_noise_decorator
def test_edit_submitter_creates_new(self):
"""When you:
1. edit an existing your contact which IS joined to another model,
2. then submit,
the domain request is linked to a new Contact, and the new Contact is updated."""
# Populate the database with a domain request that
# has submitter assigned to it, the submitter is also
# an other contact initially
# We'll do it from scratch
submitter, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",
title="Chief Tester",
email="testy@town.com",
phone="(201) 555 5555",
)
domain_request, _ = DomainRequest.objects.get_or_create(
generic_org_type="federal",
federal_type="executive",
purpose="Purpose of the site",
anything_else="No",
is_policy_acknowledged=True,
organization_name="Testorg",
address_line1="address 1",
state_territory="NY",
zipcode="10002",
submitter=submitter,
creator=self.user,
status="started",
)
domain_request.other_contacts.add(submitter)
# submitter_pk is the initial pk of the your contact. set it before update
# to be able to verify after update that the other contact is still in place
# and not updated, and that the new submitter has a new id
submitter_pk = submitter.id
# prime the form by visiting /edit
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_page = self.app.get(reverse("domain-request:your_contact"))
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_form = your_contact_page.forms[0]
# Minimal check to ensure the form is loaded
self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy")
# update the first name of the contact
your_contact_form["your_contact-first_name"] = "Testy2"
# Submit the updated form
your_contact_form.submit()
domain_request.refresh_from_db()
# assert that the other contact is not updated
other_contacts = domain_request.other_contacts.all()
other_contact = other_contacts[0]
self.assertEquals(submitter_pk, other_contact.id)
self.assertEquals("Testy", other_contact.first_name)
# assert that the submitter is updated
submitter = domain_request.submitter
self.assertEquals("Testy2", submitter.first_name)
updated_creator = domain_request.creator
self.assertEquals(creator_pk, updated_creator.id)
self.assertEquals("Testy2", updated_creator.first_name)
@less_console_noise_decorator
def test_domain_request_about_your_organiztion_interstate(self):
@ -2772,7 +2621,6 @@ class DomainRequestTests(TestWithUser, WebTest):
zipcode="10002",
senior_official=so,
requested_domain=domain,
submitter=you,
creator=self.user,
)
domain_request.other_contacts.add(other)
@ -2917,7 +2765,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
self.assertContains(detail_page, "city1.gov")
self.assertContains(detail_page, "Chief Tester")
self.assertContains(detail_page, "testy@town.com")
self.assertContains(detail_page, "Admin Tester")
self.assertContains(detail_page, "Status:")
@less_console_noise_decorator
@ -2934,7 +2781,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
self.assertContains(detail_page, "city.gov")
self.assertContains(detail_page, "Chief Tester")
self.assertContains(detail_page, "testy@town.com")
self.assertContains(detail_page, "Admin Tester")
self.assertContains(detail_page, "Status:")
@less_console_noise_decorator
@ -2948,7 +2794,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
self.assertContains(detail_page, "city1.gov")
self.assertContains(detail_page, "Chief Tester")
self.assertContains(detail_page, "testy@town.com")
self.assertContains(detail_page, "Admin Tester")
self.assertContains(detail_page, "Status:")
# click the "Withdraw request" button
mock_client = MockSESClient()
@ -2984,7 +2829,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
self.assertContains(detail_page, "city1.gov")
self.assertContains(detail_page, "Chief Tester")
self.assertContains(detail_page, "testy@town.com")
self.assertContains(detail_page, "Admin Tester")
self.assertContains(detail_page, "Status:")
# click the "Withdraw request" button
mock_client = MockSESClient()
@ -3014,7 +2858,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
self.assertContains(detail_page, "city1.gov")
self.assertContains(detail_page, "Chief Tester")
self.assertContains(detail_page, "testy@town.com")
self.assertContains(detail_page, "Admin Tester")
self.assertContains(detail_page, "Status:")
# Restricted user trying to withdraw results in 403 error
with less_console_noise():
@ -3113,10 +2956,10 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
self.assertEqual(detail_page.status_code, 200)
# 10 unlocked steps, one active step, the review step will have link_usa but not check_circle
self.assertContains(detail_page, "#check_circle", count=10)
self.assertContains(detail_page, "#check_circle", count=9)
# Type of organization
self.assertContains(detail_page, "usa-current", count=1)
self.assertContains(detail_page, "link_usa-checked", count=11)
self.assertContains(detail_page, "link_usa-checked", count=10)
else:
self.fail(f"Expected a redirect, but got a different response: {response}")
@ -3148,7 +2991,6 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
requested_domain=site,
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
senior_official=contact,
submitter=contact_user,
)
domain_request.other_contacts.set([contact_2])
@ -3174,12 +3016,12 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
# Now 'detail_page' contains the response after following the redirect
self.assertEqual(detail_page.status_code, 200)
# 5 unlocked steps (so, domain, submitter, other contacts, and current sites
# 5 unlocked steps (so, domain, other contacts, and current sites
# which unlocks if domain exists), one active step, the review step is locked
self.assertContains(detail_page, "#check_circle", count=5)
self.assertContains(detail_page, "#check_circle", count=4)
# Type of organization
self.assertContains(detail_page, "usa-current", count=1)
self.assertContains(detail_page, "link_usa-checked", count=5)
self.assertContains(detail_page, "link_usa-checked", count=4)
else:
self.fail(f"Expected a redirect, but got a different response: {response}")

View file

@ -458,3 +458,81 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
# Ensure no approved requests are included
for domain_request in data["domain_requests"]:
self.assertNotEqual(domain_request["status"], DomainRequest.DomainRequestStatus.APPROVED)
def test_search(self):
"""Tests our search functionality. We expect that search filters on creator only when we are in a portfolio"""
# Test search for domain name
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "lamb"})
self.assertEqual(response.status_code, 200)
data = response.json
self.assertEqual(len(data["domain_requests"]), 1)
requested_domain = data["domain_requests"][0]["requested_domain"]
self.assertEqual(requested_domain, "lamb-chops.gov")
# Test search for 'New domain request'
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "new domain"})
self.assertEqual(response.status_code, 200)
data = response.json
self.assertTrue(any(req["requested_domain"] is None for req in data["domain_requests"]))
# Test search with portfolio (including creator search)
self.client.force_login(self.user)
with override_flag("organization_feature", active=True), override_flag("organization_requests", active=True):
user_perm, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
response = self.app.get(
reverse("get_domain_requests_json"), {"search_term": "info", "portfolio": self.portfolio.id}
)
self.assertEqual(response.status_code, 200)
data = response.json
self.assertTrue(any(req["creator"].startswith("info") for req in data["domain_requests"]))
# Test search without portfolio (should not search on creator)
with override_flag("organization_feature", active=False), override_flag("organization_requests", active=False):
user_perm.delete()
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "info"})
self.assertEqual(response.status_code, 200)
data = response.json
self.assertEqual(len(data["domain_requests"]), 0)
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_status_filter(self):
"""Test that status filtering works properly"""
# Test a single status
response = self.app.get(reverse("get_domain_requests_json"), {"status": "started"})
self.assertEqual(response.status_code, 200)
data = response.json
self.assertTrue(all(req["status"] == "Started" for req in data["domain_requests"]))
# Test an invalid status
response = self.app.get(reverse("get_domain_requests_json"), {"status": "approved"})
self.assertEqual(response.status_code, 200)
data = response.json
self.assertEqual(len(data["domain_requests"]), 0)
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_combined_filtering_and_sorting(self):
"""Test that combining filters and sorting works properly"""
user_perm, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
self.client.force_login(self.user)
response = self.app.get(
reverse("get_domain_requests_json"),
{"search_term": "beef", "status": "started", "portfolio": self.portfolio.id},
)
self.assertEqual(response.status_code, 200)
data = response.json
self.assertTrue(all("beef" in req["requested_domain"] for req in data["domain_requests"]))
self.assertTrue(all(req["status"] == "Started" for req in data["domain_requests"]))
created_at_dates = [req["created_at"] for req in data["domain_requests"]]
self.assertEqual(created_at_dates, sorted(created_at_dates, reverse=True))
user_perm.delete()

View file

@ -77,17 +77,15 @@ class FSMErrorCodes(IntEnum):
- 1 APPROVE_DOMAIN_IN_USE The domain is already in use
- 2 NO_INVESTIGATOR No investigator is assigned
- 3 INVESTIGATOR_NOT_STAFF Investigator is a non-staff user
- 4 INVESTIGATOR_NOT_SUBMITTER The form submitter is not the investigator
- 5 NO_REJECTION_REASON No rejection reason is specified
- 6 NO_ACTION_NEEDED_REASON No action needed reason is specified
- 4 NO_REJECTION_REASON No rejection reason is specified
- 5 NO_ACTION_NEEDED_REASON No action needed reason is specified
"""
APPROVE_DOMAIN_IN_USE = 1
NO_INVESTIGATOR = 2
INVESTIGATOR_NOT_STAFF = 3
INVESTIGATOR_NOT_SUBMITTER = 4
NO_REJECTION_REASON = 5
NO_ACTION_NEEDED_REASON = 6
NO_REJECTION_REASON = 4
NO_ACTION_NEEDED_REASON = 5
class FSMDomainRequestError(Exception):
@ -100,7 +98,6 @@ class FSMDomainRequestError(Exception):
FSMErrorCodes.APPROVE_DOMAIN_IN_USE: ("Cannot approve. Requested domain is already in use."),
FSMErrorCodes.NO_INVESTIGATOR: ("Investigator is required for this status."),
FSMErrorCodes.INVESTIGATOR_NOT_STAFF: ("Investigator is not a staff user."),
FSMErrorCodes.INVESTIGATOR_NOT_SUBMITTER: ("Only the assigned investigator can make this change."),
FSMErrorCodes.NO_REJECTION_REASON: ("A reason is required for this status."),
FSMErrorCodes.NO_ACTION_NEEDED_REASON: ("A reason is required for this status."),
}

View file

@ -8,7 +8,6 @@ from .domain import (
DomainNameserversView,
DomainDNSSECView,
DomainDsDataView,
DomainYourContactInformationView,
DomainSecurityEmailView,
DomainUsersView,
DomainAddUserView,

View file

@ -40,7 +40,6 @@ from registrar.models.utility.contact_error import ContactError
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
from ..forms import (
UserForm,
SeniorOfficialContactForm,
DomainOrgNameAddressForm,
DomainAddUserForm,
@ -59,7 +58,6 @@ from epplibwrapper import (
from ..utility.email import send_templated_email, EmailSendingError
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
from waffle.decorators import waffle_flag
logger = logging.getLogger(__name__)
@ -206,7 +204,7 @@ class DomainView(DomainBaseView):
class DomainOrgNameAddressView(DomainFormBaseView):
"""Organization name and mailing address view"""
"""Organization view"""
model = Domain
template_name = "domain_org_name_address.html"
@ -641,38 +639,6 @@ class DomainDsDataView(DomainFormBaseView):
return super().form_valid(formset)
class DomainYourContactInformationView(DomainFormBaseView):
"""Domain your contact information editing view."""
template_name = "domain_your_contact_information.html"
form_class = UserForm
@waffle_flag("!profile_feature") # type: ignore
def dispatch(self, request, *args, **kwargs): # type: ignore
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.submitter instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs)
form_kwargs["instance"] = self.request.user
return form_kwargs
def get_success_url(self):
"""Redirect to the your contact information for the domain."""
return reverse("domain-your-contact-information", kwargs={"pk": self.object.pk})
def form_valid(self, form):
"""The form is valid, call setter in model."""
# Post to DB using values from the form
form.save()
messages.success(self.request, "Your contact information for all your domains has been updated.")
# superclass has the redirect
return super().form_valid(form)
class DomainSecurityEmailView(DomainFormBaseView):
"""Domain security email editing view."""

View file

@ -22,8 +22,6 @@ from .utility import (
DomainRequestWizardPermissionView,
)
from waffle.decorators import flag_is_active, waffle_flag
logger = logging.getLogger(__name__)
@ -45,7 +43,6 @@ class Step(StrEnum):
CURRENT_SITES = "current_sites"
DOTGOV_DOMAIN = "dotgov_domain"
PURPOSE = "purpose"
YOUR_CONTACT = "your_contact"
OTHER_CONTACTS = "other_contacts"
ADDITIONAL_DETAILS = "additional_details"
REQUIREMENTS = "requirements"
@ -85,13 +82,12 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
Step.TRIBAL_GOVERNMENT: _("Tribal government"),
Step.ORGANIZATION_FEDERAL: _("Federal government branch"),
Step.ORGANIZATION_ELECTION: _("Election office"),
Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"),
Step.ORGANIZATION_CONTACT: _("Organization"),
Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
Step.SENIOR_OFFICIAL: _("Senior official"),
Step.CURRENT_SITES: _("Current websites"),
Step.DOTGOV_DOMAIN: _(".gov domain"),
Step.PURPOSE: _("Purpose of your domain"),
Step.YOUR_CONTACT: _("Your contact information"),
Step.OTHER_CONTACTS: _("Other employees from your organization"),
Step.ADDITIONAL_DETAILS: _("Additional details"),
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
@ -382,7 +378,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
),
"dotgov_domain": self.domain_request.requested_domain is not None,
"purpose": self.domain_request.purpose is not None,
"your_contact": self.domain_request.submitter is not None,
"other_contacts": (
self.domain_request.other_contacts.exists()
or self.domain_request.no_other_contacts_rationale is not None
@ -452,9 +447,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
if condition:
step_list.append(step)
if flag_is_active(self.request, "profile_feature"):
step_list.remove(Step.YOUR_CONTACT)
return step_list
def goto(self, step):
@ -599,15 +591,6 @@ class Purpose(DomainRequestWizard):
forms = [forms.PurposeForm]
class YourContact(DomainRequestWizard):
template_name = "domain_request_your_contact.html"
forms = [forms.YourContactForm]
@waffle_flag("!profile_feature") # type: ignore
def dispatch(self, request, *args, **kwargs): # type: ignore
return super().dispatch(request, *args, **kwargs)
class OtherContacts(DomainRequestWizard):
template_name = "domain_request_other_contacts.html"
forms = [forms.OtherContactsYesNoForm, forms.OtherContactsFormSet, forms.NoOtherContactsForm]
@ -813,6 +796,12 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
if status not in valid_statuses:
return False
# Portfolio users cannot delete their requests if they aren't permissioned to do so
if self.request.user.is_org_user(self.request):
portfolio = self.request.session.get("portfolio")
if not self.request.user.has_edit_request_portfolio_permission(portfolio):
return False
return True
def get_success_url(self):
@ -833,7 +822,7 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
# After a delete occurs, do a second sweep on any returned duplicates.
# This determines if any of these three fields share a contact, which is used for
# the edge case where the same user may be an SO, and a submitter, for example.
# the edge case where the same user may be an SO, and a creator, for example.
if len(duplicates) > 0:
duplicates_to_delete, _ = self._get_orphaned_contacts(domain_request, check_db=True)
Contact.objects.filter(id__in=duplicates_to_delete).delete()
@ -846,7 +835,7 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
Collects all orphaned contacts associated with a given DomainRequest object.
An orphaned contact is defined as a contact that is associated with the domain request,
but not with any other domain_request. This includes the senior official, the submitter,
but not with any other domain_request. This includes the senior official, the creator,
and any other contacts linked to the domain_request.
Parameters:
@ -862,18 +851,16 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
# Get each contact object on the DomainRequest object
so = domain_request.senior_official
submitter = domain_request.submitter
other_contacts = list(domain_request.other_contacts.all())
other_contact_ids = domain_request.other_contacts.all().values_list("id", flat=True)
# Check if the desired item still exists in the DB
if check_db:
so = self._get_contacts_by_id([so.id]).first() if so is not None else None
submitter = self._get_contacts_by_id([submitter.id]).first() if submitter is not None else None
other_contacts = self._get_contacts_by_id(other_contact_ids)
# Pair each contact with its db related name for use in checking if it has joins
checked_contacts = [(so, "senior_official"), (submitter, "submitted_domain_requests")]
checked_contacts = [(so, "senior_official")]
checked_contacts.extend((contact, "contact_domain_requests") for contact in other_contacts)
for contact, related_name in checked_contacts:

View file

@ -20,14 +20,14 @@ def get_domain_requests_json(request):
unfiltered_total = objects.count()
objects = apply_search(objects, request)
objects = apply_status_filter(objects, request)
objects = apply_sorting(objects, request)
paginator = Paginator(objects, 10)
page_number = request.GET.get("page", 1)
page_obj = paginator.get_page(page_number)
domain_requests = [
serialize_domain_request(domain_request, request.user) for domain_request in page_obj.object_list
serialize_domain_request(request, domain_request, request.user) for domain_request in page_obj.object_list
]
return JsonResponse(
@ -64,6 +64,7 @@ def get_domain_request_ids_from_request(request):
def apply_search(queryset, request):
search_term = request.GET.get("search_term")
is_portfolio = request.GET.get("portfolio")
if search_term:
search_term_lower = search_term.lower()
@ -76,11 +77,34 @@ def apply_search(queryset, request):
queryset = queryset.filter(
Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True)
)
elif is_portfolio:
queryset = queryset.filter(
Q(requested_domain__name__icontains=search_term)
| Q(creator__first_name__icontains=search_term)
| Q(creator__last_name__icontains=search_term)
| Q(creator__email__icontains=search_term)
)
# For non org users
else:
queryset = queryset.filter(Q(requested_domain__name__icontains=search_term))
return queryset
def apply_status_filter(queryset, request):
status_param = request.GET.get("status")
if status_param:
status_list = status_param.split(",")
statuses = [status for status in status_list if status in DomainRequest.DomainRequestStatus.values]
# Construct Q objects for statuses that can be queried through ORM
status_query = Q()
if statuses:
status_query |= Q(status__in=statuses)
# Apply the combined query
queryset = queryset.filter(status_query)
return queryset
def apply_sorting(queryset, request):
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
order = request.GET.get("order", "asc") # Default to 'asc'
@ -90,13 +114,22 @@ def apply_sorting(queryset, request):
return queryset.order_by(sort_by)
def serialize_domain_request(domain_request, user):
# Determine if the request is deletable
is_deletable = domain_request.status in [
def serialize_domain_request(request, domain_request, user):
deletable_statuses = [
DomainRequest.DomainRequestStatus.STARTED,
DomainRequest.DomainRequestStatus.WITHDRAWN,
]
# Determine if the request is deletable
if not user.is_org_user(request):
is_deletable = domain_request.status in deletable_statuses
else:
portfolio = request.session.get("portfolio")
is_deletable = (
domain_request.status in deletable_statuses and user.has_edit_request_portfolio_permission(portfolio)
) and domain_request.creator == user
# Determine action label based on user permissions and request status
editable_statuses = [
DomainRequest.DomainRequestStatus.STARTED,

View file

@ -11,7 +11,6 @@ from django.urls import NoReverseMatch, reverse
from registrar.models.user import User
from registrar.models.utility.generic_helper import replace_url_queryparams
from registrar.views.utility.permission_views import UserProfilePermissionView
from waffle.decorators import waffle_flag
logger = logging.getLogger(__name__)
@ -46,7 +45,6 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
return self.render_to_response(context)
@waffle_flag("profile_feature") # type: ignore
def dispatch(self, request, *args, **kwargs): # type: ignore
return super().dispatch(request, *args, **kwargs)

View file

@ -81,12 +81,12 @@ class OrderableFieldsMixin:
Or for fields with multiple order_fields:
```
def get_sortable_submitter(self, obj):
return obj.submitter
def get_sortable_creator(self, obj):
return obj.creator
# Allows column order sorting
get_sortable_submitter.admin_order_field = ["submitter__first_name", "submitter__last_name"]
get_sortable_creator.admin_order_field = ["creator__first_name", "creator__last_name"]
# Sets column's header
get_sortable_submitter.short_description = "submitter"
get_sortable_creator.short_description = "creator"
```
Parameters:
@ -114,8 +114,8 @@ class OrderableFieldsMixin:
Returns (example):
```
def get_submitter(self, obj):
return obj.submitter
def get_creator(self, obj):
return obj.creator
```
"""
attr = getattr(obj, field)