@@ -176,4 +190,3 @@
Member Access
{% endblock portfolio_content%}
-
diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py
index 53c500f51..a238de3fc 100644
--- a/src/registrar/views/portfolios.py
+++ b/src/registrar/views/portfolios.py
@@ -4,7 +4,7 @@
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.contrib import messages
-
+from django.conf import settings
from registrar.forms import portfolio as portfolioForms
from registrar.models import Portfolio, User
from registrar.models.portfolio_invitation import PortfolioInvitation
@@ -16,7 +16,6 @@
PortfolioDomainsPermissionView,
PortfolioBasePermissionView,
NoPortfolioDomainsPermissionView,
- PortfolioInvitationCreatePermissionView,
PortfolioMemberDomainsPermissionView,
PortfolioMemberDomainsEditPermissionView,
PortfolioMemberEditPermissionView,
@@ -506,45 +505,163 @@ def get(self, request):
return render(request, "portfolio_members.html")
-class NewMemberView(PortfolioInvitationCreatePermissionView):
+
+class NewMemberView(PortfolioMembersPermissionView, FormMixin):
+
template_name = "portfolio_members_add_new.html"
form_class = portfolioForms.NewMemberForm
+ def get_object(self, queryset=None):
+ """Get the portfolio object based on the session."""
+ portfolio = self.request.session.get("portfolio")
+ if portfolio is None:
+ raise Http404("No organization found for this user")
+ return portfolio
+
def get_form_kwargs(self):
- """Pass request and portfolio to form."""
+ """Include the instance in the form kwargs."""
kwargs = super().get_form_kwargs()
- kwargs["portfolio"] = self.request.session.get("portfolio")
+ kwargs["instance"] = self.get_object()
return kwargs
- def get_success_url(self):
- return reverse("members")
+ def get(self, request, *args, **kwargs):
+ """Handle GET requests to display the form."""
+ self.object = self.get_object()
+ form = self.get_form()
+ return self.render_to_response(self.get_context_data(form=form))
+
+ def post(self, request, *args, **kwargs):
+ """Handle POST requests to process form submission."""
+ self.object = self.get_object()
+ form = self.get_form()
+
+ if form.is_valid():
+ return self.form_valid(form)
+ else:
+ return self.form_invalid(form)
+
+ def is_ajax(self):
+ return self.request.headers.get("X-Requested-With") == "XMLHttpRequest"
+
+ def form_invalid(self, form):
+ if self.is_ajax():
+ return JsonResponse({"is_valid": False}) # Return a JSON response
+ else:
+ return super().form_invalid(form) # Handle non-AJAX requests normally
def form_valid(self, form):
- """Create portfolio invitation from form data."""
+
if self.is_ajax():
- return JsonResponse({"is_valid": True})
+ return JsonResponse({"is_valid": True}) # Return a JSON response
+ else:
+ return self.submit_new_member(form)
- # TODO: #3019 - this will probably have to be a small try/catch. Stub for posterity.
- # requested_email = form.cleaned_data.get("email")
- # send_success = self.send_portfolio_invitation_email(requested_email)
- # if not send_success:
- # return
+ def get_success_url(self):
+ """Redirect to members table."""
+ return reverse("members")
- # Create instance using form's mapping method.
- # Pass in a new object since we are adding a new record.
- self.object = form.map_cleaned_data_to_instance(form.cleaned_data, PortfolioInvitation())
- self.object.save()
- messages.success(self.request, f"{self.object.email} has been invited.")
- return redirect(self.get_success_url())
+ def _send_portfolio_invitation_email(self, email: str, requestor: User, add_success=True):
+ """Performs the sending of the member invitation email
+ email: string- email to send to
+ add_success: bool- default True indicates:
+ adding a success message to the view if the email sending succeeds
- # TODO: #3019
- # def send_portfolio_invitation_email(self, email):
- # pass
+ raises EmailSendingError
+ """
- def form_invalid(self, form):
- if self.is_ajax():
- return JsonResponse({"is_valid": False})
- return super().form_invalid(form)
+ # Set a default email address to send to for staff
+ requestor_email = settings.DEFAULT_FROM_EMAIL
+
+ # Check if the email requestor has a valid email address
+ if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
+ requestor_email = requestor.email
+ elif not requestor.is_staff:
+ messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
+ logger.error(
+ f"Can't send email to '{email}' on domain '{self.object}'."
+ f"No email exists for the requestor '{requestor.username}'.",
+ exc_info=True,
+ )
+ return None
+
+ # Check to see if an invite has already been sent
+ try:
+ invite = PortfolioInvitation.objects.get(email=email, portfolio=self.object)
+ if invite: # We have an existin invite
+ # check if the invite has already been accepted
+ if invite.status == PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED:
+ add_success = False
+ messages.warning(
+ self.request,
+ f"{email} is already a manager for this portfolio.",
+ )
+ else:
+ add_success = False
+ # it has been sent but not accepted
+ messages.warning(self.request, f"{email} has already been invited to this portfolio")
+ return
+ except Exception as err:
+ logger.error(f"_send_portfolio_invitation_email() => An error occured: {err}")
+
+ try:
+ logger.debug("requestor email: " + requestor_email)
+
+ # send_templated_email(
+ # "emails/portfolio_invitation.txt",
+ # "emails/portfolio_invitation_subject.txt",
+ # to_address=email,
+ # context={
+ # "portfolio": self.object,
+ # "requestor_email": requestor_email,
+ # },
+ # )
+ except EmailSendingError as exc:
+ logger.warn(
+ "Could not sent email invitation to %s for domain %s",
+ email,
+ self.object,
+ exc_info=True,
+ )
+ raise EmailSendingError("Could not send email invitation.") from exc
+ else:
+ if add_success:
+ messages.success(self.request, f"{email} has been invited.")
+
+ def _make_invitation(self, email_address: str, requestor: User, add_success=True):
+ """Make a Member invitation for this email and redirect with a message."""
+ try:
+ self._send_portfolio_invitation_email(email=email_address, requestor=requestor, add_success=add_success)
+ except EmailSendingError:
+ logger.warn(
+ "Could not send email invitation (EmailSendingError)",
+ self.object,
+ exc_info=True,
+ )
+ messages.warning(self.request, "Could not send email invitation.")
+ except Exception:
+ logger.warn(
+ "Could not send email invitation (Other Exception)",
+ self.object,
+ exc_info=True,
+ )
+ messages.warning(self.request, "Could not send email invitation.")
+ else:
+ # (NOTE: only create a MemberInvitation if the e-mail sends correctly)
+ PortfolioInvitation.objects.get_or_create(email=email_address, portfolio=self.object)
+ return redirect(self.get_success_url())
- def is_ajax(self):
- return self.request.headers.get("X-Requested-With") == "XMLHttpRequest"
+ def submit_new_member(self, form):
+ """Add the specified user as a member
+ for this portfolio.
+ Throws EmailSendingError."""
+ requested_email = form.cleaned_data["email"]
+ requestor = self.request.user
+
+ requested_user = User.objects.filter(email=requested_email).first()
+ permission_exists = UserPortfolioPermission.objects.filter(user=requested_user, portfolio=self.object).exists()
+ if not requested_user or not permission_exists:
+ return self._make_invitation(requested_email, requestor)
+ else:
+ if permission_exists:
+ messages.warning(self.request, "User is already a member of this portfolio.")
+ return redirect(self.get_success_url())
diff --git a/src/registrar/views/utility/__init__.py b/src/registrar/views/utility/__init__.py
index fbf44fda1..6798eb4ee 100644
--- a/src/registrar/views/utility/__init__.py
+++ b/src/registrar/views/utility/__init__.py
@@ -9,6 +9,5 @@
PortfolioMembersPermission,
DomainRequestPortfolioViewonlyView,
DomainInvitationPermissionCancelView,
- PortfolioInvitationCreatePermissionView,
)
from .api_views import get_senior_official_from_federal_agency_json
diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py
index 45474bddc..54ce5a921 100644
--- a/src/registrar/views/utility/permission_views.py
+++ b/src/registrar/views/utility/permission_views.py
@@ -1,7 +1,6 @@
"""View classes that enforce authorization."""
import abc # abstract base class
-from django.views.generic.edit import CreateView
from django.views.generic import DetailView, DeleteView, TemplateView, UpdateView
from registrar.models import Domain, DomainRequest, DomainInvitation, Portfolio
from registrar.models.portfolio_invitation import PortfolioInvitation
@@ -227,25 +226,6 @@ def template_name(self):
raise NotImplementedError
-class PortfolioInvitationCreatePermissionView(PortfolioInvitationCreatePermission, CreateView, abc.ABC):
- """Abstract base view for portfolio views that enforces permissions.
-
- This abstract view cannot be instantiated. Actual views must specify
- `template_name`.
- """
-
- # DetailView property for what model this is viewing
- model = PortfolioInvitation
- # variable name in template context for the model object
- context_object_name = "portfolio_invitation"
-
- # Abstract property enforces NotImplementedError on an attribute.
- @property
- @abc.abstractmethod
- def template_name(self):
- raise NotImplementedError
-
-
class PortfolioDomainsPermissionView(PortfolioDomainsPermission, PortfolioBasePermissionView, abc.ABC):
"""Abstract base view for portfolio domains views that enforces permissions.
From 08082fb0aac6628e3994f63718fd3b9e77e91ea5 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 11:51:03 -0700
Subject: [PATCH 24/46] cleanup pt 2
---
.../src/js/getgov/portfolio-member-page.js | 258 +++++++++---------
.../templates/portfolio_members_add_new.html | 1 +
src/registrar/tests/test_views_portfolio.py | 31 ++-
src/registrar/views/utility/mixins.py | 18 --
.../views/utility/permission_views.py | 2 -
5 files changed, 146 insertions(+), 164 deletions(-)
diff --git a/src/registrar/assets/src/js/getgov/portfolio-member-page.js b/src/registrar/assets/src/js/getgov/portfolio-member-page.js
index af25f0f1d..280c087f0 100644
--- a/src/registrar/assets/src/js/getgov/portfolio-member-page.js
+++ b/src/registrar/assets/src/js/getgov/portfolio-member-page.js
@@ -7,41 +7,41 @@ import { hookupRadioTogglerListener } from './radios.js';
// This is specifically for the Member Profile (Manage Member) Page member/invitation removal
export function initPortfolioNewMemberPageToggle() {
document.addEventListener("DOMContentLoaded", () => {
- const wrapperDeleteAction = document.getElementById("wrapper-delete-action")
- if (wrapperDeleteAction) {
- const member_type = wrapperDeleteAction.getAttribute("data-member-type");
- const member_id = wrapperDeleteAction.getAttribute("data-member-id");
- const num_domains = wrapperDeleteAction.getAttribute("data-num-domains");
- const member_name = wrapperDeleteAction.getAttribute("data-member-name");
- const member_email = wrapperDeleteAction.getAttribute("data-member-email");
- const member_delete_url = `${member_type}-${member_id}/delete`;
- const unique_id = `${member_type}-${member_id}`;
-
- let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
- wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `for ${member_name}`);
-
- // This easter egg is only for fixtures that dont have names as we are displaying their emails
- // All prod users will have emails linked to their account
- MembersTable.addMemberModal(num_domains, member_email || "Samwise Gamgee", member_delete_url, unique_id, wrapperDeleteAction);
-
- uswdsInitializeModals();
-
- // Now the DOM and modals are ready, add listeners to the submit buttons
- const modals = document.querySelectorAll('.usa-modal__content');
-
- modals.forEach(modal => {
- const submitButton = modal.querySelector('.usa-modal__submit');
- const closeButton = modal.querySelector('.usa-modal__close');
- submitButton.addEventListener('click', () => {
- closeButton.click();
- let delete_member_form = document.getElementById("member-delete-form");
- if (delete_member_form) {
- delete_member_form.submit();
- }
- });
+ const wrapperDeleteAction = document.getElementById("wrapper-delete-action")
+ if (wrapperDeleteAction) {
+ const member_type = wrapperDeleteAction.getAttribute("data-member-type");
+ const member_id = wrapperDeleteAction.getAttribute("data-member-id");
+ const num_domains = wrapperDeleteAction.getAttribute("data-num-domains");
+ const member_name = wrapperDeleteAction.getAttribute("data-member-name");
+ const member_email = wrapperDeleteAction.getAttribute("data-member-email");
+ const member_delete_url = `${member_type}-${member_id}/delete`;
+ const unique_id = `${member_type}-${member_id}`;
+
+ let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
+ wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `for ${member_name}`);
+
+ // This easter egg is only for fixtures that dont have names as we are displaying their emails
+ // All prod users will have emails linked to their account
+ MembersTable.addMemberModal(num_domains, member_email || "Samwise Gamgee", member_delete_url, unique_id, wrapperDeleteAction);
+
+ uswdsInitializeModals();
+
+ // Now the DOM and modals are ready, add listeners to the submit buttons
+ const modals = document.querySelectorAll('.usa-modal__content');
+
+ modals.forEach(modal => {
+ const submitButton = modal.querySelector('.usa-modal__submit');
+ const closeButton = modal.querySelector('.usa-modal__close');
+ submitButton.addEventListener('click', () => {
+ closeButton.click();
+ let delete_member_form = document.getElementById("member-delete-form");
+ if (delete_member_form) {
+ delete_member_form.submit();
+ }
});
- }
- });
+ });
+ }
+});
}
@@ -52,122 +52,122 @@ export function initPortfolioNewMemberPageToggle() {
export function initAddNewMemberPageListeners() {
let add_member_form = document.getElementById("add_member_form");
if (!add_member_form){
- return;
+ return;
}
document.getElementById("confirm_new_member_submit").addEventListener("click", function() {
- // Upon confirmation, submit the form
- document.getElementById("add_member_form").submit();
+// Upon confirmation, submit the form
+document.getElementById("add_member_form").submit();
});
document.getElementById("add_member_form").addEventListener("submit", function(event) {
- event.preventDefault(); // Prevents the form from submitting
- const form = document.getElementById("add_member_form")
- const formData = new FormData(form);
-
- // Check if the form is valid
- // If the form is valid, open the confirmation modal
- // If the form is invalid, submit it to trigger error
- fetch(form.action, {
- method: "POST",
- body: formData,
- headers: {
- "X-Requested-With": "XMLHttpRequest",
- "X-CSRFToken": getCsrfToken()
- }
- })
- .then(response => response.json())
- .then(data => {
- if (data.is_valid) {
- // If the form is valid, show the confirmation modal before submitting
- openAddMemberConfirmationModal();
- } else {
- // If the form is not valid, trigger error messages by firing a submit event
- form.submit();
- }
- });
+event.preventDefault(); // Prevents the form from submitting
+const form = document.getElementById("add_member_form")
+const formData = new FormData(form);
+
+// Check if the form is valid
+// If the form is valid, open the confirmation modal
+// If the form is invalid, submit it to trigger error
+fetch(form.action, {
+ method: "POST",
+ body: formData,
+ headers: {
+ "X-Requested-With": "XMLHttpRequest",
+ "X-CSRFToken": getCsrfToken()
+ }
+})
+.then(response => response.json())
+.then(data => {
+ if (data.is_valid) {
+ // If the form is valid, show the confirmation modal before submitting
+ openAddMemberConfirmationModal();
+ } else {
+ // If the form is not valid, trigger error messages by firing a submit event
+ form.submit();
+ }
+});
});
/*
- Helper function to capitalize the first letter in a string (for display purposes)
+Helper function to capitalize the first letter in a string (for display purposes)
*/
function capitalizeFirstLetter(text) {
- if (!text) return ''; // Return empty string if input is falsy
- return text.charAt(0).toUpperCase() + text.slice(1);
+if (!text) return ''; // Return empty string if input is falsy
+return text.charAt(0).toUpperCase() + text.slice(1);
}
/*
- Populates contents of the "Add Member" confirmation modal
+Populates contents of the "Add Member" confirmation modal
*/
function populatePermissionDetails(permission_details_div_id) {
- const permissionDetailsContainer = document.getElementById("permission_details");
- permissionDetailsContainer.innerHTML = ""; // Clear previous content
-
- // Get all permission sections (divs with h3 and radio inputs)
- const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
-
- permissionSections.forEach(section => {
- // Find the
element text
- const sectionTitle = section.textContent;
-
- // Find the associated radio buttons container (next fieldset)
- const fieldset = section.nextElementSibling;
-
- if (fieldset && fieldset.tagName.toLowerCase() === 'fieldset') {
- // Get the selected radio button within this fieldset
- const selectedRadio = fieldset.querySelector('input[type="radio"]:checked');
-
- // If a radio button is selected, get its label text
- let selectedPermission = "No permission selected";
- if (selectedRadio) {
- const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`);
- selectedPermission = label ? label.textContent : "No permission selected";
- }
-
- // Create new elements for the modal content
- const titleElement = document.createElement("h4");
- titleElement.textContent = sectionTitle;
- titleElement.classList.add("text-primary");
- titleElement.classList.add("margin-bottom-0");
-
- const permissionElement = document.createElement("p");
- permissionElement.textContent = selectedPermission;
- permissionElement.classList.add("margin-top-0");
-
- // Append to the modal content container
- permissionDetailsContainer.appendChild(titleElement);
- permissionDetailsContainer.appendChild(permissionElement);
- }
- });
+const permissionDetailsContainer = document.getElementById("permission_details");
+permissionDetailsContainer.innerHTML = ""; // Clear previous content
+
+// Get all permission sections (divs with h3 and radio inputs)
+const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
+
+permissionSections.forEach(section => {
+ // Find the element text
+ const sectionTitle = section.textContent;
+
+ // Find the associated radio buttons container (next fieldset)
+ const fieldset = section.nextElementSibling;
+
+ if (fieldset && fieldset.tagName.toLowerCase() === 'fieldset') {
+ // Get the selected radio button within this fieldset
+ const selectedRadio = fieldset.querySelector('input[type="radio"]:checked');
+
+ // If a radio button is selected, get its label text
+ let selectedPermission = "No permission selected";
+ if (selectedRadio) {
+ const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`);
+ selectedPermission = label ? label.textContent : "No permission selected";
+ }
+
+ // Create new elements for the modal content
+ const titleElement = document.createElement("h4");
+ titleElement.textContent = sectionTitle;
+ titleElement.classList.add("text-primary");
+ titleElement.classList.add("margin-bottom-0");
+
+ const permissionElement = document.createElement("p");
+ permissionElement.textContent = selectedPermission;
+ permissionElement.classList.add("margin-top-0");
+
+ // Append to the modal content container
+ permissionDetailsContainer.appendChild(titleElement);
+ permissionDetailsContainer.appendChild(permissionElement);
+ }
+});
}
/*
- Updates and opens the "Add Member" confirmation modal.
+Updates and opens the "Add Member" confirmation modal.
*/
function openAddMemberConfirmationModal() {
- //------- Populate modal details
- // Get email value
- let emailValue = document.getElementById('id_email').value;
- document.getElementById('modalEmail').textContent = emailValue;
-
- // Get selected radio button for access level
- let selectedAccess = document.querySelector('input[name="member_access_level"]:checked');
- // Set the selected permission text to 'Basic' or 'Admin' (the value of the selected radio button)
- // This value does not have the first letter capitalized so let's capitalize it
- let accessText = selectedAccess ? capitalizeFirstLetter(selectedAccess.value) : "No access level selected";
- document.getElementById('modalAccessLevel').textContent = accessText;
-
- // Populate permission details based on access level
- if (selectedAccess && selectedAccess.value === 'admin') {
- populatePermissionDetails('new-member-admin-permissions');
- } else {
- populatePermissionDetails('new-member-basic-permissions');
+ //------- Populate modal details
+ // Get email value
+ let emailValue = document.getElementById('id_email').value;
+ document.getElementById('modalEmail').textContent = emailValue;
+
+ // Get selected radio button for access level
+ let selectedAccess = document.querySelector('input[name="member_access_level"]:checked');
+ // Set the selected permission text to 'Basic' or 'Admin' (the value of the selected radio button)
+ // This value does not have the first letter capitalized so let's capitalize it
+ let accessText = selectedAccess ? capitalizeFirstLetter(selectedAccess.value) : "No access level selected";
+ document.getElementById('modalAccessLevel').textContent = accessText;
+
+ // Populate permission details based on access level
+ if (selectedAccess && selectedAccess.value === 'admin') {
+ populatePermissionDetails('new-member-admin-permissions');
+ } else {
+ populatePermissionDetails('new-member-basic-permissions');
+ }
+
+ //------- Show the modal
+ let modalTrigger = document.querySelector("#invite_member_trigger");
+ if (modalTrigger) {
+ modalTrigger.click();
}
-
- //------- Show the modal
- let modalTrigger = document.querySelector("#invite_member_trigger");
- if (modalTrigger) {
- modalTrigger.click();
- }
}
}
diff --git a/src/registrar/templates/portfolio_members_add_new.html b/src/registrar/templates/portfolio_members_add_new.html
index 466358915..655b01852 100644
--- a/src/registrar/templates/portfolio_members_add_new.html
+++ b/src/registrar/templates/portfolio_members_add_new.html
@@ -190,3 +190,4 @@ Member Access
{% endblock portfolio_content%}
+
diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py
index f5f1a4401..01383ae77 100644
--- a/src/registrar/tests/test_views_portfolio.py
+++ b/src/registrar/tests/test_views_portfolio.py
@@ -2567,20 +2567,18 @@ def test_member_invite_for_new_users(self):
final_response = self.client.post(
reverse("new-member"),
{
- "role": "organization_member",
- "domain_request_permission_member": "view_all_requests",
+ "member_access_level": "basic",
+ "basic_org_domain_request_permissions": "view_only",
"email": self.new_member_email,
},
)
# Ensure the final submission is successful
self.assertEqual(final_response.status_code, 302) # redirects after success
+
# Validate Database Changes
portfolio_invite = PortfolioInvitation.objects.filter(
- email=self.new_member_email,
- portfolio=self.portfolio,
- roles__exact=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
- additional_permissions__exact=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS],
+ email=self.new_member_email, portfolio=self.portfolio
).first()
self.assertIsNotNone(portfolio_invite)
self.assertEqual(portfolio_invite.email, self.new_member_email)
@@ -2602,14 +2600,15 @@ def test_member_invite_for_previously_invited_member(self):
response = self.client.post(
reverse("new-member"),
{
- "role": "organization_member",
- "domain_request_permission_member": "view_all_requests",
+ "member_access_level": "basic",
+ "basic_org_domain_request_permissions": "view_only",
"email": self.invited_member_email,
},
)
- # Unsucessful form submissions return the same page with a 200
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context["form"].errors["email"][0], "An invitation already exists for this user.")
+ self.assertEqual(response.status_code, 302) # Redirects
+
+ # TODO: verify messages
+
# Validate Database has not changed
invite_count_after = PortfolioInvitation.objects.count()
self.assertEqual(invite_count_after, invite_count_before)
@@ -2631,13 +2630,14 @@ def test_member_invite_for_existing_member(self):
response = self.client.post(
reverse("new-member"),
{
- "role": "organization_member",
- "domain_request_permissions_member": "view_all_requests",
+ "member_access_level": "basic",
+ "basic_org_domain_request_permissions": "view_only",
"email": self.user.email,
},
)
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context["form"].errors["email"][0], "User is already a member of this portfolio.")
+ self.assertEqual(response.status_code, 302) # Redirects
+
+ # TODO: verify messages
# Validate Database has not changed
invite_count_after = PortfolioInvitation.objects.count()
@@ -2645,6 +2645,7 @@ def test_member_invite_for_existing_member(self):
class TestEditPortfolioMemberView(WebTest):
+ """Tests for the edit member page on portfolios"""
def setUp(self):
self.user = create_user()
diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py
index e62944c40..11384ca09 100644
--- a/src/registrar/views/utility/mixins.py
+++ b/src/registrar/views/utility/mixins.py
@@ -466,24 +466,6 @@ def has_permission(self):
return self.request.user.is_org_user(self.request)
-class PortfolioInvitationCreatePermission(PortfolioBasePermission):
- """Permission mixin that redirects to portfolio pages if user
- has access, otherwise 403"""
-
- def has_permission(self):
- """Check if this user has access to this portfolio.
-
- The user is in self.request.user and the portfolio can be looked
- up from the portfolio's primary key in self.kwargs["pk"]
- """
- has_perm = super().has_permission()
- if not has_perm:
- return False
-
- portfolio = self.request.session.get("portfolio")
- return self.request.user.has_edit_members_portfolio_permission(portfolio)
-
-
class PortfolioDomainsPermission(PortfolioBasePermission):
"""Permission mixin that allows access to portfolio domain pages if user
has access, otherwise 403"""
diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py
index 54ce5a921..c49f2daa1 100644
--- a/src/registrar/views/utility/permission_views.py
+++ b/src/registrar/views/utility/permission_views.py
@@ -3,7 +3,6 @@
import abc # abstract base class
from django.views.generic import DetailView, DeleteView, TemplateView, UpdateView
from registrar.models import Domain, DomainRequest, DomainInvitation, Portfolio
-from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.user import User
from registrar.models.user_domain_role import UserDomainRole
@@ -15,7 +14,6 @@
DomainRequestWizardPermission,
PortfolioDomainRequestsPermission,
PortfolioDomainsPermission,
- PortfolioInvitationCreatePermission,
PortfolioMemberDomainsPermission,
PortfolioMemberDomainsEditPermission,
PortfolioMemberEditPermission,
From 6965607f61d28aa400505efe231d4ecdc80c6c8a Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 11:55:17 -0700
Subject: [PATCH 25/46] Update portfolio-member-page.js
---
.../src/js/getgov/portfolio-member-page.js | 296 +++++++++---------
1 file changed, 148 insertions(+), 148 deletions(-)
diff --git a/src/registrar/assets/src/js/getgov/portfolio-member-page.js b/src/registrar/assets/src/js/getgov/portfolio-member-page.js
index 280c087f0..83fee661c 100644
--- a/src/registrar/assets/src/js/getgov/portfolio-member-page.js
+++ b/src/registrar/assets/src/js/getgov/portfolio-member-page.js
@@ -6,169 +6,169 @@ import { hookupRadioTogglerListener } from './radios.js';
// This is specifically for the Member Profile (Manage Member) Page member/invitation removal
export function initPortfolioNewMemberPageToggle() {
- document.addEventListener("DOMContentLoaded", () => {
- const wrapperDeleteAction = document.getElementById("wrapper-delete-action")
- if (wrapperDeleteAction) {
- const member_type = wrapperDeleteAction.getAttribute("data-member-type");
- const member_id = wrapperDeleteAction.getAttribute("data-member-id");
- const num_domains = wrapperDeleteAction.getAttribute("data-num-domains");
- const member_name = wrapperDeleteAction.getAttribute("data-member-name");
- const member_email = wrapperDeleteAction.getAttribute("data-member-email");
- const member_delete_url = `${member_type}-${member_id}/delete`;
- const unique_id = `${member_type}-${member_id}`;
-
- let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
- wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `for ${member_name}`);
-
- // This easter egg is only for fixtures that dont have names as we are displaying their emails
- // All prod users will have emails linked to their account
- MembersTable.addMemberModal(num_domains, member_email || "Samwise Gamgee", member_delete_url, unique_id, wrapperDeleteAction);
-
- uswdsInitializeModals();
-
- // Now the DOM and modals are ready, add listeners to the submit buttons
- const modals = document.querySelectorAll('.usa-modal__content');
-
- modals.forEach(modal => {
- const submitButton = modal.querySelector('.usa-modal__submit');
- const closeButton = modal.querySelector('.usa-modal__close');
- submitButton.addEventListener('click', () => {
- closeButton.click();
- let delete_member_form = document.getElementById("member-delete-form");
- if (delete_member_form) {
- delete_member_form.submit();
- }
- });
- });
- }
-});
+ document.addEventListener("DOMContentLoaded", () => {
+ const wrapperDeleteAction = document.getElementById("wrapper-delete-action")
+ if (wrapperDeleteAction) {
+ const member_type = wrapperDeleteAction.getAttribute("data-member-type");
+ const member_id = wrapperDeleteAction.getAttribute("data-member-id");
+ const num_domains = wrapperDeleteAction.getAttribute("data-num-domains");
+ const member_name = wrapperDeleteAction.getAttribute("data-member-name");
+ const member_email = wrapperDeleteAction.getAttribute("data-member-email");
+ const member_delete_url = `${member_type}-${member_id}/delete`;
+ const unique_id = `${member_type}-${member_id}`;
+
+ let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
+ wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `for ${member_name}`);
+
+ // This easter egg is only for fixtures that dont have names as we are displaying their emails
+ // All prod users will have emails linked to their account
+ MembersTable.addMemberModal(num_domains, member_email || "Samwise Gamgee", member_delete_url, unique_id, wrapperDeleteAction);
+
+ uswdsInitializeModals();
+
+ // Now the DOM and modals are ready, add listeners to the submit buttons
+ const modals = document.querySelectorAll('.usa-modal__content');
+
+ modals.forEach(modal => {
+ const submitButton = modal.querySelector('.usa-modal__submit');
+ const closeButton = modal.querySelector('.usa-modal__close');
+ submitButton.addEventListener('click', () => {
+ closeButton.click();
+ let delete_member_form = document.getElementById("member-delete-form");
+ if (delete_member_form) {
+ delete_member_form.submit();
+ }
+ });
+ });
+ }
+ });
}
/**
-* Hooks up specialized listeners for handling form validation and modals
-* on the Add New Member page.
-*/
+ * Hooks up specialized listeners for handling form validation and modals
+ * on the Add New Member page.
+ */
export function initAddNewMemberPageListeners() {
-let add_member_form = document.getElementById("add_member_form");
-if (!add_member_form){
- return;
-}
-document.getElementById("confirm_new_member_submit").addEventListener("click", function() {
-// Upon confirmation, submit the form
-document.getElementById("add_member_form").submit();
-});
-
-document.getElementById("add_member_form").addEventListener("submit", function(event) {
-event.preventDefault(); // Prevents the form from submitting
-const form = document.getElementById("add_member_form")
-const formData = new FormData(form);
-
-// Check if the form is valid
-// If the form is valid, open the confirmation modal
-// If the form is invalid, submit it to trigger error
-fetch(form.action, {
- method: "POST",
- body: formData,
- headers: {
- "X-Requested-With": "XMLHttpRequest",
- "X-CSRFToken": getCsrfToken()
- }
-})
-.then(response => response.json())
-.then(data => {
- if (data.is_valid) {
- // If the form is valid, show the confirmation modal before submitting
- openAddMemberConfirmationModal();
- } else {
- // If the form is not valid, trigger error messages by firing a submit event
- form.submit();
- }
-});
-});
-
-/*
-Helper function to capitalize the first letter in a string (for display purposes)
-*/
-function capitalizeFirstLetter(text) {
-if (!text) return ''; // Return empty string if input is falsy
-return text.charAt(0).toUpperCase() + text.slice(1);
-}
+ let add_member_form = document.getElementById("add_member_form");
+ if (!add_member_form){
+ return;
+ }
+ document.getElementById("confirm_new_member_submit").addEventListener("click", function() {
+ // Upon confirmation, submit the form
+ document.getElementById("add_member_form").submit();
+ });
-/*
-Populates contents of the "Add Member" confirmation modal
-*/
-function populatePermissionDetails(permission_details_div_id) {
-const permissionDetailsContainer = document.getElementById("permission_details");
-permissionDetailsContainer.innerHTML = ""; // Clear previous content
+ document.getElementById("add_member_form").addEventListener("submit", function(event) {
+ event.preventDefault(); // Prevents the form from submitting
+ const form = document.getElementById("add_member_form")
+ const formData = new FormData(form);
+
+ // Check if the form is valid
+ // If the form is valid, open the confirmation modal
+ // If the form is invalid, submit it to trigger error
+ fetch(form.action, {
+ method: "POST",
+ body: formData,
+ headers: {
+ "X-Requested-With": "XMLHttpRequest",
+ "X-CSRFToken": getCsrfToken()
+ }
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.is_valid) {
+ // If the form is valid, show the confirmation modal before submitting
+ openAddMemberConfirmationModal();
+ } else {
+ // If the form is not valid, trigger error messages by firing a submit event
+ form.submit();
+ }
+ });
+ });
-// Get all permission sections (divs with h3 and radio inputs)
-const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
+ /*
+ Helper function to capitalize the first letter in a string (for display purposes)
+ */
+ function capitalizeFirstLetter(text) {
+ if (!text) return ''; // Return empty string if input is falsy
+ return text.charAt(0).toUpperCase() + text.slice(1);
+ }
-permissionSections.forEach(section => {
- // Find the element text
- const sectionTitle = section.textContent;
+ /*
+ Populates contents of the "Add Member" confirmation modal
+ */
+ function populatePermissionDetails(permission_details_div_id) {
+ const permissionDetailsContainer = document.getElementById("permission_details");
+ permissionDetailsContainer.innerHTML = ""; // Clear previous content
- // Find the associated radio buttons container (next fieldset)
- const fieldset = section.nextElementSibling;
+ // Get all permission sections (divs with h3 and radio inputs)
+ const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
- if (fieldset && fieldset.tagName.toLowerCase() === 'fieldset') {
- // Get the selected radio button within this fieldset
- const selectedRadio = fieldset.querySelector('input[type="radio"]:checked');
+ permissionSections.forEach(section => {
+ // Find the element text
+ const sectionTitle = section.textContent;
- // If a radio button is selected, get its label text
- let selectedPermission = "No permission selected";
- if (selectedRadio) {
- const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`);
- selectedPermission = label ? label.textContent : "No permission selected";
- }
+ // Find the associated radio buttons container (next fieldset)
+ const fieldset = section.nextElementSibling;
- // Create new elements for the modal content
- const titleElement = document.createElement("h4");
- titleElement.textContent = sectionTitle;
- titleElement.classList.add("text-primary");
- titleElement.classList.add("margin-bottom-0");
-
- const permissionElement = document.createElement("p");
- permissionElement.textContent = selectedPermission;
- permissionElement.classList.add("margin-top-0");
-
- // Append to the modal content container
- permissionDetailsContainer.appendChild(titleElement);
- permissionDetailsContainer.appendChild(permissionElement);
- }
-});
-}
+ if (fieldset && fieldset.tagName.toLowerCase() === 'fieldset') {
+ // Get the selected radio button within this fieldset
+ const selectedRadio = fieldset.querySelector('input[type="radio"]:checked');
+
+ // If a radio button is selected, get its label text
+ let selectedPermission = "No permission selected";
+ if (selectedRadio) {
+ const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`);
+ selectedPermission = label ? label.textContent : "No permission selected";
+ }
-/*
-Updates and opens the "Add Member" confirmation modal.
-*/
-function openAddMemberConfirmationModal() {
- //------- Populate modal details
- // Get email value
- let emailValue = document.getElementById('id_email').value;
- document.getElementById('modalEmail').textContent = emailValue;
-
- // Get selected radio button for access level
- let selectedAccess = document.querySelector('input[name="member_access_level"]:checked');
- // Set the selected permission text to 'Basic' or 'Admin' (the value of the selected radio button)
- // This value does not have the first letter capitalized so let's capitalize it
- let accessText = selectedAccess ? capitalizeFirstLetter(selectedAccess.value) : "No access level selected";
- document.getElementById('modalAccessLevel').textContent = accessText;
-
- // Populate permission details based on access level
- if (selectedAccess && selectedAccess.value === 'admin') {
- populatePermissionDetails('new-member-admin-permissions');
- } else {
- populatePermissionDetails('new-member-basic-permissions');
+ // Create new elements for the modal content
+ const titleElement = document.createElement("h4");
+ titleElement.textContent = sectionTitle;
+ titleElement.classList.add("text-primary");
+ titleElement.classList.add("margin-bottom-0");
+
+ const permissionElement = document.createElement("p");
+ permissionElement.textContent = selectedPermission;
+ permissionElement.classList.add("margin-top-0");
+
+ // Append to the modal content container
+ permissionDetailsContainer.appendChild(titleElement);
+ permissionDetailsContainer.appendChild(permissionElement);
+ }
+ });
}
- //------- Show the modal
- let modalTrigger = document.querySelector("#invite_member_trigger");
- if (modalTrigger) {
- modalTrigger.click();
- }
-}
+ /*
+ Updates and opens the "Add Member" confirmation modal.
+ */
+ function openAddMemberConfirmationModal() {
+ //------- Populate modal details
+ // Get email value
+ let emailValue = document.getElementById('id_email').value;
+ document.getElementById('modalEmail').textContent = emailValue;
+
+ // Get selected radio button for access level
+ let selectedAccess = document.querySelector('input[name="member_access_level"]:checked');
+ // Set the selected permission text to 'Basic' or 'Admin' (the value of the selected radio button)
+ // This value does not have the first letter capitalized so let's capitalize it
+ let accessText = selectedAccess ? capitalizeFirstLetter(selectedAccess.value) : "No access level selected";
+ document.getElementById('modalAccessLevel').textContent = accessText;
+
+ // Populate permission details based on access level
+ if (selectedAccess && selectedAccess.value === 'admin') {
+ populatePermissionDetails('new-member-admin-permissions');
+ } else {
+ populatePermissionDetails('new-member-basic-permissions');
+ }
+
+ //------- Show the modal
+ let modalTrigger = document.querySelector("#invite_member_trigger");
+ if (modalTrigger) {
+ modalTrigger.click();
+ }
+ }
}
From adfd6be7bbba732cce481c42b0769cf79e2bbeac Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 11:59:33 -0700
Subject: [PATCH 26/46] Fix some merge things
---
src/registrar/views/portfolios.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py
index a238de3fc..855194f6b 100644
--- a/src/registrar/views/portfolios.py
+++ b/src/registrar/views/portfolios.py
@@ -1,15 +1,17 @@
import logging
+from django.conf import settings
+
from django.http import Http404, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.contrib import messages
-from django.conf import settings
from registrar.forms import portfolio as portfolioForms
from registrar.models import Portfolio, User
from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.user_portfolio_permission import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
+from registrar.utility.email import EmailSendingError
from registrar.views.utility.mixins import PortfolioMemberPermission
from registrar.views.utility.permission_views import (
PortfolioDomainRequestsPermissionView,
@@ -505,7 +507,6 @@ def get(self, request):
return render(request, "portfolio_members.html")
-
class NewMemberView(PortfolioMembersPermissionView, FormMixin):
template_name = "portfolio_members_add_new.html"
From c3480893dab229ce6a9f8e5ef65cfa052126c2d3 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 12:06:40 -0700
Subject: [PATCH 27/46] Readd modelforms
---
src/registrar/forms/portfolio.py | 276 +++++++++++++++++++------------
1 file changed, 166 insertions(+), 110 deletions(-)
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index 34d334a3b..935c7c019 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -5,12 +5,14 @@
from django.core.validators import RegexValidator
from django.core.validators import MaxLengthValidator
from django.utils.safestring import mark_safe
+
from registrar.models import (
- User,
+ PortfolioInvitation,
UserPortfolioPermission,
DomainInformation,
Portfolio,
SeniorOfficial,
+ User,
)
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
@@ -109,6 +111,169 @@ def clean(self):
return cleaned_data
+class PortfolioMemberForm(forms.ModelForm):
+ """
+ Form for updating a portfolio member.
+ """
+
+ roles = forms.MultipleChoiceField(
+ choices=UserPortfolioRoleChoices.choices,
+ widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
+ required=False,
+ label="Roles",
+ )
+
+ additional_permissions = forms.MultipleChoiceField(
+ choices=UserPortfolioPermissionChoices.choices,
+ widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
+ required=False,
+ label="Additional Permissions",
+ )
+
+ class Meta:
+ model = UserPortfolioPermission
+ fields = [
+ "roles",
+ "additional_permissions",
+ ]
+
+
+class PortfolioInvitedMemberForm(forms.ModelForm):
+ """
+ Form for updating a portfolio invited member.
+ """
+
+ roles = forms.MultipleChoiceField(
+ choices=UserPortfolioRoleChoices.choices,
+ widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
+ required=False,
+ label="Roles",
+ )
+
+ additional_permissions = forms.MultipleChoiceField(
+ choices=UserPortfolioPermissionChoices.choices,
+ widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
+ required=False,
+ label="Additional Permissions",
+ )
+
+ class Meta:
+ model = PortfolioInvitation
+ fields = [
+ "roles",
+ "additional_permissions",
+ ]
+
+
+class NewMemberForm(forms.ModelForm):
+ member_access_level = forms.ChoiceField(
+ label="Select permission",
+ choices=[("admin", "Admin Access"), ("basic", "Basic Access")],
+ widget=forms.RadioSelect(attrs={"class": "usa-radio__input usa-radio__input--tile"}),
+ required=True,
+ error_messages={
+ "required": "Member access level is required",
+ },
+ )
+ admin_org_domain_request_permissions = forms.ChoiceField(
+ label="Select permission",
+ choices=[("view_only", "View all requests"), ("view_and_create", "View all requests plus create requests")],
+ widget=forms.RadioSelect,
+ required=True,
+ error_messages={
+ "required": "Admin domain request permission is required",
+ },
+ )
+ admin_org_members_permissions = forms.ChoiceField(
+ label="Select permission",
+ choices=[("view_only", "View all members"), ("view_and_create", "View all members plus manage members")],
+ widget=forms.RadioSelect,
+ required=True,
+ error_messages={
+ "required": "Admin member permission is required",
+ },
+ )
+ basic_org_domain_request_permissions = forms.ChoiceField(
+ label="Select permission",
+ choices=[
+ ("view_only", "View all requests"),
+ ("view_and_create", "View all requests plus create requests"),
+ ("no_access", "No access"),
+ ],
+ widget=forms.RadioSelect,
+ required=True,
+ error_messages={
+ "required": "Basic member permission is required",
+ },
+ )
+
+ email = forms.EmailField(
+ label="Enter the email of the member you'd like to invite",
+ max_length=None,
+ error_messages={
+ "invalid": ("Enter an email address in the required format, like name@example.com."),
+ "required": ("Enter an email address in the required format, like name@example.com."),
+ },
+ validators=[
+ MaxLengthValidator(
+ 320,
+ message="Response must be less than 320 characters.",
+ )
+ ],
+ required=True,
+ )
+
+ class Meta:
+ model = User
+ fields = ["email"]
+
+ def clean(self):
+ cleaned_data = super().clean()
+
+ # Lowercase the value of the 'email' field
+ email_value = cleaned_data.get("email")
+ if email_value:
+ cleaned_data["email"] = email_value.lower()
+
+ ##########################################
+ # TODO: future ticket
+ # (invite new member)
+ ##########################################
+ # Check for an existing user (if there isn't any, send an invite)
+ # if email_value:
+ # try:
+ # existingUser = User.objects.get(email=email_value)
+ # except User.DoesNotExist:
+ # raise forms.ValidationError("User with this email does not exist.")
+
+ member_access_level = cleaned_data.get("member_access_level")
+
+ # Intercept the error messages so that we don't validate hidden inputs
+ if not member_access_level:
+ # If no member access level has been selected, delete error messages
+ # for all hidden inputs (which is everything except the e-mail input
+ # and member access selection)
+ for field in self.fields:
+ if field in self.errors and field != "email" and field != "member_access_level":
+ del self.errors[field]
+ return cleaned_data
+
+ basic_dom_req_error = "basic_org_domain_request_permissions"
+ admin_dom_req_error = "admin_org_domain_request_permissions"
+ admin_member_error = "admin_org_members_permissions"
+
+ if member_access_level == "admin" and basic_dom_req_error in self.errors:
+ # remove the error messages pertaining to basic permission inputs
+ del self.errors[basic_dom_req_error]
+ elif member_access_level == "basic":
+ # remove the error messages pertaining to admin permission inputs
+ if admin_dom_req_error in self.errors:
+ del self.errors[admin_dom_req_error]
+ if admin_member_error in self.errors:
+ del self.errors[admin_member_error]
+ return cleaned_data
+
+
class BasePortfolioMemberForm(forms.Form):
required_star = '*'
role = forms.ChoiceField(
@@ -332,112 +497,3 @@ def map_cleaned_data_to_instance(self, cleaned_data, instance):
role_permissions = UserPortfolioPermission.get_portfolio_permissions(instance.roles, [], get_list=False)
instance.additional_permissions = list(additional_permissions - role_permissions)
return instance
-
-
-class NewMemberForm(forms.ModelForm):
- member_access_level = forms.ChoiceField(
- label="Select permission",
- choices=[("admin", "Admin Access"), ("basic", "Basic Access")],
- widget=forms.RadioSelect(attrs={"class": "usa-radio__input usa-radio__input--tile"}),
- required=True,
- error_messages={
- "required": "Member access level is required",
- },
- )
- admin_org_domain_request_permissions = forms.ChoiceField(
- label="Select permission",
- choices=[("view_only", "View all requests"), ("view_and_create", "View all requests plus create requests")],
- widget=forms.RadioSelect,
- required=True,
- error_messages={
- "required": "Admin domain request permission is required",
- },
- )
- admin_org_members_permissions = forms.ChoiceField(
- label="Select permission",
- choices=[("view_only", "View all members"), ("view_and_create", "View all members plus manage members")],
- widget=forms.RadioSelect,
- required=True,
- error_messages={
- "required": "Admin member permission is required",
- },
- )
- basic_org_domain_request_permissions = forms.ChoiceField(
- label="Select permission",
- choices=[
- ("view_only", "View all requests"),
- ("view_and_create", "View all requests plus create requests"),
- ("no_access", "No access"),
- ],
- widget=forms.RadioSelect,
- required=True,
- error_messages={
- "required": "Basic member permission is required",
- },
- )
-
- email = forms.EmailField(
- label="Enter the email of the member you'd like to invite",
- max_length=None,
- error_messages={
- "invalid": ("Enter an email address in the required format, like name@example.com."),
- "required": ("Enter an email address in the required format, like name@example.com."),
- },
- validators=[
- MaxLengthValidator(
- 320,
- message="Response must be less than 320 characters.",
- )
- ],
- required=True,
- )
-
- class Meta:
- model = User
- fields = ["email"]
-
- def clean(self):
- cleaned_data = super().clean()
-
- # Lowercase the value of the 'email' field
- email_value = cleaned_data.get("email")
- if email_value:
- cleaned_data["email"] = email_value.lower()
-
- ##########################################
- # TODO: future ticket
- # (invite new member)
- ##########################################
- # Check for an existing user (if there isn't any, send an invite)
- # if email_value:
- # try:
- # existingUser = User.objects.get(email=email_value)
- # except User.DoesNotExist:
- # raise forms.ValidationError("User with this email does not exist.")
-
- member_access_level = cleaned_data.get("member_access_level")
-
- # Intercept the error messages so that we don't validate hidden inputs
- if not member_access_level:
- # If no member access level has been selected, delete error messages
- # for all hidden inputs (which is everything except the e-mail input
- # and member access selection)
- for field in self.fields:
- if field in self.errors and field != "email" and field != "member_access_level":
- del self.errors[field]
- return cleaned_data
-
- basic_dom_req_error = "basic_org_domain_request_permissions"
- admin_dom_req_error = "admin_org_domain_request_permissions"
- admin_member_error = "admin_org_members_permissions"
-
- if member_access_level == "admin" and basic_dom_req_error in self.errors:
- # remove the error messages pertaining to basic permission inputs
- del self.errors[basic_dom_req_error]
- elif member_access_level == "basic":
- # remove the error messages pertaining to admin permission inputs
- if admin_dom_req_error in self.errors:
- del self.errors[admin_dom_req_error]
- if admin_member_error in self.errors:
- del self.errors[admin_member_error]
- return cleaned_data
From a9923f4cc951022ac5f4068bf6d9ae106c7f9fd3 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 12:09:01 -0700
Subject: [PATCH 28/46] Update portfolio.py
---
src/registrar/forms/portfolio.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index 935c7c019..5e3a7b324 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -273,7 +273,6 @@ def clean(self):
del self.errors[admin_member_error]
return cleaned_data
-
class BasePortfolioMemberForm(forms.Form):
required_star = '*'
role = forms.ChoiceField(
From f19ff3cd66e5ea4d9b1ac671c2c69d3c689929c7 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 12:25:21 -0700
Subject: [PATCH 29/46] PR suggestions
---
src/registrar/admin.py | 4 --
src/registrar/forms/__init__.py | 1 +
src/registrar/forms/portfolio.py | 43 ++++++-------------
.../models/user_portfolio_permission.py | 7 ++-
.../views/utility/permission_views.py | 1 +
5 files changed, 22 insertions(+), 34 deletions(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 72eff6d79..4465b7098 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -4004,10 +4004,6 @@ def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["dns_prototype_flag"] = flag_is_active_for_user(request.user, "dns_prototype_flag")
- # Normally you have to first enable the org feature then navigate to an org before you see these.
- # Lets just auto-populate it on page load to make development easier.
- extra_context["organization_members"] = flag_is_active_for_user(request.user, "organization_members")
- extra_context["organization_requests"] = flag_is_active_for_user(request.user, "organization_requests")
return super().changelist_view(request, extra_context=extra_context)
diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py
index 033e955ed..121e2b3f7 100644
--- a/src/registrar/forms/__init__.py
+++ b/src/registrar/forms/__init__.py
@@ -13,4 +13,5 @@
)
from .portfolio import (
PortfolioOrgAddressForm,
+ PortfolioMemberForm,
)
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index 5e3a7b324..ecd21b8ee 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -273,7 +273,11 @@ def clean(self):
del self.errors[admin_member_error]
return cleaned_data
+
class BasePortfolioMemberForm(forms.Form):
+ """Base form for the PortfolioMemberForm and PortfolioInvitedMemberForm"""
+
+ # The label for each of these has a red "required" star. We can just embed that here for simplicity.
required_star = '*'
role = forms.ChoiceField(
choices=[
@@ -354,25 +358,11 @@ def __init__(self, *args, instance=None, **kwargs):
}
def clean(self):
- """
- Validates form data based on selected role and its required fields.
-
- Since form fields are dynamically shown/hidden via JavaScript based on role selection,
- we only validate fields that are relevant to the selected role:
- - organization_admin: ["member_permission_admin", "domain_request_permission_admin"]
- - organization_member: ["domain_request_permission_member"]
- This ensures users aren't required to fill out hidden fields and maintains
- proper validation based on their role selection.
-
- NOTE: This page uses ROLE_REQUIRED_FIELDS for the aforementioned mapping.
- Raises:
- ValueError: If ROLE_REQUIRED_FIELDS references a non-existent form field
- """
+ """Validates form data based on selected role and its required fields."""
cleaned_data = super().clean()
role = cleaned_data.get("role")
- # Get required fields for the selected role.
- # Then validate all required fields for the role.
+ # Get required fields for the selected role. Then validate all required fields for the role.
required_fields = self.ROLE_REQUIRED_FIELDS.get(role, [])
for field_name in required_fields:
# Helpful error for if this breaks
@@ -394,15 +384,17 @@ def save(self):
self.instance.save()
return self.instance
+ # Explanation of how map_instance_to_form / map_cleaned_data_to_instance work:
+ # map_instance_to_form => called on init to set self.instance.
+ # Converts the incoming object (usually PortfolioInvitation or UserPortfolioPermission)
+ # into a dictionary representation for the form to use automatically.
+
+ # map_cleaned_data_to_instance => called on save() to save the instance to the db.
+ # Takes the self.cleaned_data dict, and converts this dict back to the object.
+
def map_instance_to_form(self, instance):
"""
Maps user instance to form fields, handling roles and permissions.
-
- Determines:
- - User's role (admin vs member)
- - Domain request permissions (EDIT_REQUESTS, VIEW_ALL_REQUESTS, or "no_access")
- - Member management permissions (EDIT_MEMBERS or VIEW_MEMBERS)
-
Returns form data dictionary with appropriate permission levels based on user role:
{
"role": "organization_admin" or "organization_member",
@@ -462,13 +454,6 @@ def map_instance_to_form(self, instance):
def map_cleaned_data_to_instance(self, cleaned_data, instance):
"""
Maps cleaned data to a member instance, setting roles and permissions.
-
- Additional permissions logic:
- - For org admins: Adds domain request and member admin permissions if selected
- - For other roles: Adds domain request member permissions if not 'no_access'
- - Automatically adds VIEW permissions when EDIT permissions are granted
- - Filters out permissions already granted by base role
-
Args:
cleaned_data (dict): Cleaned data containing role and permission choices
instance: Instance to update
diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py
index f312f3dd0..25abb6748 100644
--- a/src/registrar/models/user_portfolio_permission.py
+++ b/src/registrar/models/user_portfolio_permission.py
@@ -111,7 +111,12 @@ def _get_portfolio_permissions(self):
@classmethod
def get_portfolio_permissions(cls, roles, additional_permissions, get_list=True):
- """Class method to return a list of permissions based on roles and addtl permissions"""
+ """Class method to return a list of permissions based on roles and addtl permissions.
+ Params:
+ roles => An array of roles
+ additional_permissions => An array of additional_permissions
+ get_list => If true, returns a list of perms. If false, returns a set of perms.
+ """
# Use a set to avoid duplicate permissions
portfolio_permissions = set()
if roles:
diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py
index c49f2daa1..a3067d3a2 100644
--- a/src/registrar/views/utility/permission_views.py
+++ b/src/registrar/views/utility/permission_views.py
@@ -1,6 +1,7 @@
"""View classes that enforce authorization."""
import abc # abstract base class
+
from django.views.generic import DetailView, DeleteView, TemplateView, UpdateView
from registrar.models import Domain, DomainRequest, DomainInvitation, Portfolio
from registrar.models.user import User
From 0e1d07d59bcadbe81d1c60b527a92f3482ef1718 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 12:32:47 -0700
Subject: [PATCH 30/46] Update portfolio.py
---
src/registrar/forms/portfolio.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index ecd21b8ee..6e1e7d43c 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -331,7 +331,9 @@ class BasePortfolioMemberForm(forms.Form):
},
)
- # Tracks what form elements are required for a given role choice
+ # Tracks what form elements are required for a given role choice.
+ # All of the fields included here have "required=False" by default as they are conditionally required.
+ # see def clean() for more details.
ROLE_REQUIRED_FIELDS = {
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
"domain_request_permission_admin",
From a314649de2e42c209b02e7565ab19f3736443545 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 12:33:34 -0700
Subject: [PATCH 31/46] Update portfolio.py
---
src/registrar/forms/portfolio.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index 6e1e7d43c..ddfa93bc1 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -387,7 +387,7 @@ def save(self):
return self.instance
# Explanation of how map_instance_to_form / map_cleaned_data_to_instance work:
- # map_instance_to_form => called on init to set self.instance.
+ # map_instance_to_form => called on init to set self.initial.
# Converts the incoming object (usually PortfolioInvitation or UserPortfolioPermission)
# into a dictionary representation for the form to use automatically.
From dec9e3362dc2534b6953b122a46bc599585ef87e Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 13:02:27 -0700
Subject: [PATCH 32/46] Condense logic and change names
---
src/registrar/forms/portfolio.py | 89 +++++++++++++++-----------------
1 file changed, 42 insertions(+), 47 deletions(-)
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index ddfa93bc1..ce164607e 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -281,6 +281,7 @@ class BasePortfolioMemberForm(forms.Form):
required_star = '*'
role = forms.ChoiceField(
choices=[
+ # Uses .value because the choice has a different label (on /admin)
(UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value, "Admin access"),
(UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "Basic access"),
],
@@ -345,10 +346,12 @@ class BasePortfolioMemberForm(forms.Form):
}
def __init__(self, *args, instance=None, **kwargs):
+ """Initialize self.instance, self.initial, and descriptions under each radio button.
+ Uses map_instance_to_initial to set the initial dictionary."""
super().__init__(*args, **kwargs)
if instance:
self.instance = instance
- self.initial = self.map_instance_to_form(self.instance)
+ self.initial = self.map_instance_to_initial(self.instance)
# Adds a
description beneath each role option
self.fields["role"].descriptions = {
"organization_admin": UserPortfolioRoleChoices.get_role_description(
@@ -359,6 +362,14 @@ def __init__(self, *args, instance=None, **kwargs):
),
}
+ def save(self):
+ """Saves self.instance by grabbing data from self.cleaned_data.
+ Uses map_cleaned_data_to_instance.
+ """
+ self.instance = self.map_cleaned_data_to_instance(self.cleaned_data, self.instance)
+ self.instance.save()
+ return self.instance
+
def clean(self):
"""Validates form data based on selected role and its required fields."""
cleaned_data = super().clean()
@@ -380,23 +391,17 @@ def clean(self):
return cleaned_data
- def save(self):
- """Save the form data to the instance"""
- self.instance = self.map_cleaned_data_to_instance(self.cleaned_data, self.instance)
- self.instance.save()
- return self.instance
-
- # Explanation of how map_instance_to_form / map_cleaned_data_to_instance work:
- # map_instance_to_form => called on init to set self.initial.
+ # Explanation of how map_instance_to_initial / map_cleaned_data_to_instance work:
+ # map_instance_to_initial => called on init to set self.initial.
# Converts the incoming object (usually PortfolioInvitation or UserPortfolioPermission)
# into a dictionary representation for the form to use automatically.
# map_cleaned_data_to_instance => called on save() to save the instance to the db.
# Takes the self.cleaned_data dict, and converts this dict back to the object.
- def map_instance_to_form(self, instance):
+ def map_instance_to_initial(self, instance):
"""
- Maps user instance to form fields, handling roles and permissions.
+ Maps self.instance to self.initial, handling roles and permissions.
Returns form data dictionary with appropriate permission levels based on user role:
{
"role": "organization_admin" or "organization_member",
@@ -405,57 +410,47 @@ def map_instance_to_form(self, instance):
"domain_request_permission_member": permission level if member
}
"""
- if not instance:
- return {}
-
# Function variables
form_data = {}
perms = UserPortfolioPermission.get_portfolio_permissions(
instance.roles, instance.additional_permissions, get_list=False
)
- # Explanation of this logic pattern: we can only display one item in the list at a time.
- # But how do we determine what is most important to display in a list? Order-based hierarchy.
- # Example: print(instance.roles) => (output) ["organization_admin", "organization_member"]
- # If we can only pick one item in this list, we should pick organization_admin.
-
- # Get role
- roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN, UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
- role = next((role for role in roles if role in instance.roles), None)
- is_admin = role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN
-
- # Get domain request permission level
- # First we get permissions we expect to display (ordered hierarchically).
- # Then we check if this item exists in the list and return the first instance of it.
- domain_permissions = [
+ # Get the available options for roles, domains, and member.
+ roles = [
+ UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
+ UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
+ ]
+ domain_perms = [
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
]
- domain_request_permission = next((perm for perm in domain_permissions if perm in perms), None)
-
- # Get member permission level.
- member_permissions = [UserPortfolioPermissionChoices.EDIT_MEMBERS, UserPortfolioPermissionChoices.VIEW_MEMBERS]
- member_permission = next((perm for perm in member_permissions if perm in perms), None)
-
- # Build form data based on role.
- form_data = {
- "role": role,
- "member_permission_admin": getattr(member_permission, "value", None) if is_admin else None,
- "domain_request_permission_admin": getattr(domain_request_permission, "value", None) if is_admin else None,
- "domain_request_permission_member": (
- getattr(domain_request_permission, "value", None) if not is_admin else None
- ),
- }
+ member_perms = [
+ UserPortfolioPermissionChoices.EDIT_MEMBERS,
+ UserPortfolioPermissionChoices.VIEW_MEMBERS,
+ ]
- # Edgecase: Member uses a special form value for None called "no_access". This ensures a form selection.
- if domain_request_permission is None and not is_admin:
- form_data["domain_request_permission_member"] = "no_access"
+ # Build form data based on role (which options are available).
+ # Get which one should be "selected" by assuming that EDIT takes precedence over view,
+ # and ADMIN takes precedence over MEMBER.
+ selected_role = next((role for role in roles if role in instance.roles), None)
+ form_data = {"role": selected_role}
+ is_admin = selected_role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN
+ if is_admin:
+ selected_domain_permission = next((perm for perm in domain_perms if perm in perms), None)
+ selected_member_permission = next((perm for perm in member_perms if perm in perms), None)
+ form_data["domain_request_permission_admin"] = selected_domain_permission
+ form_data["member_permission_admin"] = selected_member_permission
+ else:
+ # Edgecase: Member uses a special form value for None called "no_access". This ensures a form selection.
+ selected_domain_permission = next((perm for perm in domain_perms if perm in perms), "no_access")
+ form_data["domain_request_permission_member"] = selected_domain_permission
return form_data
def map_cleaned_data_to_instance(self, cleaned_data, instance):
"""
- Maps cleaned data to a member instance, setting roles and permissions.
+ Maps self.cleaned_data to self.instance, setting roles and permissions.
Args:
cleaned_data (dict): Cleaned data containing role and permission choices
instance: Instance to update
From 57c7c6f709051d41ed392124d732d670d06257fb Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 13:56:29 -0700
Subject: [PATCH 33/46] Revert "revert csv-export"
This reverts commit b0d1bc26da85e0e6ac8bae4746d529089a6b61de.
---
src/registrar/tests/test_reports.py | 138 ++++----
src/registrar/utility/csv_export.py | 519 ++++++++++++++++++++++------
2 files changed, 476 insertions(+), 181 deletions(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index f91c5b299..cafaff7b1 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -71,8 +71,8 @@ def test_generate_federal_report(self):
fake_open = mock_open()
expected_file_content = [
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
+ call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
- call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
]
@@ -93,8 +93,8 @@ def test_generate_full_report(self):
fake_open = mock_open()
expected_file_content = [
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
+ call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
- call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("zdomain12.gov,Interstate,,,,,(blank)\r\n"),
@@ -251,32 +251,35 @@ def test_domain_data_type(self):
# We expect READY domains,
# sorted alphabetially by domain name
expected_content = (
- "Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,City,State,SO,"
- "SO email,Security contact email,Domain managers,Invited domain managers\n"
- "cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,World War I Centennial Commission,,,,(blank),,,"
+ "Domain name,Status,First ready on,Expiration date,Domain type,Agency,"
+ "Organization name,City,State,SO,SO email,"
+ "Security contact email,Domain managers,Invited domain managers\n"
+ "adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,"
+ "Portfolio 1 Federal Agency,,,, ,,(blank),"
+ "meoward@rocks.com,squeaker@rocks.com\n"
+ "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,"
+ "Portfolio 1 Federal Agency,,,, ,,(blank),"
+ '"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
+ "cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,"
+ "World War I Centennial Commission,,,, ,,(blank),"
"meoward@rocks.com,\n"
- "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,"
- ',,,(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
- "woofwardthethird@rocks.com\n"
- "adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,"
+ "adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,"
"squeaker@rocks.com\n"
- "bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
- "bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
- "bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
- "ddomain3.gov,On hold,(blank),2023-11-15,Federal,Armed Forces Retirement Home,,,,,,"
- "security@mail.gov,,\n"
- "sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
- "xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
- "zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
- "adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,,(blank),,,"
- "meoward@rocks.com,squeaker@rocks.com\n"
- "zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,,(blank),,,meoward@rocks.com,\n"
+ "bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
+ "bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
+ "bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
+ "ddomain3.gov,On hold,(blank),2023-11-15,Federal,"
+ "Armed Forces Retirement Home,,,, ,,security@mail.gov,,\n"
+ "sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
+ "xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
+ "zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
+ "zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,, ,,(blank),meoward@rocks.com,\n"
)
+
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
- self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -312,20 +315,17 @@ def test_domain_data_type_user(self):
# We expect only domains associated with the user
expected_content = (
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,"
- "City,State,SO,SO email,"
- "Security contact email,Domain managers,Invited domain managers\n"
- "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,, ,,"
- '(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
- "woofwardthethird@rocks.com\n"
- "adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,, ,,(blank),"
+ "City,State,SO,SO email,Security contact email,Domain managers,Invited domain managers\n"
+ "adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
'"info@example.com, meoward@rocks.com",squeaker@rocks.com\n'
+ "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
+ '"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
- self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -493,17 +493,17 @@ def test_domain_data_full(self):
# sorted alphabetially by domain name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
- "cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
- "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
- "adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
- "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
+ "defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
+ "cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
+ "adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
+ "ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
"zdomain12.gov,Interstate,,,,,(blank)\n"
)
+
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
- self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -533,16 +533,16 @@ def test_domain_data_federal(self):
# sorted alphabetially by domain name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
- "cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
- "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
- "adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
- "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
+ "defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
+ "cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
+ "adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
+ "ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
)
+
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
- self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -587,13 +587,13 @@ def test_domain_growth(self):
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,"
"State,Status,Expiration date, Deleted\n"
- "cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,(blank)\n"
- "adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n"
- "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n"
- "zdomain12.govInterstateReady(blank)\n"
+ "cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Ready,(blank)\n"
+ "adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
+ "cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n"
+ "zdomain12.gov,Interstate,Ready,(blank)\n"
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n"
- "sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank),2024-04-02\n"
- "xdomain7.gov,FederalArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
+ "sdomain8.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
+ "xdomain7.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
@@ -611,7 +611,6 @@ def test_domain_managed(self):
squeaker@rocks.com is invited to domain2 (DNS_NEEDED) and domain10 (No managers).
She should show twice in this report but not in test_DomainManaged."""
- self.maxDiff = None
# Create a CSV file in memory
csv_file = StringIO()
# Call the export functions
@@ -646,7 +645,6 @@ def test_domain_managed(self):
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
- self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -683,7 +681,6 @@ def test_domain_unmanaged(self):
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
-
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -721,10 +718,9 @@ def test_domain_request_growth(self):
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
-
self.assertEqual(csv_content, expected_content)
- @less_console_noise_decorator
+ # @less_console_noise_decorator
def test_domain_request_data_full(self):
"""Tests the full domain request report."""
# Remove "Submitted at" because we can't guess this immutable, dynamically generated test data
@@ -766,35 +762,34 @@ def test_domain_request_data_full(self):
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()
+
expected_content = (
# Header
- "Domain request,Status,Domain type,Federal type,"
- "Federal agency,Organization name,Election office,City,State/territory,"
- "Region,Creator first name,Creator last name,Creator email,Creator approved domains count,"
- "Creator active requests count,Alternative domains,SO first name,SO last name,SO email,"
- "SO title/role,Request purpose,Request additional details,Other contacts,"
+ "Domain request,Status,Domain type,Federal type,Federal agency,Organization name,Election office,"
+ "City,State/territory,Region,Creator first name,Creator last name,Creator email,"
+ "Creator approved domains count,Creator active requests count,Alternative domains,SO first name,"
+ "SO last name,SO email,SO title/role,Request purpose,Request additional details,Other contacts,"
"CISA regional representative,Current websites,Investigator\n"
# Content
- "city5.gov,,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
- "Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
- "city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
- "testy@town.com,"
+ "city5.gov,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
- 'city3.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,"cheeseville.gov, city1.gov,'
- 'igorville.gov",Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,CISA-first-name '
- "CISA-last-name "
- '| There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, Testy Tester '
- 'testy2@town.com"'
- ',test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
- "city4.gov,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
- "Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
- "testy2@town.com"
- ",cisaRep@igorville.gov,city.com,\n"
- "city6.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
- "Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
- "testy2@town.com,"
+ "city2.gov,In review,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
+ "Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
+ "city3.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,"
+ '"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | '
+ 'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
+ 'Testy Tester testy2@town.com",'
+ 'test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
+ "city4.gov,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,"
+ "Tester,testy@town.com,"
+ "Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
+ "Testy Tester testy2@town.com,"
+ "cisaRep@igorville.gov,city.com,\n"
+ "city6.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
+ "Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester testy2@town.com,"
"cisaRep@igorville.gov,city.com,\n"
)
+
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
@@ -862,7 +857,6 @@ def test_member_export(self):
# Create a request and add the user to the request
request = self.factory.get("/")
request.user = self.user
- self.maxDiff = None
# Add portfolio to session
request = GenericTestHelper._mock_user_request_for_factory(request)
request.session["portfolio"] = self.portfolio_1
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index a03e51de5..97feae20c 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -414,8 +414,11 @@ def get_model_annotation_dict(cls, request=None, **kwargs):
)
.values(*shared_columns)
)
-
- return convert_queryset_to_dict(permissions.union(invitations), is_model=False)
+ # Adding a order_by increases output predictability.
+ # Doesn't matter as much for normal use, but makes tests easier.
+ # We should also just be ordering by default anyway.
+ members = permissions.union(invitations).order_by("email_display")
+ return convert_queryset_to_dict(members, is_model=False)
@classmethod
def get_invited_by_query(cls, object_id_query):
@@ -525,6 +528,115 @@ def model(cls):
# Return the model class that this export handles
return DomainInformation
+ @classmethod
+ def get_computed_fields(cls, **kwargs):
+ """
+ Get a dict of computed fields.
+ """
+ # NOTE: These computed fields imitate @Property functions in the Domain model and Portfolio model where needed.
+ # This is for performance purposes. Since we are working with dictionary values and not
+ # model objects as we export data, trying to reinstate model objects in order to grab @property
+ # values negatively impacts performance. Therefore, we will follow best practice and use annotations
+ return {
+ "converted_generic_org_type": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__organization_type")),
+ # Otherwise, return the natively assigned value
+ default=F("generic_org_type"),
+ output_field=CharField(),
+ ),
+ "converted_federal_agency": Case(
+ # When portfolio is present, use its value instead
+ When(
+ Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
+ then=F("portfolio__federal_agency__agency"),
+ ),
+ # Otherwise, return the natively assigned value
+ default=F("federal_agency__agency"),
+ output_field=CharField(),
+ ),
+ "converted_federal_type": Case(
+ # When portfolio is present, use its value instead
+ # NOTE: this is an @Property funciton in portfolio.
+ When(
+ Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
+ then=F("portfolio__federal_agency__federal_type"),
+ ),
+ # Otherwise, return the natively assigned value
+ default=F("federal_type"),
+ output_field=CharField(),
+ ),
+ "converted_organization_name": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__organization_name")),
+ # Otherwise, return the natively assigned value
+ default=F("organization_name"),
+ output_field=CharField(),
+ ),
+ "converted_city": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__city")),
+ # Otherwise, return the natively assigned value
+ default=F("city"),
+ output_field=CharField(),
+ ),
+ "converted_state_territory": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__state_territory")),
+ # Otherwise, return the natively assigned value
+ default=F("state_territory"),
+ output_field=CharField(),
+ ),
+ "converted_so_email": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__senior_official__email")),
+ # Otherwise, return the natively assigned senior official
+ default=F("senior_official__email"),
+ output_field=CharField(),
+ ),
+ "converted_senior_official_last_name": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__senior_official__last_name")),
+ # Otherwise, return the natively assigned senior official
+ default=F("senior_official__last_name"),
+ output_field=CharField(),
+ ),
+ "converted_senior_official_first_name": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__senior_official__first_name")),
+ # Otherwise, return the natively assigned senior official
+ default=F("senior_official__first_name"),
+ output_field=CharField(),
+ ),
+ "converted_senior_official_title": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__senior_official__title")),
+ # Otherwise, return the natively assigned senior official
+ default=F("senior_official__title"),
+ output_field=CharField(),
+ ),
+ "converted_so_name": Case(
+ # When portfolio is present, use that senior official instead
+ When(
+ Q(portfolio__isnull=False) & Q(portfolio__senior_official__isnull=False),
+ then=Concat(
+ Coalesce(F("portfolio__senior_official__first_name"), Value("")),
+ Value(" "),
+ Coalesce(F("portfolio__senior_official__last_name"), Value("")),
+ output_field=CharField(),
+ ),
+ ),
+ # Otherwise, return the natively assigned senior official
+ default=Concat(
+ Coalesce(F("senior_official__first_name"), Value("")),
+ Value(" "),
+ Coalesce(F("senior_official__last_name"), Value("")),
+ output_field=CharField(),
+ ),
+ output_field=CharField(),
+ ),
+ }
+
@classmethod
def update_queryset(cls, queryset, **kwargs):
"""
@@ -614,10 +726,10 @@ def parse_row(cls, columns, model):
if first_ready_on is None:
first_ready_on = "(blank)"
- # organization_type has generic_org_type AND is_election
- domain_org_type = model.get("organization_type")
+ # organization_type has organization_type AND is_election
+ domain_org_type = model.get("converted_generic_org_type")
human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type)
- domain_federal_type = model.get("federal_type")
+ domain_federal_type = model.get("converted_federal_type")
human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type)
domain_type = human_readable_domain_org_type
if domain_federal_type and domain_org_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
@@ -640,12 +752,12 @@ def parse_row(cls, columns, model):
"First ready on": first_ready_on,
"Expiration date": expiration_date,
"Domain type": domain_type,
- "Agency": model.get("federal_agency__agency"),
- "Organization name": model.get("organization_name"),
- "City": model.get("city"),
- "State": model.get("state_territory"),
- "SO": model.get("so_name"),
- "SO email": model.get("senior_official__email"),
+ "Agency": model.get("converted_federal_agency"),
+ "Organization name": model.get("converted_organization_name"),
+ "City": model.get("converted_city"),
+ "State": model.get("converted_state_territory"),
+ "SO": model.get("converted_so_name"),
+ "SO email": model.get("converted_so_email"),
"Security contact email": security_contact_email,
"Created at": model.get("domain__created_at"),
"Deleted": model.get("domain__deleted"),
@@ -654,8 +766,23 @@ def parse_row(cls, columns, model):
}
row = [FIELDS.get(column, "") for column in columns]
+
return row
+ def get_filtered_domain_infos_by_org(domain_infos_to_filter, org_to_filter_by):
+ """Returns a list of Domain Requests that has been filtered by the given organization value."""
+
+ annotated_queryset = domain_infos_to_filter.annotate(
+ converted_generic_org_type=Case(
+ # Recreate the logic of the converted_generic_org_type property
+ # here in annotations
+ When(portfolio__isnull=False, then=F("portfolio__organization_type")),
+ default=F("generic_org_type"),
+ output_field=CharField(),
+ )
+ )
+ return annotated_queryset.filter(converted_generic_org_type=org_to_filter_by)
+
@classmethod
def get_sliced_domains(cls, filter_condition):
"""Get filtered domains counts sliced by org type and election office.
@@ -663,23 +790,51 @@ def get_sliced_domains(cls, filter_condition):
when a domain has more that one manager.
"""
- domains = DomainInformation.objects.all().filter(**filter_condition).distinct()
- domains_count = domains.count()
- federal = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
- interstate = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).count()
+ domain_informations = DomainInformation.objects.all().filter(**filter_condition).distinct()
+ domains_count = domain_informations.count()
+ federal = (
+ cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.FEDERAL)
+ .distinct()
+ .count()
+ )
+ interstate = cls.get_filtered_domain_infos_by_org(
+ domain_informations, DomainRequest.OrganizationChoices.INTERSTATE
+ ).count()
state_or_territory = (
- domains.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count()
+ cls.get_filtered_domain_infos_by_org(
+ domain_informations, DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
+ )
+ .distinct()
+ .count()
+ )
+ tribal = (
+ cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.TRIBAL)
+ .distinct()
+ .count()
+ )
+ county = (
+ cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.COUNTY)
+ .distinct()
+ .count()
+ )
+ city = (
+ cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.CITY)
+ .distinct()
+ .count()
)
- tribal = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.TRIBAL).distinct().count()
- county = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.COUNTY).distinct().count()
- city = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.CITY).distinct().count()
special_district = (
- domains.filter(generic_org_type=DomainRequest.OrganizationChoices.SPECIAL_DISTRICT).distinct().count()
+ cls.get_filtered_domain_infos_by_org(
+ domain_informations, DomainRequest.OrganizationChoices.SPECIAL_DISTRICT
+ )
+ .distinct()
+ .count()
)
school_district = (
- domains.filter(generic_org_type=DomainRequest.OrganizationChoices.SCHOOL_DISTRICT).distinct().count()
+ cls.get_filtered_domain_infos_by_org(domain_informations, DomainRequest.OrganizationChoices.SCHOOL_DISTRICT)
+ .distinct()
+ .count()
)
- election_board = domains.filter(is_election_board=True).distinct().count()
+ election_board = domain_informations.filter(is_election_board=True).distinct().count()
return [
domains_count,
@@ -706,6 +861,7 @@ def get_columns(cls):
"""
Overrides the columns for CSV export specific to DomainExport.
"""
+
return [
"Domain name",
"Status",
@@ -723,6 +879,13 @@ def get_columns(cls):
"Invited domain managers",
]
+ @classmethod
+ def get_annotations_for_sort(cls):
+ """
+ Get a dict of annotations to make available for sorting.
+ """
+ return cls.get_computed_fields()
+
@classmethod
def get_sort_fields(cls):
"""
@@ -730,9 +893,9 @@ def get_sort_fields(cls):
"""
# Coalesce is used to replace federal_type of None with ZZZZZ
return [
- "organization_type",
- Coalesce("federal_type", Value("ZZZZZ")),
- "federal_agency",
+ "converted_generic_org_type",
+ Coalesce("converted_federal_type", Value("ZZZZZ")),
+ "converted_federal_agency",
"domain__name",
]
@@ -773,20 +936,6 @@ def get_prefetch_related(cls):
"""
return ["domain__permissions"]
- @classmethod
- def get_computed_fields(cls, delimiter=", ", **kwargs):
- """
- Get a dict of computed fields.
- """
- return {
- "so_name": Concat(
- Coalesce(F("senior_official__first_name"), Value("")),
- Value(" "),
- Coalesce(F("senior_official__last_name"), Value("")),
- output_field=CharField(),
- ),
- }
-
@classmethod
def get_related_table_fields(cls):
"""
@@ -892,7 +1041,7 @@ def exporting_dr_data_to_csv(cls, response, request=None):
cls.safe_get(getattr(request, "region_field", None)),
request.status,
cls.safe_get(getattr(request, "election_office", None)),
- request.federal_type,
+ request.converted_federal_type,
cls.safe_get(getattr(request, "domain_type", None)),
cls.safe_get(getattr(request, "additional_details", None)),
cls.safe_get(getattr(request, "creator_approved_domains_count", None)),
@@ -943,6 +1092,13 @@ def get_columns(cls):
"Security contact email",
]
+ @classmethod
+ def get_annotations_for_sort(cls, delimiter=", "):
+ """
+ Get a dict of annotations to make available for sorting.
+ """
+ return cls.get_computed_fields()
+
@classmethod
def get_sort_fields(cls):
"""
@@ -950,9 +1106,9 @@ def get_sort_fields(cls):
"""
# Coalesce is used to replace federal_type of None with ZZZZZ
return [
- "organization_type",
- Coalesce("federal_type", Value("ZZZZZ")),
- "federal_agency",
+ "converted_generic_org_type",
+ Coalesce("converted_federal_type", Value("ZZZZZ")),
+ "converted_federal_agency",
"domain__name",
]
@@ -990,20 +1146,6 @@ def get_filter_conditions(cls, **kwargs):
],
)
- @classmethod
- def get_computed_fields(cls, delimiter=", ", **kwargs):
- """
- Get a dict of computed fields.
- """
- return {
- "so_name": Concat(
- Coalesce(F("senior_official__first_name"), Value("")),
- Value(" "),
- Coalesce(F("senior_official__last_name"), Value("")),
- output_field=CharField(),
- ),
- }
-
@classmethod
def get_related_table_fields(cls):
"""
@@ -1037,6 +1179,13 @@ def get_columns(cls):
"Security contact email",
]
+ @classmethod
+ def get_annotations_for_sort(cls, delimiter=", "):
+ """
+ Get a dict of annotations to make available for sorting.
+ """
+ return cls.get_computed_fields()
+
@classmethod
def get_sort_fields(cls):
"""
@@ -1044,9 +1193,9 @@ def get_sort_fields(cls):
"""
# Coalesce is used to replace federal_type of None with ZZZZZ
return [
- "organization_type",
- Coalesce("federal_type", Value("ZZZZZ")),
- "federal_agency",
+ "converted_generic_org_type",
+ Coalesce("converted_federal_type", Value("ZZZZZ")),
+ "converted_federal_agency",
"domain__name",
]
@@ -1085,20 +1234,6 @@ def get_filter_conditions(cls, **kwargs):
],
)
- @classmethod
- def get_computed_fields(cls, delimiter=", ", **kwargs):
- """
- Get a dict of computed fields.
- """
- return {
- "so_name": Concat(
- Coalesce(F("senior_official__first_name"), Value("")),
- Value(" "),
- Coalesce(F("senior_official__last_name"), Value("")),
- output_field=CharField(),
- ),
- }
-
@classmethod
def get_related_table_fields(cls):
"""
@@ -1476,24 +1611,180 @@ def model(cls):
# Return the model class that this export handles
return DomainRequest
+ def get_filtered_domain_requests_by_org(domain_requests_to_filter, org_to_filter_by):
+ """Returns a list of Domain Requests that has been filtered by the given organization value"""
+ annotated_queryset = domain_requests_to_filter.annotate(
+ converted_generic_org_type=Case(
+ # Recreate the logic of the converted_generic_org_type property
+ # here in annotations
+ When(portfolio__isnull=False, then=F("portfolio__organization_type")),
+ default=F("generic_org_type"),
+ output_field=CharField(),
+ )
+ )
+ return annotated_queryset.filter(converted_generic_org_type=org_to_filter_by)
+
+ # return domain_requests_to_filter.filter(
+ # # Filter based on the generic org value returned by converted_generic_org_type
+ # id__in=[
+ # domainRequest.id
+ # for domainRequest in domain_requests_to_filter
+ # if domainRequest.converted_generic_org_type
+ # and domainRequest.converted_generic_org_type == org_to_filter_by
+ # ]
+ # )
+
+ @classmethod
+ def get_computed_fields(cls, delimiter=", ", **kwargs):
+ """
+ Get a dict of computed fields.
+ """
+ # NOTE: These computed fields imitate @Property functions in the Domain model and Portfolio model where needed.
+ # This is for performance purposes. Since we are working with dictionary values and not
+ # model objects as we export data, trying to reinstate model objects in order to grab @property
+ # values negatively impacts performance. Therefore, we will follow best practice and use annotations
+ return {
+ "converted_generic_org_type": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__organization_type")),
+ # Otherwise, return the natively assigned value
+ default=F("generic_org_type"),
+ output_field=CharField(),
+ ),
+ "converted_federal_agency": Case(
+ # When portfolio is present, use its value instead
+ When(
+ Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
+ then=F("portfolio__federal_agency__agency"),
+ ),
+ # Otherwise, return the natively assigned value
+ default=F("federal_agency__agency"),
+ output_field=CharField(),
+ ),
+ "converted_federal_type": Case(
+ # When portfolio is present, use its value instead
+ # NOTE: this is an @Property funciton in portfolio.
+ When(
+ Q(portfolio__isnull=False) & Q(portfolio__federal_agency__isnull=False),
+ then=F("portfolio__federal_agency__federal_type"),
+ ),
+ # Otherwise, return the natively assigned value
+ default=F("federal_type"),
+ output_field=CharField(),
+ ),
+ "converted_organization_name": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__organization_name")),
+ # Otherwise, return the natively assigned value
+ default=F("organization_name"),
+ output_field=CharField(),
+ ),
+ "converted_city": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__city")),
+ # Otherwise, return the natively assigned value
+ default=F("city"),
+ output_field=CharField(),
+ ),
+ "converted_state_territory": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__state_territory")),
+ # Otherwise, return the natively assigned value
+ default=F("state_territory"),
+ output_field=CharField(),
+ ),
+ "converted_so_email": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__senior_official__email")),
+ # Otherwise, return the natively assigned senior official
+ default=F("senior_official__email"),
+ output_field=CharField(),
+ ),
+ "converted_senior_official_last_name": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__senior_official__last_name")),
+ # Otherwise, return the natively assigned senior official
+ default=F("senior_official__last_name"),
+ output_field=CharField(),
+ ),
+ "converted_senior_official_first_name": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__senior_official__first_name")),
+ # Otherwise, return the natively assigned senior official
+ default=F("senior_official__first_name"),
+ output_field=CharField(),
+ ),
+ "converted_senior_official_title": Case(
+ # When portfolio is present, use its value instead
+ When(portfolio__isnull=False, then=F("portfolio__senior_official__title")),
+ # Otherwise, return the natively assigned senior official
+ default=F("senior_official__title"),
+ output_field=CharField(),
+ ),
+ "converted_so_name": Case(
+ # When portfolio is present, use that senior official instead
+ When(
+ Q(portfolio__isnull=False) & Q(portfolio__senior_official__isnull=False),
+ then=Concat(
+ Coalesce(F("portfolio__senior_official__first_name"), Value("")),
+ Value(" "),
+ Coalesce(F("portfolio__senior_official__last_name"), Value("")),
+ output_field=CharField(),
+ ),
+ ),
+ # Otherwise, return the natively assigned senior official
+ default=Concat(
+ Coalesce(F("senior_official__first_name"), Value("")),
+ Value(" "),
+ Coalesce(F("senior_official__last_name"), Value("")),
+ output_field=CharField(),
+ ),
+ output_field=CharField(),
+ ),
+ }
+
@classmethod
def get_sliced_requests(cls, filter_condition):
"""Get filtered requests counts sliced by org type and election office."""
requests = DomainRequest.objects.all().filter(**filter_condition).distinct()
requests_count = requests.count()
- federal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
- interstate = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).distinct().count()
+ federal = (
+ cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.FEDERAL)
+ .distinct()
+ .count()
+ )
+ interstate = (
+ cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.INTERSTATE)
+ .distinct()
+ .count()
+ )
state_or_territory = (
- requests.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count()
+ cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.STATE_OR_TERRITORY)
+ .distinct()
+ .count()
+ )
+ tribal = (
+ cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.TRIBAL)
+ .distinct()
+ .count()
+ )
+ county = (
+ cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.COUNTY)
+ .distinct()
+ .count()
+ )
+ city = (
+ cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.CITY).distinct().count()
)
- tribal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.TRIBAL).distinct().count()
- county = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.COUNTY).distinct().count()
- city = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.CITY).distinct().count()
special_district = (
- requests.filter(generic_org_type=DomainRequest.OrganizationChoices.SPECIAL_DISTRICT).distinct().count()
+ cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.SPECIAL_DISTRICT)
+ .distinct()
+ .count()
)
school_district = (
- requests.filter(generic_org_type=DomainRequest.OrganizationChoices.SCHOOL_DISTRICT).distinct().count()
+ cls.get_filtered_domain_requests_by_org(requests, DomainRequest.OrganizationChoices.SCHOOL_DISTRICT)
+ .distinct()
+ .count()
)
election_board = requests.filter(is_election_board=True).distinct().count()
@@ -1517,11 +1808,11 @@ def parse_row(cls, columns, model):
"""
# Handle the federal_type field. Defaults to the wrong format.
- federal_type = model.get("federal_type")
+ federal_type = model.get("converted_federal_type")
human_readable_federal_type = BranchChoices.get_branch_label(federal_type) if federal_type else None
# Handle the org_type field
- org_type = model.get("generic_org_type") or model.get("organization_type")
+ org_type = model.get("converted_generic_org_type")
human_readable_org_type = DomainRequest.OrganizationChoices.get_org_label(org_type) if org_type else None
# Handle the status field. Defaults to the wrong format.
@@ -1569,19 +1860,19 @@ def parse_row(cls, columns, model):
"Other contacts": model.get("all_other_contacts"),
"Current websites": model.get("all_current_websites"),
# Untouched FK fields - passed into the request dict.
- "Federal agency": model.get("federal_agency__agency"),
- "SO first name": model.get("senior_official__first_name"),
- "SO last name": model.get("senior_official__last_name"),
- "SO email": model.get("senior_official__email"),
- "SO title/role": model.get("senior_official__title"),
+ "Federal agency": model.get("converted_federal_agency"),
+ "SO first name": model.get("converted_senior_official_first_name"),
+ "SO last name": model.get("converted_senior_official_last_name"),
+ "SO email": model.get("converted_so_email"),
+ "SO title/role": model.get("converted_senior_official_title"),
"Creator first name": model.get("creator__first_name"),
"Creator last name": model.get("creator__last_name"),
"Creator email": model.get("creator__email"),
"Investigator": model.get("investigator__email"),
# Untouched fields
- "Organization name": model.get("organization_name"),
- "City": model.get("city"),
- "State/territory": model.get("state_territory"),
+ "Organization name": model.get("converted_organization_name"),
+ "City": model.get("converted_city"),
+ "State/territory": model.get("converted_state_territory"),
"Request purpose": model.get("purpose"),
"CISA regional representative": model.get("cisa_representative_email"),
"Last submitted date": model.get("last_submitted_date"),
@@ -1724,24 +2015,34 @@ def get_computed_fields(cls, delimiter=", ", **kwargs):
"""
Get a dict of computed fields.
"""
- return {
- "creator_approved_domains_count": cls.get_creator_approved_domains_count_query(),
- "creator_active_requests_count": cls.get_creator_active_requests_count_query(),
- "all_current_websites": StringAgg("current_websites__website", delimiter=delimiter, distinct=True),
- "all_alternative_domains": StringAgg("alternative_domains__website", delimiter=delimiter, distinct=True),
- # Coerce the other contacts object to "{first_name} {last_name} {email}"
- "all_other_contacts": StringAgg(
- Concat(
- "other_contacts__first_name",
- Value(" "),
- "other_contacts__last_name",
- Value(" "),
- "other_contacts__email",
+ # Get computed fields from the parent class
+ computed_fields = super().get_computed_fields()
+
+ # Add additional computed fields
+ computed_fields.update(
+ {
+ "creator_approved_domains_count": cls.get_creator_approved_domains_count_query(),
+ "creator_active_requests_count": cls.get_creator_active_requests_count_query(),
+ "all_current_websites": StringAgg("current_websites__website", delimiter=delimiter, distinct=True),
+ "all_alternative_domains": StringAgg(
+ "alternative_domains__website", delimiter=delimiter, distinct=True
),
- delimiter=delimiter,
- distinct=True,
- ),
- }
+ # Coerce the other contacts object to "{first_name} {last_name} {email}"
+ "all_other_contacts": StringAgg(
+ Concat(
+ "other_contacts__first_name",
+ Value(" "),
+ "other_contacts__last_name",
+ Value(" "),
+ "other_contacts__email",
+ ),
+ delimiter=delimiter,
+ distinct=True,
+ ),
+ }
+ )
+
+ return computed_fields
@classmethod
def get_related_table_fields(cls):
From 8bfeedf6ef43eb1d8df216834a9f0da9804554d1 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 14:09:09 -0700
Subject: [PATCH 34/46] remove converted fields
---
src/registrar/utility/csv_export.py | 122 ++++++++++++++++++++++++++++
1 file changed, 122 insertions(+)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 97feae20c..310bfd8c3 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -1077,6 +1077,67 @@ class DomainDataFull(DomainExport):
Inherits from BaseExport -> DomainExport
"""
+ # NOTE - this override is temporary. Delete this after we consolidate these @property fields.
+ @classmethod
+ def parse_row(cls, columns, model):
+ """
+ Given a set of columns and a model dictionary, generate a new row from cleaned column data.
+ """
+
+ status = model.get("domain__state")
+ human_readable_status = Domain.State.get_state_label(status)
+
+ expiration_date = model.get("domain__expiration_date")
+ if expiration_date is None:
+ expiration_date = "(blank)"
+
+ first_ready_on = model.get("domain__first_ready")
+ if first_ready_on is None:
+ first_ready_on = "(blank)"
+
+ # organization_type has organization_type AND is_election
+ domain_org_type = model.get("converted_generic_org_type")
+ human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type)
+ domain_federal_type = model.get("converted_federal_type")
+ human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type)
+ domain_type = human_readable_domain_org_type
+ if domain_federal_type and domain_org_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
+ domain_type = f"{human_readable_domain_org_type} - {human_readable_domain_federal_type}"
+
+ security_contact_email = model.get("security_contact_email")
+ invalid_emails = {DefaultEmail.LEGACY_DEFAULT.value, DefaultEmail.PUBLIC_CONTACT_DEFAULT.value}
+ if (
+ not security_contact_email
+ or not isinstance(security_contact_email, str)
+ or security_contact_email.lower().strip() in invalid_emails
+ ):
+ security_contact_email = "(blank)"
+
+ # create a dictionary of fields which can be included in output.
+ # "extra_fields" are precomputed fields (generated in the DB or parsed).
+ FIELDS = {
+ "Domain name": model.get("domain__name"),
+ "Status": human_readable_status,
+ "First ready on": first_ready_on,
+ "Expiration date": expiration_date,
+ "Domain type": domain_type,
+ "Agency": model.get("federal_agency"),
+ "Organization name": model.get("organization_name"),
+ "City": model.get("city"),
+ "State": model.get("state_territory"),
+ "SO": model.get("so_name"),
+ "SO email": model.get("so_email"),
+ "Security contact email": security_contact_email,
+ "Created at": model.get("domain__created_at"),
+ "Deleted": model.get("domain__deleted"),
+ "Domain managers": model.get("managers"),
+ "Invited domain managers": model.get("invited_users"),
+ }
+
+ row = [FIELDS.get(column, "") for column in columns]
+
+ return row
+
@classmethod
def get_columns(cls):
"""
@@ -1164,6 +1225,67 @@ class DomainDataFederal(DomainExport):
Inherits from BaseExport -> DomainExport
"""
+ # NOTE - this override is temporary. Delete this after we consolidate these @property fields.
+ @classmethod
+ def parse_row(cls, columns, model):
+ """
+ Given a set of columns and a model dictionary, generate a new row from cleaned column data.
+ """
+
+ status = model.get("domain__state")
+ human_readable_status = Domain.State.get_state_label(status)
+
+ expiration_date = model.get("domain__expiration_date")
+ if expiration_date is None:
+ expiration_date = "(blank)"
+
+ first_ready_on = model.get("domain__first_ready")
+ if first_ready_on is None:
+ first_ready_on = "(blank)"
+
+ # organization_type has organization_type AND is_election
+ domain_org_type = model.get("converted_generic_org_type")
+ human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type)
+ domain_federal_type = model.get("converted_federal_type")
+ human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type)
+ domain_type = human_readable_domain_org_type
+ if domain_federal_type and domain_org_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
+ domain_type = f"{human_readable_domain_org_type} - {human_readable_domain_federal_type}"
+
+ security_contact_email = model.get("security_contact_email")
+ invalid_emails = {DefaultEmail.LEGACY_DEFAULT.value, DefaultEmail.PUBLIC_CONTACT_DEFAULT.value}
+ if (
+ not security_contact_email
+ or not isinstance(security_contact_email, str)
+ or security_contact_email.lower().strip() in invalid_emails
+ ):
+ security_contact_email = "(blank)"
+
+ # create a dictionary of fields which can be included in output.
+ # "extra_fields" are precomputed fields (generated in the DB or parsed).
+ FIELDS = {
+ "Domain name": model.get("domain__name"),
+ "Status": human_readable_status,
+ "First ready on": first_ready_on,
+ "Expiration date": expiration_date,
+ "Domain type": domain_type,
+ "Agency": model.get("federal_agency"),
+ "Organization name": model.get("organization_name"),
+ "City": model.get("city"),
+ "State": model.get("state_territory"),
+ "SO": model.get("so_name"),
+ "SO email": model.get("so_email"),
+ "Security contact email": security_contact_email,
+ "Created at": model.get("domain__created_at"),
+ "Deleted": model.get("domain__deleted"),
+ "Domain managers": model.get("managers"),
+ "Invited domain managers": model.get("invited_users"),
+ }
+
+ row = [FIELDS.get(column, "") for column in columns]
+
+ return row
+
@classmethod
def get_columns(cls):
"""
From 0cd504eb781bf47160f4188b77e7e6196246431d Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 14:16:17 -0700
Subject: [PATCH 35/46] Update csv_export.py
---
src/registrar/utility/csv_export.py | 133 +++++++---------------------
1 file changed, 34 insertions(+), 99 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 310bfd8c3..07014f185 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -744,30 +744,41 @@ def parse_row(cls, columns, model):
):
security_contact_email = "(blank)"
+ model["status"] = human_readable_status
+ model["first_ready_on"] = first_ready_on
+ model["expiration_date"] = expiration_date
+ model["domain_type"] = domain_type
+ model["security_contact_email"] = security_contact_email
# create a dictionary of fields which can be included in output.
# "extra_fields" are precomputed fields (generated in the DB or parsed).
+ FIELDS = cls.get_fields(model)
+
+ row = [FIELDS.get(column, "") for column in columns]
+
+ return row
+
+ # NOTE - this override is temporary. Delete this after we consolidate these @property fields.
+ @classmethod
+ def get_fields(cls, model):
FIELDS = {
"Domain name": model.get("domain__name"),
- "Status": human_readable_status,
- "First ready on": first_ready_on,
- "Expiration date": expiration_date,
- "Domain type": domain_type,
+ "Status": model.get("status"),
+ "First ready on": model.get("first_ready_on"),
+ "Expiration date": model.get("expiration_date"),
+ "Domain type": model.get("domain_type"),
"Agency": model.get("converted_federal_agency"),
"Organization name": model.get("converted_organization_name"),
"City": model.get("converted_city"),
"State": model.get("converted_state_territory"),
"SO": model.get("converted_so_name"),
"SO email": model.get("converted_so_email"),
- "Security contact email": security_contact_email,
+ "Security contact email": model.get("security_contact_email"),
"Created at": model.get("domain__created_at"),
"Deleted": model.get("domain__deleted"),
"Domain managers": model.get("managers"),
"Invited domain managers": model.get("invited_users"),
}
-
- row = [FIELDS.get(column, "") for column in columns]
-
- return row
+ return FIELDS
def get_filtered_domain_infos_by_org(domain_infos_to_filter, org_to_filter_by):
"""Returns a list of Domain Requests that has been filtered by the given organization value."""
@@ -1079,64 +1090,26 @@ class DomainDataFull(DomainExport):
# NOTE - this override is temporary. Delete this after we consolidate these @property fields.
@classmethod
- def parse_row(cls, columns, model):
- """
- Given a set of columns and a model dictionary, generate a new row from cleaned column data.
- """
-
- status = model.get("domain__state")
- human_readable_status = Domain.State.get_state_label(status)
-
- expiration_date = model.get("domain__expiration_date")
- if expiration_date is None:
- expiration_date = "(blank)"
-
- first_ready_on = model.get("domain__first_ready")
- if first_ready_on is None:
- first_ready_on = "(blank)"
-
- # organization_type has organization_type AND is_election
- domain_org_type = model.get("converted_generic_org_type")
- human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type)
- domain_federal_type = model.get("converted_federal_type")
- human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type)
- domain_type = human_readable_domain_org_type
- if domain_federal_type and domain_org_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
- domain_type = f"{human_readable_domain_org_type} - {human_readable_domain_federal_type}"
-
- security_contact_email = model.get("security_contact_email")
- invalid_emails = {DefaultEmail.LEGACY_DEFAULT.value, DefaultEmail.PUBLIC_CONTACT_DEFAULT.value}
- if (
- not security_contact_email
- or not isinstance(security_contact_email, str)
- or security_contact_email.lower().strip() in invalid_emails
- ):
- security_contact_email = "(blank)"
-
- # create a dictionary of fields which can be included in output.
- # "extra_fields" are precomputed fields (generated in the DB or parsed).
+ def get_fields(cls, model):
FIELDS = {
"Domain name": model.get("domain__name"),
- "Status": human_readable_status,
- "First ready on": first_ready_on,
- "Expiration date": expiration_date,
- "Domain type": domain_type,
+ "Status": model.get("status"),
+ "First ready on": model.get("first_ready_on"),
+ "Expiration date": model.get("expiration_date"),
+ "Domain type": model.get("domain_type"),
"Agency": model.get("federal_agency"),
"Organization name": model.get("organization_name"),
"City": model.get("city"),
"State": model.get("state_territory"),
"SO": model.get("so_name"),
"SO email": model.get("so_email"),
- "Security contact email": security_contact_email,
+ "Security contact email": model.get("security_contact_email"),
"Created at": model.get("domain__created_at"),
"Deleted": model.get("domain__deleted"),
"Domain managers": model.get("managers"),
"Invited domain managers": model.get("invited_users"),
}
-
- row = [FIELDS.get(column, "") for column in columns]
-
- return row
+ return FIELDS
@classmethod
def get_columns(cls):
@@ -1227,64 +1200,26 @@ class DomainDataFederal(DomainExport):
# NOTE - this override is temporary. Delete this after we consolidate these @property fields.
@classmethod
- def parse_row(cls, columns, model):
- """
- Given a set of columns and a model dictionary, generate a new row from cleaned column data.
- """
-
- status = model.get("domain__state")
- human_readable_status = Domain.State.get_state_label(status)
-
- expiration_date = model.get("domain__expiration_date")
- if expiration_date is None:
- expiration_date = "(blank)"
-
- first_ready_on = model.get("domain__first_ready")
- if first_ready_on is None:
- first_ready_on = "(blank)"
-
- # organization_type has organization_type AND is_election
- domain_org_type = model.get("converted_generic_org_type")
- human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type)
- domain_federal_type = model.get("converted_federal_type")
- human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type)
- domain_type = human_readable_domain_org_type
- if domain_federal_type and domain_org_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
- domain_type = f"{human_readable_domain_org_type} - {human_readable_domain_federal_type}"
-
- security_contact_email = model.get("security_contact_email")
- invalid_emails = {DefaultEmail.LEGACY_DEFAULT.value, DefaultEmail.PUBLIC_CONTACT_DEFAULT.value}
- if (
- not security_contact_email
- or not isinstance(security_contact_email, str)
- or security_contact_email.lower().strip() in invalid_emails
- ):
- security_contact_email = "(blank)"
-
- # create a dictionary of fields which can be included in output.
- # "extra_fields" are precomputed fields (generated in the DB or parsed).
+ def get_fields(cls, model):
FIELDS = {
"Domain name": model.get("domain__name"),
- "Status": human_readable_status,
- "First ready on": first_ready_on,
- "Expiration date": expiration_date,
- "Domain type": domain_type,
+ "Status": model.get("status"),
+ "First ready on": model.get("first_ready_on"),
+ "Expiration date": model.get("expiration_date"),
+ "Domain type": model.get("domain_type"),
"Agency": model.get("federal_agency"),
"Organization name": model.get("organization_name"),
"City": model.get("city"),
"State": model.get("state_territory"),
"SO": model.get("so_name"),
"SO email": model.get("so_email"),
- "Security contact email": security_contact_email,
+ "Security contact email": model.get("security_contact_email"),
"Created at": model.get("domain__created_at"),
"Deleted": model.get("domain__deleted"),
"Domain managers": model.get("managers"),
"Invited domain managers": model.get("invited_users"),
}
-
- row = [FIELDS.get(column, "") for column in columns]
-
- return row
+ return FIELDS
@classmethod
def get_columns(cls):
From 26fd19ffe80e15c7381ed52802be42da02075880 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 14:21:06 -0700
Subject: [PATCH 36/46] Update test_reports.py
---
src/registrar/tests/test_reports.py | 138 +++++++++++++++-------------
1 file changed, 72 insertions(+), 66 deletions(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index cafaff7b1..f91c5b299 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -71,8 +71,8 @@ def test_generate_federal_report(self):
fake_open = mock_open()
expected_file_content = [
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
- call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
+ call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
]
@@ -93,8 +93,8 @@ def test_generate_full_report(self):
fake_open = mock_open()
expected_file_content = [
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
- call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
+ call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("zdomain12.gov,Interstate,,,,,(blank)\r\n"),
@@ -251,35 +251,32 @@ def test_domain_data_type(self):
# We expect READY domains,
# sorted alphabetially by domain name
expected_content = (
- "Domain name,Status,First ready on,Expiration date,Domain type,Agency,"
- "Organization name,City,State,SO,SO email,"
- "Security contact email,Domain managers,Invited domain managers\n"
- "adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,"
- "Portfolio 1 Federal Agency,,,, ,,(blank),"
- "meoward@rocks.com,squeaker@rocks.com\n"
- "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,"
- "Portfolio 1 Federal Agency,,,, ,,(blank),"
- '"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
- "cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,"
- "World War I Centennial Commission,,,, ,,(blank),"
+ "Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,City,State,SO,"
+ "SO email,Security contact email,Domain managers,Invited domain managers\n"
+ "cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,World War I Centennial Commission,,,,(blank),,,"
"meoward@rocks.com,\n"
- "adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,"
+ "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,"
+ ',,,(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
+ "woofwardthethird@rocks.com\n"
+ "adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,"
"squeaker@rocks.com\n"
- "bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
- "bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
- "bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
- "ddomain3.gov,On hold,(blank),2023-11-15,Federal,"
- "Armed Forces Retirement Home,,,, ,,security@mail.gov,,\n"
- "sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
- "xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
- "zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
- "zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,, ,,(blank),meoward@rocks.com,\n"
+ "bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
+ "bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
+ "bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
+ "ddomain3.gov,On hold,(blank),2023-11-15,Federal,Armed Forces Retirement Home,,,,,,"
+ "security@mail.gov,,\n"
+ "sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
+ "xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
+ "zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
+ "adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,,(blank),,,"
+ "meoward@rocks.com,squeaker@rocks.com\n"
+ "zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,,(blank),,,meoward@rocks.com,\n"
)
-
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
+ self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -315,17 +312,20 @@ def test_domain_data_type_user(self):
# We expect only domains associated with the user
expected_content = (
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,"
- "City,State,SO,SO email,Security contact email,Domain managers,Invited domain managers\n"
- "adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
+ "City,State,SO,SO email,"
+ "Security contact email,Domain managers,Invited domain managers\n"
+ "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,, ,,"
+ '(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
+ "woofwardthethird@rocks.com\n"
+ "adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,, ,,(blank),"
'"info@example.com, meoward@rocks.com",squeaker@rocks.com\n'
- "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
- '"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
+ self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -493,17 +493,17 @@ def test_domain_data_full(self):
# sorted alphabetially by domain name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
- "defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
- "cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
- "adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
- "ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
+ "cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
+ "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
+ "adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
+ "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
"zdomain12.gov,Interstate,,,,,(blank)\n"
)
-
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
+ self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -533,16 +533,16 @@ def test_domain_data_federal(self):
# sorted alphabetially by domain name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
- "defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
- "cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
- "adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
- "ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
+ "cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
+ "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
+ "adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
+ "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
)
-
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
+ self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -587,13 +587,13 @@ def test_domain_growth(self):
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,"
"State,Status,Expiration date, Deleted\n"
- "cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Ready,(blank)\n"
- "adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
- "cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n"
- "zdomain12.gov,Interstate,Ready,(blank)\n"
+ "cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,(blank)\n"
+ "adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n"
+ "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n"
+ "zdomain12.govInterstateReady(blank)\n"
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n"
- "sdomain8.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
- "xdomain7.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
+ "sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank),2024-04-02\n"
+ "xdomain7.gov,FederalArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
@@ -611,6 +611,7 @@ def test_domain_managed(self):
squeaker@rocks.com is invited to domain2 (DNS_NEEDED) and domain10 (No managers).
She should show twice in this report but not in test_DomainManaged."""
+ self.maxDiff = None
# Create a CSV file in memory
csv_file = StringIO()
# Call the export functions
@@ -645,6 +646,7 @@ def test_domain_managed(self):
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
+ self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -681,6 +683,7 @@ def test_domain_unmanaged(self):
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
+
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -718,9 +721,10 @@ def test_domain_request_growth(self):
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
+
self.assertEqual(csv_content, expected_content)
- # @less_console_noise_decorator
+ @less_console_noise_decorator
def test_domain_request_data_full(self):
"""Tests the full domain request report."""
# Remove "Submitted at" because we can't guess this immutable, dynamically generated test data
@@ -762,34 +766,35 @@ def test_domain_request_data_full(self):
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()
-
expected_content = (
# Header
- "Domain request,Status,Domain type,Federal type,Federal agency,Organization name,Election office,"
- "City,State/territory,Region,Creator first name,Creator last name,Creator email,"
- "Creator approved domains count,Creator active requests count,Alternative domains,SO first name,"
- "SO last name,SO email,SO title/role,Request purpose,Request additional details,Other contacts,"
+ "Domain request,Status,Domain type,Federal type,"
+ "Federal agency,Organization name,Election office,City,State/territory,"
+ "Region,Creator first name,Creator last name,Creator email,Creator approved domains count,"
+ "Creator active requests count,Alternative domains,SO first name,SO last name,SO email,"
+ "SO title/role,Request purpose,Request additional details,Other contacts,"
"CISA regional representative,Current websites,Investigator\n"
# Content
- "city5.gov,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
+ "city5.gov,,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
- "city2.gov,In review,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
- "Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
- "city3.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,"
- '"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | '
- 'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
- 'Testy Tester testy2@town.com",'
- 'test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
- "city4.gov,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,"
- "Tester,testy@town.com,"
- "Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
- "Testy Tester testy2@town.com,"
- "cisaRep@igorville.gov,city.com,\n"
- "city6.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
- "Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester testy2@town.com,"
+ "city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
+ "testy@town.com,"
+ "Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
+ 'city3.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,"cheeseville.gov, city1.gov,'
+ 'igorville.gov",Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,CISA-first-name '
+ "CISA-last-name "
+ '| There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, Testy Tester '
+ 'testy2@town.com"'
+ ',test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
+ "city4.gov,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
+ "Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
+ "testy2@town.com"
+ ",cisaRep@igorville.gov,city.com,\n"
+ "city6.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
+ "Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
+ "testy2@town.com,"
"cisaRep@igorville.gov,city.com,\n"
)
-
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
@@ -857,6 +862,7 @@ def test_member_export(self):
# Create a request and add the user to the request
request = self.factory.get("/")
request.user = self.user
+ self.maxDiff = None
# Add portfolio to session
request = GenericTestHelper._mock_user_request_for_factory(request)
request.session["portfolio"] = self.portfolio_1
From 431b0e80cf316558c3bd1eea72aba142ae379f5b Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 14:21:52 -0700
Subject: [PATCH 37/46] Revert "Update test_reports.py"
This reverts commit 26fd19ffe80e15c7381ed52802be42da02075880.
---
src/registrar/tests/test_reports.py | 138 +++++++++++++---------------
1 file changed, 66 insertions(+), 72 deletions(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index f91c5b299..cafaff7b1 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -71,8 +71,8 @@ def test_generate_federal_report(self):
fake_open = mock_open()
expected_file_content = [
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
+ call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
- call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
]
@@ -93,8 +93,8 @@ def test_generate_full_report(self):
fake_open = mock_open()
expected_file_content = [
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
+ call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
- call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("zdomain12.gov,Interstate,,,,,(blank)\r\n"),
@@ -251,32 +251,35 @@ def test_domain_data_type(self):
# We expect READY domains,
# sorted alphabetially by domain name
expected_content = (
- "Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,City,State,SO,"
- "SO email,Security contact email,Domain managers,Invited domain managers\n"
- "cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,World War I Centennial Commission,,,,(blank),,,"
+ "Domain name,Status,First ready on,Expiration date,Domain type,Agency,"
+ "Organization name,City,State,SO,SO email,"
+ "Security contact email,Domain managers,Invited domain managers\n"
+ "adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,"
+ "Portfolio 1 Federal Agency,,,, ,,(blank),"
+ "meoward@rocks.com,squeaker@rocks.com\n"
+ "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,"
+ "Portfolio 1 Federal Agency,,,, ,,(blank),"
+ '"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
+ "cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,"
+ "World War I Centennial Commission,,,, ,,(blank),"
"meoward@rocks.com,\n"
- "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,"
- ',,,(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
- "woofwardthethird@rocks.com\n"
- "adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,"
+ "adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,"
"squeaker@rocks.com\n"
- "bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
- "bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
- "bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
- "ddomain3.gov,On hold,(blank),2023-11-15,Federal,Armed Forces Retirement Home,,,,,,"
- "security@mail.gov,,\n"
- "sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
- "xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
- "zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
- "adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,,(blank),,,"
- "meoward@rocks.com,squeaker@rocks.com\n"
- "zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,,(blank),,,meoward@rocks.com,\n"
+ "bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
+ "bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
+ "bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
+ "ddomain3.gov,On hold,(blank),2023-11-15,Federal,"
+ "Armed Forces Retirement Home,,,, ,,security@mail.gov,,\n"
+ "sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
+ "xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
+ "zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,(blank),,\n"
+ "zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,, ,,(blank),meoward@rocks.com,\n"
)
+
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
- self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -312,20 +315,17 @@ def test_domain_data_type_user(self):
# We expect only domains associated with the user
expected_content = (
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,"
- "City,State,SO,SO email,"
- "Security contact email,Domain managers,Invited domain managers\n"
- "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,, ,,"
- '(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
- "woofwardthethird@rocks.com\n"
- "adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,, ,,(blank),"
+ "City,State,SO,SO email,Security contact email,Domain managers,Invited domain managers\n"
+ "adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
'"info@example.com, meoward@rocks.com",squeaker@rocks.com\n'
+ "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
+ '"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
- self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -493,17 +493,17 @@ def test_domain_data_full(self):
# sorted alphabetially by domain name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
- "cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
- "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
- "adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
- "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
+ "defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
+ "cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
+ "adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
+ "ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
"zdomain12.gov,Interstate,,,,,(blank)\n"
)
+
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
- self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -533,16 +533,16 @@ def test_domain_data_federal(self):
# sorted alphabetially by domain name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
- "cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
- "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
- "adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
- "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
+ "defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
+ "cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
+ "adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
+ "ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
)
+
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
- self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -587,13 +587,13 @@ def test_domain_growth(self):
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,"
"State,Status,Expiration date, Deleted\n"
- "cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,(blank)\n"
- "adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n"
- "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n"
- "zdomain12.govInterstateReady(blank)\n"
+ "cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Ready,(blank)\n"
+ "adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
+ "cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n"
+ "zdomain12.gov,Interstate,Ready,(blank)\n"
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n"
- "sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank),2024-04-02\n"
- "xdomain7.gov,FederalArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
+ "sdomain8.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
+ "xdomain7.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
@@ -611,7 +611,6 @@ def test_domain_managed(self):
squeaker@rocks.com is invited to domain2 (DNS_NEEDED) and domain10 (No managers).
She should show twice in this report but not in test_DomainManaged."""
- self.maxDiff = None
# Create a CSV file in memory
csv_file = StringIO()
# Call the export functions
@@ -646,7 +645,6 @@ def test_domain_managed(self):
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
- self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -683,7 +681,6 @@ def test_domain_unmanaged(self):
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
-
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -721,10 +718,9 @@ def test_domain_request_growth(self):
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
-
self.assertEqual(csv_content, expected_content)
- @less_console_noise_decorator
+ # @less_console_noise_decorator
def test_domain_request_data_full(self):
"""Tests the full domain request report."""
# Remove "Submitted at" because we can't guess this immutable, dynamically generated test data
@@ -766,35 +762,34 @@ def test_domain_request_data_full(self):
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()
+
expected_content = (
# Header
- "Domain request,Status,Domain type,Federal type,"
- "Federal agency,Organization name,Election office,City,State/territory,"
- "Region,Creator first name,Creator last name,Creator email,Creator approved domains count,"
- "Creator active requests count,Alternative domains,SO first name,SO last name,SO email,"
- "SO title/role,Request purpose,Request additional details,Other contacts,"
+ "Domain request,Status,Domain type,Federal type,Federal agency,Organization name,Election office,"
+ "City,State/territory,Region,Creator first name,Creator last name,Creator email,"
+ "Creator approved domains count,Creator active requests count,Alternative domains,SO first name,"
+ "SO last name,SO email,SO title/role,Request purpose,Request additional details,Other contacts,"
"CISA regional representative,Current websites,Investigator\n"
# Content
- "city5.gov,,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
- "Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
- "city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
- "testy@town.com,"
+ "city5.gov,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
- 'city3.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,"cheeseville.gov, city1.gov,'
- 'igorville.gov",Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,CISA-first-name '
- "CISA-last-name "
- '| There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, Testy Tester '
- 'testy2@town.com"'
- ',test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
- "city4.gov,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
- "Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
- "testy2@town.com"
- ",cisaRep@igorville.gov,city.com,\n"
- "city6.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
- "Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
- "testy2@town.com,"
+ "city2.gov,In review,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
+ "Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
+ "city3.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,"
+ '"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | '
+ 'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
+ 'Testy Tester testy2@town.com",'
+ 'test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
+ "city4.gov,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,"
+ "Tester,testy@town.com,"
+ "Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
+ "Testy Tester testy2@town.com,"
+ "cisaRep@igorville.gov,city.com,\n"
+ "city6.gov,Submitted,Federal,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,0,1,city1.gov,,,,,"
+ "Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester testy2@town.com,"
"cisaRep@igorville.gov,city.com,\n"
)
+
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
@@ -862,7 +857,6 @@ def test_member_export(self):
# Create a request and add the user to the request
request = self.factory.get("/")
request.user = self.user
- self.maxDiff = None
# Add portfolio to session
request = GenericTestHelper._mock_user_request_for_factory(request)
request.session["portfolio"] = self.portfolio_1
From d2d787c8eaf8562c35a791f2f22825ccaaca15a7 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 14:24:59 -0700
Subject: [PATCH 38/46] Revert relevant tests back to normal
---
src/registrar/tests/test_reports.py | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index cafaff7b1..995782eea 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -71,8 +71,8 @@ def test_generate_federal_report(self):
fake_open = mock_open()
expected_file_content = [
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
- call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
+ call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
]
@@ -93,8 +93,8 @@ def test_generate_full_report(self):
fake_open = mock_open()
expected_file_content = [
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
- call("cdomain1.gov,Federal - Executive,Portfolio 1 Federal Agency,,,,(blank)\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
+ call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("zdomain12.gov,Interstate,,,,,(blank)\r\n"),
@@ -493,17 +493,17 @@ def test_domain_data_full(self):
# sorted alphabetially by domain name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
- "defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
- "cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
- "adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
- "ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
+ "cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
+ "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
+ "adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
+ "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
"zdomain12.gov,Interstate,,,,,(blank)\n"
)
-
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
+ self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@@ -533,16 +533,16 @@ def test_domain_data_federal(self):
# sorted alphabetially by domain name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
- "defaultsecurity.gov,Federal - Executive,Portfolio1FederalAgency,,,,(blank)\n"
- "cdomain11.gov,Federal - Executive,WorldWarICentennialCommission,,,,(blank)\n"
- "adomain10.gov,Federal,ArmedForcesRetirementHome,,,,(blank)\n"
- "ddomain3.gov,Federal,ArmedForcesRetirementHome,,,,security@mail.gov\n"
+ "cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
+ "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
+ "adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
+ "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
)
-
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
+ self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
From 297be33e645bbf454fe43084099db661912c3286 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 14:42:44 -0700
Subject: [PATCH 39/46] remove log
---
src/registrar/assets/src/js/getgov/portfolio-member-page.js | 1 -
src/registrar/forms/portfolio.py | 3 ++-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/assets/src/js/getgov/portfolio-member-page.js b/src/registrar/assets/src/js/getgov/portfolio-member-page.js
index 83fee661c..02d927438 100644
--- a/src/registrar/assets/src/js/getgov/portfolio-member-page.js
+++ b/src/registrar/assets/src/js/getgov/portfolio-member-page.js
@@ -175,7 +175,6 @@ export function initAddNewMemberPageListeners() {
// Initalize the radio for the member pages
export function initPortfolioMemberPageRadio() {
document.addEventListener("DOMContentLoaded", () => {
- console.log("new content 2")
let memberForm = document.getElementById("member_form");
let newMemberForm = document.getElementById("add_member_form")
if (memberForm) {
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index ce164607e..eaa885a85 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -433,7 +433,8 @@ def map_instance_to_initial(self, instance):
# Build form data based on role (which options are available).
# Get which one should be "selected" by assuming that EDIT takes precedence over view,
# and ADMIN takes precedence over MEMBER.
- selected_role = next((role for role in roles if role in instance.roles), None)
+ roles = instance.roles or []
+ selected_role = next((role for role in roles if role in roles), None)
form_data = {"role": selected_role}
is_admin = selected_role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN
if is_admin:
From 8b72c654b8a3fb5c234b03e3a7bec334664dc8f1 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 14:49:28 -0700
Subject: [PATCH 40/46] Update csv_export.py
---
src/registrar/utility/csv_export.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 07014f185..93fcaaf84 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -1097,12 +1097,12 @@ def get_fields(cls, model):
"First ready on": model.get("first_ready_on"),
"Expiration date": model.get("expiration_date"),
"Domain type": model.get("domain_type"),
- "Agency": model.get("federal_agency"),
+ "Agency": model.get("federal_agency__agency"),
"Organization name": model.get("organization_name"),
"City": model.get("city"),
"State": model.get("state_territory"),
"SO": model.get("so_name"),
- "SO email": model.get("so_email"),
+ "SO email": model.get("senior_official__email"),
"Security contact email": model.get("security_contact_email"),
"Created at": model.get("domain__created_at"),
"Deleted": model.get("domain__deleted"),
@@ -1207,12 +1207,12 @@ def get_fields(cls, model):
"First ready on": model.get("first_ready_on"),
"Expiration date": model.get("expiration_date"),
"Domain type": model.get("domain_type"),
- "Agency": model.get("federal_agency"),
+ "Agency": model.get("federal_agency__agency"),
"Organization name": model.get("organization_name"),
"City": model.get("city"),
"State": model.get("state_territory"),
"SO": model.get("so_name"),
- "SO email": model.get("so_email"),
+ "SO email": model.get("senior_official__email"),
"Security contact email": model.get("security_contact_email"),
"Created at": model.get("domain__created_at"),
"Deleted": model.get("domain__deleted"),
From fcdc0f0f0fc4f5d319530619260ab9ee5aaf287d Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 14:53:17 -0700
Subject: [PATCH 41/46] Fix different ordering
---
src/registrar/utility/csv_export.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 93fcaaf84..4de947594 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -1140,9 +1140,9 @@ def get_sort_fields(cls):
"""
# Coalesce is used to replace federal_type of None with ZZZZZ
return [
- "converted_generic_org_type",
- Coalesce("converted_federal_type", Value("ZZZZZ")),
- "converted_federal_agency",
+ "organization_type",
+ Coalesce("federal_type", Value("ZZZZZ")),
+ "federal_agency",
"domain__name",
]
@@ -1250,9 +1250,9 @@ def get_sort_fields(cls):
"""
# Coalesce is used to replace federal_type of None with ZZZZZ
return [
- "converted_generic_org_type",
- Coalesce("converted_federal_type", Value("ZZZZZ")),
- "converted_federal_agency",
+ "organization_type",
+ Coalesce("federal_type", Value("ZZZZZ")),
+ "federal_agency",
"domain__name",
]
From b0cf5df7984800fa1283301f67d61883e58dee1f Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 15:06:47 -0700
Subject: [PATCH 42/46] add zap
---
src/zap.conf | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/zap.conf b/src/zap.conf
index 65468773a..782eaa0e4 100644
--- a/src/zap.conf
+++ b/src/zap.conf
@@ -75,6 +75,7 @@
10038 OUTOFSCOPE http://app:8080/suborganization/
10038 OUTOFSCOPE http://app:8080/transfer/
10038 OUTOFSCOPE http://app:8080/prototype-dns
+10038 OUTOFSCOPE http://app:8080/suborganization
# This URL always returns 404, so include it as well.
10038 OUTOFSCOPE http://app:8080/todo
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
From 6b2552bdbc3f4ee093dcf9edfa5bd1d428781c30 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 18 Dec 2024 15:08:05 -0700
Subject: [PATCH 43/46] Revert "add zap"
This reverts commit b0cf5df7984800fa1283301f67d61883e58dee1f.
---
src/zap.conf | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/zap.conf b/src/zap.conf
index 782eaa0e4..65468773a 100644
--- a/src/zap.conf
+++ b/src/zap.conf
@@ -75,7 +75,6 @@
10038 OUTOFSCOPE http://app:8080/suborganization/
10038 OUTOFSCOPE http://app:8080/transfer/
10038 OUTOFSCOPE http://app:8080/prototype-dns
-10038 OUTOFSCOPE http://app:8080/suborganization
# This URL always returns 404, so include it as well.
10038 OUTOFSCOPE http://app:8080/todo
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
From 0777cf1334e48ff607ddd5061d7e5b52bda0165d Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 19 Dec 2024 09:10:26 -0700
Subject: [PATCH 44/46] Comment comet
---
src/registrar/utility/csv_export.py | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 4de947594..3b3fe350c 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -757,7 +757,10 @@ def parse_row(cls, columns, model):
return row
- # NOTE - this override is temporary. Delete this after we consolidate these @property fields.
+ # NOTE - this override is temporary.
+ # We are running into a problem where DomainDataFull and DomainDataFederal are
+ # pulling the portfolio name, rather than the suborganization name.
+ # This can be removed after that gets fixed.
@classmethod
def get_fields(cls, model):
FIELDS = {
@@ -1088,7 +1091,10 @@ class DomainDataFull(DomainExport):
Inherits from BaseExport -> DomainExport
"""
- # NOTE - this override is temporary. Delete this after we consolidate these @property fields.
+ # NOTE - this override is temporary.
+ # We are running into a problem where DomainDataFull is
+ # pulling the portfolio name, rather than the suborganization name.
+ # This can be removed after that gets fixed.
@classmethod
def get_fields(cls, model):
FIELDS = {
@@ -1198,7 +1204,10 @@ class DomainDataFederal(DomainExport):
Inherits from BaseExport -> DomainExport
"""
- # NOTE - this override is temporary. Delete this after we consolidate these @property fields.
+ # NOTE - this override is temporary.
+ # We are running into a problem where DomainDataFederal is
+ # pulling the portfolio name, rather than the suborganization name.
+ # This can be removed after that gets fixed.
@classmethod
def get_fields(cls, model):
FIELDS = {
From e9d0a5425134d020e35aa6b6b8ac32eac70b7915 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 19 Dec 2024 09:20:25 -0700
Subject: [PATCH 45/46] Update csv_export.py
---
src/registrar/utility/csv_export.py | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 3b3fe350c..40d84e251 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -759,7 +759,8 @@ def parse_row(cls, columns, model):
# NOTE - this override is temporary.
# We are running into a problem where DomainDataFull and DomainDataFederal are
- # pulling the portfolio name, rather than the suborganization name.
+ # pulling the wrong data.
+ # For example, the portfolio name, rather than the suborganization name.
# This can be removed after that gets fixed.
@classmethod
def get_fields(cls, model):
@@ -1093,7 +1094,8 @@ class DomainDataFull(DomainExport):
# NOTE - this override is temporary.
# We are running into a problem where DomainDataFull is
- # pulling the portfolio name, rather than the suborganization name.
+ # pulling the wrong data.
+ # For example, the portfolio name, rather than the suborganization name.
# This can be removed after that gets fixed.
@classmethod
def get_fields(cls, model):
@@ -1206,7 +1208,8 @@ class DomainDataFederal(DomainExport):
# NOTE - this override is temporary.
# We are running into a problem where DomainDataFederal is
- # pulling the portfolio name, rather than the suborganization name.
+ # pulling the wrong data.
+ # For example, the portfolio name, rather than the suborganization name.
# This can be removed after that gets fixed.
@classmethod
def get_fields(cls, model):
From 5f5ca0b780d50a67ce6d76f84f9bdd01c162dcd6 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 19 Dec 2024 09:25:04 -0700
Subject: [PATCH 46/46] Update csv_export.py
---
src/registrar/utility/csv_export.py | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index 40d84e251..66809777b 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -1097,6 +1097,12 @@ class DomainDataFull(DomainExport):
# pulling the wrong data.
# For example, the portfolio name, rather than the suborganization name.
# This can be removed after that gets fixed.
+ # The following fields are changed from DomainExport:
+ # converted_organization_name => organization_name
+ # converted_city => city
+ # converted_state_territory => state_territory
+ # converted_so_name => so_name
+ # converted_so_email => senior_official__email
@classmethod
def get_fields(cls, model):
FIELDS = {
@@ -1207,10 +1213,16 @@ class DomainDataFederal(DomainExport):
"""
# NOTE - this override is temporary.
- # We are running into a problem where DomainDataFederal is
+ # We are running into a problem where DomainDataFull is
# pulling the wrong data.
# For example, the portfolio name, rather than the suborganization name.
# This can be removed after that gets fixed.
+ # The following fields are changed from DomainExport:
+ # converted_organization_name => organization_name
+ # converted_city => city
+ # converted_state_territory => state_territory
+ # converted_so_name => so_name
+ # converted_so_email => senior_official__email
@classmethod
def get_fields(cls, model):
FIELDS = {