diff --git a/docs/developer/README.md b/docs/developer/README.md
index 860140a96..72f6b9f20 100644
--- a/docs/developer/README.md
+++ b/docs/developer/README.md
@@ -320,16 +320,6 @@ it may help to resync your laptop with time.nist.gov:
sudo sntp -sS time.nist.gov
```
-### Settings
-The config for the connection pool exists inside the `settings.py` file.
-| Name | Purpose |
-| ------------------------ | ------------------------------------------------------------------------------------------------- |
-| EPP_CONNECTION_POOL_SIZE | Determines the number of concurrent sockets that should exist in the pool. |
-| POOL_KEEP_ALIVE | Determines the interval in which we ping open connections in seconds. Calculated as POOL_KEEP_ALIVE / EPP_CONNECTION_POOL_SIZE |
-| POOL_TIMEOUT | Determines how long we try to keep a pool alive for, before restarting it. |
-
-Consider updating the `POOL_TIMEOUT` or `POOL_KEEP_ALIVE` periods if the pool often restarts. If the pool only restarts after a period of inactivity, update `POOL_KEEP_ALIVE`. If it restarts during the EPP call itself, then `POOL_TIMEOUT` needs to be updated.
-
## Adding a S3 instance to your sandbox
This can either be done through the CLI, or through the cloud.gov dashboard. Generally, it is better to do it through the dashboard as it handles app binding for you.
@@ -405,3 +395,9 @@ This function is triggered by the post_save event on the User model, designed to
1. For New Users: Upon the creation of a new user, it checks for an existing `Contact` by email. If no matching contact is found, it creates a new Contact using the user's details from Login.gov. If a matching contact is found, it associates this contact with the user. In cases where multiple contacts with the same email exist, it logs a warning and associates the first contact found.
2. For Existing Users: For users logging in subsequent times, the function ensures that any updates from Login.gov are applied to the associated User record. However, it does not alter any existing Contact records.
+
+## Disable email sending (toggling the disable_email_sending flag)
+1. On the app, navigate to `\admin`.
+2. Under models, click `Waffle flags`.
+3. Click the `disable_email_sending` record. This should exist by default, if not - create one with that name.
+4. (Important) Set the field `everyone` to `Yes`. This field overrides all other settings
\ No newline at end of file
diff --git a/docs/operations/data_migration.md b/docs/operations/data_migration.md
index e4543a28c..472362a79 100644
--- a/docs/operations/data_migration.md
+++ b/docs/operations/data_migration.md
@@ -668,3 +668,32 @@ Example: `cf ssh getgov-za`
#### Step 1: Running the script
```docker-compose exec app ./manage.py populate_verification_type```
+
+
+## Copy names from contacts to users
+
+### Running on sandboxes
+
+#### Step 1: Login to CloudFoundry
+```cf login -a api.fr.cloud.gov --sso```
+
+#### Step 2: SSH into your environment
+```cf ssh getgov-{space}```
+
+Example: `cf ssh getgov-za`
+
+#### Step 3: Create a shell instance
+```/tmp/lifecycle/shell```
+
+#### Step 4: Running the script
+```./manage.py copy_names_from_contacts_to_users --debug```
+
+### Running locally
+
+#### Step 1: Running the script
+```docker-compose exec app ./manage.py copy_names_from_contacts_to_users --debug```
+
+##### Optional parameters
+| | Parameter | Description |
+|:-:|:-------------------------- |:----------------------------------------------------------------------------|
+| 1 | **debug** | Increases logging detail. Defaults to False. |
diff --git a/docs/operations/import_export.md b/docs/operations/import_export.md
index 7c3ee1159..7ddfd5d3b 100644
--- a/docs/operations/import_export.md
+++ b/docs/operations/import_export.md
@@ -1,18 +1,29 @@
# Export / Import Tables
-A means is provided to export and import individual tables from
+A means is provided to export and import tables from
one environment to another. This allows for replication of
production data in a development environment. Import and export
-are provided through the django admin interface, through a modified
-library, django-import-export. Each supported model has an Import
-and an Export button on the list view.
+are provided through a modified library, django-import-export.
+Simple scripts are provided as detailed below.
### Export
-When exporting models from the source environment, make sure that
-no filters are selected. This will ensure that all rows of the model
-are exported. Due to database dependencies, the following models
-need to be exported:
+To export from the source environment, run the following command from src directory:
+manage.py export_tables
+
+Connect to the source sandbox and run the command:
+cf ssh {source-app}
+/tmp/lifecycle/shell
+./manage.py export_tables
+
+example exporting from getgov-stable:
+cf ssh getgov-stable
+/tmp/lifecycle/shell
+./manage.py export_tables
+
+This exports a file, exported_tables.zip, to the tmp directory
+
+For reference, the zip file will contain the following tables in csv form:
* User
* Contact
@@ -25,6 +36,20 @@ need to be exported:
* Host
* HostIP
+After exporting the file from the target environment, scp the exported_tables.zip
+file from the target environment to local. Run the below commands from local.
+
+Get passcode by running:
+cf ssh-code
+
+scp file from source app to local file:
+scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app {source-app} --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 ssh.fr.cloud.gov:app/tmp/exported_tables.zip {local_file_path}
+when prompted, supply the passcode retrieved in the 'cf ssh-code' command
+
+example copying from stable to local cwd:
+scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app getgov-stable --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 ssh.fr.cloud.gov:app/tmp/exported_tables.zip .
+
+
### Import
When importing into the target environment, if the target environment
@@ -34,7 +59,18 @@ that there are no database conflicts on import.
#### Preparing Target Environment
-Delete all rows from tables in the following order through django admin:
+In order to delete all rows from the appropriate tables, run the following
+command:
+cf ssh {target-app}
+/tmp/lifecycle/shell
+./manage.py clean_tables
+
+example cleaning getgov-backup:
+cf ssh getgov-backup
+/tmp/lifecycle/backup
+./manage.py clean_tables
+
+For reference, this deletes all rows from the following tables:
* DomainInformation
* DomainRequest
@@ -48,10 +84,34 @@ Delete all rows from tables in the following order through django admin:
#### Importing into Target Environment
-Once target environment is prepared, files can be imported in the following
-order:
+Once target environment is prepared, files can be imported.
-* User (After importing User table, you need to delete all rows from Contact table before importing Contacts)
+To scp the exported_tables.zip file from local to the sandbox, run the following:
+
+Get passcode by running:
+cf ssh-code
+
+scp file from local to target app:
+scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app {target-app} --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 {local_file_path} ssh.fr.cloud.gov:app/tmp/exported_tables.zip
+when prompted, supply the passcode retrieved in the 'cf ssh-code' command
+
+example copy of local file in tmp to getgov-backup:
+scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app getgov-backup --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 tmp/exported_tables.zip ssh.fr.cloud.gov:app/tmp/exported_tables.zip
+
+
+Then connect to a shell in the target environment, and run the following import command:
+cf ssh {target-app}
+/tmp/lifecycle/shell
+./manage.py import_tables
+
+example cleaning getgov-backup:
+cf ssh getgov-backup
+/tmp/lifecycle/backup
+./manage.py import_tables
+
+For reference, this imports tables in the following order:
+
+* User
* Contact
* Domain
* Host
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index e693c41cc..a2f1a1dfc 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -596,7 +596,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
None,
{"fields": ("username", "password", "status", "verification_type")},
),
- ("Personal info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}),
+ ("Personal info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
(
"Permissions",
{
@@ -627,7 +627,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
)
},
),
- ("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}),
+ ("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
(
"Permissions",
{
@@ -2262,9 +2262,46 @@ class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
return response
-class PublicContactAdmin(ListHeaderAdmin):
+class PublicContactResource(resources.ModelResource):
+ """defines how each field in the referenced model should be mapped to the corresponding fields in the
+ import/export file"""
+
+ class Meta:
+ model = models.PublicContact
+
+ def import_row(self, row, instance_loader, using_transactions=True, dry_run=False, raise_errors=None, **kwargs):
+ """Override kwargs skip_epp_save and set to True"""
+ kwargs["skip_epp_save"] = True
+ return super().import_row(
+ row,
+ instance_loader,
+ using_transactions=using_transactions,
+ dry_run=dry_run,
+ raise_errors=raise_errors,
+ **kwargs,
+ )
+
+ def save_instance(self, instance, is_create, using_transactions=True, dry_run=False):
+ """Override save_instance setting skip_epp_save to True"""
+ self.before_save_instance(instance, using_transactions, dry_run)
+ if self._meta.use_bulk:
+ if is_create:
+ self.create_instances.append(instance)
+ else:
+ self.update_instances.append(instance)
+ elif not using_transactions and dry_run:
+ # we don't have transactions and we want to do a dry_run
+ pass
+ else:
+ instance.save(skip_epp_save=True)
+ self.after_save_instance(instance, using_transactions, dry_run)
+
+
+class PublicContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"""Custom PublicContact admin class."""
+ resource_classes = [PublicContactResource]
+
change_form_template = "django/admin/email_clipboard_change_form.html"
autocomplete_fields = ["domain"]
@@ -2322,6 +2359,8 @@ class UserGroupAdmin(AuditedAdmin):
class WaffleFlagAdmin(FlagAdmin):
+ """Custom admin implementation of django-waffle's Flag class"""
+
class Meta:
"""Contains meta information about this class"""
@@ -2355,6 +2394,6 @@ admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
# Register our custom waffle implementations
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)
-# Unregister Sample and Switch from the waffle library
-admin.site.unregister(Sample)
+# Unregister Switch and Sample from the waffle library
admin.site.unregister(Switch)
+admin.site.unregister(Sample)
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index c33a31aa2..0d594b315 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -879,6 +879,146 @@ function unloadModals() {
window.modal.off();
}
+/**
+ * Helper function that scrolls to an element
+ * @param {string} attributeName - The string "class" or "id"
+ * @param {string} attributeValue - The class or id name
+ */
+function ScrollToElement(attributeName, attributeValue) {
+ let targetEl = null;
+
+ if (attributeName === 'class') {
+ targetEl = document.getElementsByClassName(attributeValue)[0];
+ } else if (attributeName === 'id') {
+ targetEl = document.getElementById(attributeValue);
+ } else {
+ console.log('Error: unknown attribute name provided.');
+ return; // Exit the function if an invalid attributeName is provided
+ }
+
+ if (targetEl) {
+ const rect = targetEl.getBoundingClientRect();
+ const scrollTop = window.scrollY || document.documentElement.scrollTop;
+ window.scrollTo({
+ top: rect.top + scrollTop,
+ behavior: 'smooth' // Optional: for smooth scrolling
+ });
+ }
+}
+
+/**
+ * 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} headerAnchor - CSS selector for the header element to anchor the links to.
+ * @param {Function} loadPageFunction - Function to call when a page link is clicked.
+ * @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.
+ */
+function updatePagination(itemName, paginationSelector, counterSelector, headerAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems) {
+ const paginationContainer = document.querySelector(paginationSelector);
+ const paginationCounter = document.querySelector(counterSelector);
+ const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
+ paginationCounter.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);
+
+ paginationCounter.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}`;
+
+ if (hasPrevious) {
+ const prevPageItem = document.createElement('li');
+ prevPageItem.className = 'usa-pagination__item usa-pagination__arrow';
+ prevPageItem.innerHTML = `
+
+
+ Previous
+
+ `;
+ prevPageItem.querySelector('a').addEventListener('click', (event) => {
+ event.preventDefault();
+ loadPageFunction(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 = `
+ ${page}
+ `;
+ 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));
+ if (currentPage > 3) {
+ const ellipsis = document.createElement('li');
+ ellipsis.className = 'usa-pagination__item usa-pagination__overflow';
+ ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages');
+ ellipsis.innerHTML = '…';
+ paginationButtons.appendChild(ellipsis);
+ }
+ }
+
+ // 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));
+ }
+
+ // Add last page and ellipsis if necessary
+ if (currentPage < numPages - 1) {
+ if (currentPage < numPages - 2) {
+ const ellipsis = document.createElement('li');
+ ellipsis.className = 'usa-pagination__item usa-pagination__overflow';
+ ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages');
+ ellipsis.innerHTML = '…';
+ paginationButtons.appendChild(ellipsis);
+ }
+ paginationButtons.appendChild(createPageItem(numPages));
+ }
+
+ if (hasNext) {
+ const nextPageItem = document.createElement('li');
+ nextPageItem.className = 'usa-pagination__item usa-pagination__arrow';
+ nextPageItem.innerHTML = `
+
+ Next
+
+
+ `;
+ nextPageItem.querySelector('a').addEventListener('click', (event) => {
+ event.preventDefault();
+ loadPageFunction(currentPage + 1);
+ });
+ paginationButtons.appendChild(nextPageItem);
+ }
+}
+
+
/**
* 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.
@@ -891,6 +1031,7 @@ document.addEventListener('DOMContentLoaded', function() {
let currentSortBy = 'id';
let currentOrder = 'asc';
let noDomainsWrapper = document.querySelector('.no-domains-wrapper');
+ let hasLoaded = false;
/**
* Loads rows in the domains list, as well as updates pagination around the domains list
@@ -898,8 +1039,9 @@ document.addEventListener('DOMContentLoaded', function() {
* @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 {*} loaded - control for the scrollToElement functionality
*/
- function loadDomains(page, sortBy = currentSortBy, order = currentOrder) {
+ function loadDomains(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded) {
//fetch json of page of domains, given page # and sort
fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}`)
.then(response => response.json())
@@ -923,9 +1065,12 @@ document.addEventListener('DOMContentLoaded', function() {
domainList.innerHTML = '';
data.domains.forEach(domain => {
+ const options = { year: 'numeric', month: 'short', day: 'numeric' };
const expirationDate = domain.expiration_date ? new Date(domain.expiration_date) : null;
+ const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : null;
const expirationDateSortValue = expirationDate ? expirationDate.getTime() : '';
const actionUrl = domain.action_url;
+
const row = document.createElement('tr');
row.innerHTML = `
@@ -933,7 +1078,7 @@ document.addEventListener('DOMContentLoaded', function() {
${domain.name}
{% endblock wrapper%}
- {% include "includes/footer.html" %}
+ {% block footer %}
+ {% include "includes/footer.html" with show_manage_your_domains=True %}
+ {% endblock footer %}
{% block init_js %}{% endblock %}{# useful for vars and other initializations #}
diff --git a/src/registrar/templates/domain_request_form.html b/src/registrar/templates/domain_request_form.html
index cde12ad80..17948a110 100644
--- a/src/registrar/templates/domain_request_form.html
+++ b/src/registrar/templates/domain_request_form.html
@@ -105,7 +105,7 @@
aria-describedby="Are you sure you want to submit a domain request?"
data-force-action
>
- {% include 'includes/modal.html' with modal_heading=modal_heading|safe modal_description="Once you submit this request, you won’t be able to edit it until we review it. You’ll only be able to withdraw your request." modal_button=modal_button|safe %}
+ {% include 'includes/modal.html' with is_domain_request_form=True review_form_is_complete=review_form_is_complete modal_heading=modal_heading|safe modal_description=modal_description|safe modal_button=modal_button|safe %}
{% block after_form_content %}{% endblock %}
diff --git a/src/registrar/templates/domain_request_intro.html b/src/registrar/templates/domain_request_intro.html
index d6d3b3b7f..285777a80 100644
--- a/src/registrar/templates/domain_request_intro.html
+++ b/src/registrar/templates/domain_request_intro.html
@@ -13,12 +13,12 @@
We’ll use the information you provide to verify your organization’s eligibility for a .gov domain. We’ll also verify that the domain you request meets our guidelines.
While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review If the contact information below is not correct, visit your profile to make updates.
- {% include "includes/profile_information.html" with user=user%}
- {% endif %}
+ completing your domain request might take around 15 minutes.
+ {% if has_profile_feature_flag %}
+
How we’ll reach you
+
While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review If the contact information below is not correct, visit your profile to make updates.
+ {% include "includes/profile_information.html" with user=user%}
+ {% endif %}
{% block form_buttons %}
diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html
index 5f359e95f..1f21683a5 100644
--- a/src/registrar/templates/domain_request_review.html
+++ b/src/registrar/templates/domain_request_review.html
@@ -25,11 +25,11 @@
{% if step == Step.ORGANIZATION_TYPE %}
{% namespaced_url 'domain-request' step as domain_request_url %}
{% if domain_request.generic_org_type is not None %}
- {% with title=form_titles|get_item:step value=domain_request.get_generic_org_type_display|default:"Incomplete" %}
+ {% with title=form_titles|get_item:step value=domain_request.get_generic_org_type_display|default:"Incomplete"|safe %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
{% endwith %}
{% else %}
- {% with title=form_titles|get_item:step value="Incomplete" %}
+ {% with title=form_titles|get_item:step value="Incomplete"|safe %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
{% endwith %}
{% endif %}
@@ -37,7 +37,7 @@
{% if step == Step.TRIBAL_GOVERNMENT %}
{% namespaced_url 'domain-request' step as domain_request_url %}
- {% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"Incomplete" %}
+ {% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"Incomplete"|safe %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
{% endwith %}
{% if domain_request.federally_recognized_tribe %}
Federally-recognized tribe
{% endif %}
@@ -47,7 +47,7 @@
{% if step == Step.ORGANIZATION_FEDERAL %}
{% namespaced_url 'domain-request' step as domain_request_url %}
- {% with title=form_titles|get_item:step value=domain_request.get_federal_type_display|default:"Incomplete" %}
+ {% with title=form_titles|get_item:step value=domain_request.get_federal_type_display|default:"Incomplete"|safe %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
{% endwith %}
{% endif %}
@@ -66,7 +66,7 @@
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url address='true' %}
{% endwith %}
{% else %}
- {% with title=form_titles|get_item:step value='Incomplete' %}
+ {% with title=form_titles|get_item:step value="Incomplete"|safe %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
{% endwith %}
{% endif %}
@@ -74,7 +74,7 @@
{% if step == Step.ABOUT_YOUR_ORGANIZATION %}
{% namespaced_url 'domain-request' step as domain_request_url %}
- {% with title=form_titles|get_item:step value=domain_request.about_your_organization|default:"Incomplete" %}
+ {% with title=form_titles|get_item:step value=domain_request.about_your_organization|default:"Incomplete"|safe %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
{% endwith %}
{% endif %}
@@ -86,7 +86,7 @@
{% 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 %}
- {% with title=form_titles|get_item:step value="Incomplete" %}
+ {% with title=form_titles|get_item:step value="Incomplete"|safe %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
{% endwith %}
{% endif %}
@@ -107,7 +107,7 @@
{% if step == Step.DOTGOV_DOMAIN %}
{% namespaced_url 'domain-request' step as domain_request_url %}
- {% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %}
+ {% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete"|safe%}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
{% endwith %}
@@ -123,7 +123,7 @@
{% if step == Step.PURPOSE %}
{% namespaced_url 'domain-request' step as domain_request_url %}
- {% with title=form_titles|get_item:step value=domain_request.purpose|default:"Incomplete" %}
+ {% with title=form_titles|get_item:step value=domain_request.purpose|default:"Incomplete"|safe %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
{% endwith %}
{% endif %}
@@ -135,7 +135,7 @@
{% 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 %}
- {% with title=form_titles|get_item:step value="Incomplete" %}
+ {% with title=form_titles|get_item:step value="Incomplete"|safe %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
{% endwith %}
{% endif %}
@@ -148,7 +148,7 @@
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' list='true' %}
{% endwith %}
{% else %}
- {% with title=form_titles|get_item:step value=domain_request.no_other_contacts_rationale|default:"Incomplete" %}
+ {% with title=form_titles|get_item:step value=domain_request.no_other_contacts_rationale|default:"Incomplete"|safe %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
{% endwith %}
{% endif %}
@@ -157,7 +157,7 @@
{% if step == Step.ADDITIONAL_DETAILS %}
{% namespaced_url 'domain-request' step as domain_request_url %}
- {% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %}
+ {% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete"|safe %}
{% include "includes/summary_item.html" with title=title sub_header_text='CISA regional representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %}
{% endwith %}
diff --git a/src/registrar/templates/finish_profile_setup.html b/src/registrar/templates/finish_profile_setup.html
new file mode 100644
index 000000000..f8070551b
--- /dev/null
+++ b/src/registrar/templates/finish_profile_setup.html
@@ -0,0 +1,20 @@
+{% extends "profile.html" %}
+
+{% load static form_helpers url_helpers field_helpers %}
+{% block title %} Finish setting up your profile | {% endblock %}
+
+{# Disable the redirect #}
+{% block logo %}
+ {% include "includes/gov_extended_logo.html" with logo_clickable=confirm_changes %}
+{% endblock %}
+
+{# Add the new form #}
+{% block content_bottom %}
+ {% include "includes/finish_profile_form.html" with form=form %}
+
+
+{% endblock content_bottom %}
+
+{% block footer %}
+ {% include "includes/footer.html" with show_manage_your_domains=confirm_changes %}
+{% endblock footer %}
diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html
index 9a5082104..fd54769a8 100644
--- a/src/registrar/templates/home.html
+++ b/src/registrar/templates/home.html
@@ -63,7 +63,7 @@