From 237e79e4938bcee8186e46a5f8db83e9a60cceeb Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Fri, 7 Mar 2025 09:22:27 -0800 Subject: [PATCH 01/73] Add portfolio org email templates --- .../portfolio_org_update_notification.txt | 34 +++++++++++++++++++ ...tfolio_org_update_notification_subject.txt | 1 + 2 files changed, 35 insertions(+) create mode 100644 src/registrar/templates/emails/portfolio_org_update_notification.txt create mode 100644 src/registrar/templates/emails/portfolio_org_update_notification_subject.txt diff --git a/src/registrar/templates/emails/portfolio_org_update_notification.txt b/src/registrar/templates/emails/portfolio_org_update_notification.txt new file mode 100644 index 000000000..80dfd2ecb --- /dev/null +++ b/src/registrar/templates/emails/portfolio_org_update_notification.txt @@ -0,0 +1,34 @@ +{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} +Hi, {% if portfolio_admin and portfolio_admin.first_name %} + +An update was made to your .gov organization + +ORGANIZATION: {{ portfolio.organization_name }} +UPDATED BY: {{ editor_email }} +UPDATED ON: {{ date }} +INFORMATION UPDATED: put page where info was edited here + +You can view this update in the .gov registrar . + +---------------------------------------------------------------- + +WHY DID YOU RECEIVE THIS EMAIL? +You're listed as an admin for {{ $portfolio.organization_name }}, so you'll receive a +notification whenever changes are made to that .gov organization. + +If you have questions or concerns, reach out to the person who made the change or reply +to this email. + +THANK YOU +.Gov helps the public identify official, trusted information. Thank you for using a .gov +domain. + +---------------------------------------------------------------- + +The .gov team +Contact us: +Learn about .gov + +The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency +(CISA) +{% endautoescape %} diff --git a/src/registrar/templates/emails/portfolio_org_update_notification_subject.txt b/src/registrar/templates/emails/portfolio_org_update_notification_subject.txt new file mode 100644 index 000000000..a30f72a54 --- /dev/null +++ b/src/registrar/templates/emails/portfolio_org_update_notification_subject.txt @@ -0,0 +1 @@ +An update was made to your .gov organization \ No newline at end of file From dd5d00ddcaf21536b466753fdd6b4aa5daf0c1b2 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Fri, 7 Mar 2025 15:49:07 -0800 Subject: [PATCH 02/73] Create portfolio org update email methods --- src/registrar/utility/email_invitations.py | 46 ++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index 08ebb4d86..eaac4f06a 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -226,6 +226,52 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_i ) return all_admin_emails_sent +def send_portfolio_organization_update_email(editor, portfolio-portfolio): + """ + Sends an email notification to all portfolio admin when portfolio organization is updated. + + Raises exceptions for validation or email-sending issues. + + Args: + editor (User): The user editing the portfolio organization. + portfolio (Portfolio): The portfolio object whose organization information is changed. + + Returns: + Boolean indicating if all messages were sent successfully. + + Raises: + MissingEmailError: If the requestor has no email associated with their account. + EmailSendingError: If there is an error while sending the email. + """ + editor_email = _get_requestor_email(editor, portfolio=portfolio) + # Get each portfolio admin from list + user_portfolio_permissions = UserPortfolioPermission.objects.filter( + portfolio=portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ).exclude(user__email=editor_email) + for user_portfolio_permission in user_portfolio_permissions: + # Send email to each portfolio_admin + user = user_portfolio_permission.user + try: + send_templated_email( + "emails/portfolio_org_update_notification.txt", + "emails/portfolio_org_update_notification_subject.txt", + to_address=user.email, + context={ + "portfolio": portfolio, + "editor_email": editor_email, + "portfolio_admin": user, + "date": date.today(), + }, + ) + except EmailSendingError: + logger.warning( + "Could not send email organization admin notification to %s " "for portfolio: %s", + user.email, + portfolio.organization_name, + exc_info=True, + ) + all_emails_sent = False + return all_emails_sent def send_portfolio_member_permission_update_email(requestor, permissions: UserPortfolioPermission): """ From 8fb349f0b953c8e8357846ab65295607e1ad2b72 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:50:11 -0800 Subject: [PATCH 03/73] Save email template progress --- .../portfolio_org_update_notification.txt | 8 ++++---- src/registrar/utility/email_invitations.py | 10 +++++----- src/registrar/views/portfolios.py | 19 ++++++++++++++++++- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/registrar/templates/emails/portfolio_org_update_notification.txt b/src/registrar/templates/emails/portfolio_org_update_notification.txt index 80dfd2ecb..639f08fdf 100644 --- a/src/registrar/templates/emails/portfolio_org_update_notification.txt +++ b/src/registrar/templates/emails/portfolio_org_update_notification.txt @@ -1,10 +1,10 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} -Hi, {% if portfolio_admin and portfolio_admin.first_name %} +Hi, {% if portfolio_admin and portfolio_admin.first_name %}{% endif %} An update was made to your .gov organization -ORGANIZATION: {{ portfolio.organization_name }} -UPDATED BY: {{ editor_email }} +ORGANIZATION: {{ portfolio }} +UPDATED BY: {{ editor.email }} UPDATED ON: {{ date }} INFORMATION UPDATED: put page where info was edited here @@ -13,7 +13,7 @@ You can view this update in the .gov registrar . ---------------------------------------------------------------- WHY DID YOU RECEIVE THIS EMAIL? -You're listed as an admin for {{ $portfolio.organization_name }}, so you'll receive a +You're listed as an admin for {{ portfolio }}, so you'll receive a notification whenever changes are made to that .gov organization. If you have questions or concerns, reach out to the person who made the change or reply diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index eaac4f06a..df1ee1bd4 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -226,7 +226,7 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_i ) return all_admin_emails_sent -def send_portfolio_organization_update_email(editor, portfolio-portfolio): +def send_portfolio_organization_update_email(editor, portfolio): """ Sends an email notification to all portfolio admin when portfolio organization is updated. @@ -243,7 +243,7 @@ def send_portfolio_organization_update_email(editor, portfolio-portfolio): MissingEmailError: If the requestor has no email associated with their account. EmailSendingError: If there is an error while sending the email. """ - editor_email = _get_requestor_email(editor, portfolio=portfolio) + editor_email = editor.email # Get each portfolio admin from list user_portfolio_permissions = UserPortfolioPermission.objects.filter( portfolio=portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] @@ -258,7 +258,7 @@ def send_portfolio_organization_update_email(editor, portfolio-portfolio): to_address=user.email, context={ "portfolio": portfolio, - "editor_email": editor_email, + "editor": editor, "portfolio_admin": user, "date": date.today(), }, @@ -267,7 +267,7 @@ def send_portfolio_organization_update_email(editor, portfolio-portfolio): logger.warning( "Could not send email organization admin notification to %s " "for portfolio: %s", user.email, - portfolio.organization_name, + portfolio, exc_info=True, ) all_emails_sent = False @@ -444,7 +444,7 @@ def _send_portfolio_admin_addition_emails_to_portfolio_admins(email: str, reques logger.warning( "Could not send email organization admin notification to %s " "for portfolio: %s", user.email, - portfolio.organization_name, + portfolio, exc_info=True, ) all_emails_sent = False diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 1882cc11b..5d33adf17 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -35,6 +35,7 @@ from registrar.utility.email_invitations import ( send_portfolio_invitation_remove_email, send_portfolio_member_permission_remove_email, send_portfolio_member_permission_update_email, + send_portfolio_organization_update_email ) from registrar.utility.errors import MissingEmailError from registrar.utility.enums import DefaultUserValues @@ -840,7 +841,23 @@ class PortfolioOrganizationView(DetailView, FormMixin): self.object = self.get_object() form = self.get_form() if form.is_valid(): - return self.form_valid(form) + user=request.user + try: + if not send_portfolio_organization_update_email( + editor=user, portfolio=self.request.session.get("portfolio") + ): + messages.warning(self.request, f"Could not send email notification to {user.email}.") + return redirect(reverse("organization")) + except Exception as e: + messages.error( + request, + f"An unexpected error occurred: {str(e)}. If the issue persists, " + f"please contact {DefaultUserValues.HELP_EMAIL}.", + ) + logger.error(f"An unexpected error occurred: {str(e)}.", exc_info=True) + return None + messages.success(self.request, "The portfolio organization information has been updated.") + return redirect(reverse("organization")) else: return self.form_invalid(form) From 7516e1af0b331178fa5814616282d87d34d12460 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 10 Mar 2025 10:13:31 -0600 Subject: [PATCH 04/73] Change defaults --- src/registrar/models/domain.py | 2 +- src/registrar/models/public_contact.py | 19 +++++++++---------- src/registrar/utility/csv_export.py | 2 +- src/registrar/utility/enums.py | 9 +++++---- src/registrar/views/domain.py | 6 +++--- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index d3c0ed347..9a54adb0d 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1680,7 +1680,7 @@ class Domain(TimeStampedModel, DomainHelper): DF = epp.DiscloseField fields = {DF.EMAIL} - hidden_security_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value] + hidden_security_emails = [email for email in DefaultEmail] disclose = is_security and contact.email not in hidden_security_emails # Delete after testing on other devices logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose) diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index 71ed07de5..6a0228a7b 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -92,13 +92,12 @@ class PublicContact(TimeStampedModel): return cls( contact_type=PublicContact.ContactTypeChoices.REGISTRANT, registry_id=get_id(), - name="CSD/CB – Attn: Cameron Dixon", + name="CSD/CB – Attn: .gov TLD", org="Cybersecurity and Infrastructure Security Agency", - street1="CISA – NGR STOP 0645", - street2="1110 N. Glebe Rd.", + street1="1110 N. Glebe Rd", city="Arlington", sp="VA", - pc="20598-0645", + pc="22201", cc="US", email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, voice="+1.8882820870", @@ -110,9 +109,9 @@ class PublicContact(TimeStampedModel): return cls( contact_type=PublicContact.ContactTypeChoices.ADMINISTRATIVE, registry_id=get_id(), - name="Program Manager", + name="CSD/CB – Attn: .gov TLD", org="Cybersecurity and Infrastructure Security Agency", - street1="4200 Wilson Blvd.", + street1="1110 N. Glebe Rd", city="Arlington", sp="VA", pc="22201", @@ -127,9 +126,9 @@ class PublicContact(TimeStampedModel): return cls( contact_type=PublicContact.ContactTypeChoices.TECHNICAL, registry_id=get_id(), - name="Registry Customer Service", + name="CSD/CB – Attn: .gov TLD", org="Cybersecurity and Infrastructure Security Agency", - street1="4200 Wilson Blvd.", + street1="1110 N. Glebe Rd", city="Arlington", sp="VA", pc="22201", @@ -144,9 +143,9 @@ class PublicContact(TimeStampedModel): return cls( contact_type=PublicContact.ContactTypeChoices.SECURITY, registry_id=get_id(), - name="Registry Customer Service", + name="CSD/CB – Attn: .gov TLD", org="Cybersecurity and Infrastructure Security Agency", - street1="4200 Wilson Blvd.", + street1="1110 N. Glebe Rd", city="Arlington", sp="VA", pc="22201", diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index fad58b2e2..5660cb41d 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -740,7 +740,7 @@ class DomainExport(BaseExport): 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} + invalid_emails = [email for email in DefaultEmail] if ( not security_contact_email or not isinstance(security_contact_email, str) diff --git a/src/registrar/utility/enums.py b/src/registrar/utility/enums.py index 47e6da47f..8b3aff7ad 100644 --- a/src/registrar/utility/enums.py +++ b/src/registrar/utility/enums.py @@ -29,16 +29,17 @@ class LogCode(Enum): DEFAULT = 5 -class DefaultEmail(Enum): +class DefaultEmail(StrEnum): """Stores the string values of default emails Overview of emails: - - PUBLIC_CONTACT_DEFAULT: "dotgov@cisa.dhs.gov" + - PUBLIC_CONTACT_DEFAULT: "help@get.gov" + - OLD_PUBLIC_CONTACT_DEFAULT: "dotgov@cisa.dhs.gov" - LEGACY_DEFAULT: "registrar@dotgov.gov" - - HELP_EMAIL: "help@get.gov" """ - PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov" + PUBLIC_CONTACT_DEFAULT = "help@get.gov" + OLD_PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov" LEGACY_DEFAULT = "registrar@dotgov.gov" diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 3a083393e..0b13dda69 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -398,7 +398,7 @@ class DomainView(DomainBaseView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value] + default_emails = [email for email in DefaultEmail] context["hidden_security_emails"] = default_emails @@ -456,7 +456,7 @@ class DomainRenewalView(DomainBaseView): context = super().get_context_data(**kwargs) - default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value] + default_emails = [email for email in DefaultEmail] context["hidden_security_emails"] = default_emails @@ -1166,7 +1166,7 @@ class DomainSecurityEmailView(DomainFormBaseView): initial = super().get_initial() security_contact = self.object.security_contact - invalid_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value] + invalid_emails = [email for email in DefaultEmail] if security_contact is None or security_contact.email in invalid_emails: initial["security_email"] = None return initial From 918fc689c13afd8c51cd6923d666233540542f62 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 10 Mar 2025 14:54:58 -0700 Subject: [PATCH 05/73] Send email when senior official updated --- .../portfolio_org_update_notification.txt | 6 ++--- src/registrar/utility/email_invitations.py | 4 ++- src/registrar/views/portfolios.py | 27 ++++++++++++++++++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/emails/portfolio_org_update_notification.txt b/src/registrar/templates/emails/portfolio_org_update_notification.txt index 639f08fdf..1b9dbf2fc 100644 --- a/src/registrar/templates/emails/portfolio_org_update_notification.txt +++ b/src/registrar/templates/emails/portfolio_org_update_notification.txt @@ -1,12 +1,12 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} -Hi, {% if portfolio_admin and portfolio_admin.first_name %}{% endif %} +Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first_name }}.{% endif %} -An update was made to your .gov organization +An update was made to your .gov organization. ORGANIZATION: {{ portfolio }} UPDATED BY: {{ editor.email }} UPDATED ON: {{ date }} -INFORMATION UPDATED: put page where info was edited here +INFORMATION UPDATED: {{ updated_info }} You can view this update in the .gov registrar . diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index df1ee1bd4..729da3f11 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -226,7 +226,7 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_i ) return all_admin_emails_sent -def send_portfolio_organization_update_email(editor, portfolio): +def send_portfolio_organization_update_email(editor, portfolio, updated_page): """ Sends an email notification to all portfolio admin when portfolio organization is updated. @@ -257,10 +257,12 @@ def send_portfolio_organization_update_email(editor, portfolio): "emails/portfolio_org_update_notification_subject.txt", to_address=user.email, context={ + "requested_user": user, "portfolio": portfolio, "editor": editor, "portfolio_admin": user, "date": date.today(), + "updated_info": "Organization" }, ) except EmailSendingError: diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 5d33adf17..14e6234b8 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -844,7 +844,7 @@ class PortfolioOrganizationView(DetailView, FormMixin): user=request.user try: if not send_portfolio_organization_update_email( - editor=user, portfolio=self.request.session.get("portfolio") + editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Organization" ): messages.warning(self.request, f"Could not send email notification to {user.email}.") return redirect(reverse("organization")) @@ -909,6 +909,31 @@ class PortfolioSeniorOfficialView(DetailView, FormMixin): 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(): + user=request.user + try: + if not send_portfolio_organization_update_email( + editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Senior Official" + ): + messages.warning(self.request, f"Could not send email notification to {user.email}.") + return redirect(reverse("senior-official")) + except Exception as e: + messages.error( + request, + f"An unexpected error occurred: {str(e)}. If the issue persists, " + f"please contact {DefaultUserValues.HELP_EMAIL}.", + ) + logger.error(f"An unexpected error occurred: {str(e)}.", exc_info=True) + return None + messages.success(self.request, "The portfolio organization information has been updated.") + return redirect(reverse("senior-official")) + else: + return self.form_invalid(form) + @grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) class PortfolioMembersView(View): From ca06625c14453d4b12e4849e491bff2ded89b509 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Mar 2025 08:46:51 -0600 Subject: [PATCH 06/73] Test disclose settings --- src/registrar/models/domain.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 9a54adb0d..fede6c96d 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1676,15 +1676,17 @@ class Domain(TimeStampedModel, DomainHelper): """creates a disclose object that can be added to a contact Create using .disclose= on the command before sending. if item is security email then make sure email is visible""" - is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY DF = epp.DiscloseField - fields = {DF.EMAIL} - - hidden_security_emails = [email for email in DefaultEmail] - disclose = is_security and contact.email not in hidden_security_emails - # Delete after testing on other devices + fields = {} + disclose = False + match contact.contact_type: + case contact.ContactTypeChoices.SECURITY: + fields = {DF.EMAIL} + disclose = True + case contact.ContactTypeChoices.ADMINISTRATIVE: + fields = {DF.EMAIL, DF.VOICE, DF.ADDR} + disclose = True logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose) - # Will only disclose DF.EMAIL if its not the default return epp.Disclose( flag=disclose, fields=fields, From 7c42ed958b4dc58f28d5290d349bc90d5e666b3f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Mar 2025 11:32:50 -0600 Subject: [PATCH 07/73] cleanup --- src/registrar/models/public_contact.py | 8 ++++---- src/registrar/templates/domain_security_email.html | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index 6a0228a7b..6c123474d 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -99,7 +99,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT, voice="+1.8882820870", pw="thisisnotapassword", ) @@ -116,7 +116,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT, voice="+1.8882820870", pw="thisisnotapassword", ) @@ -133,7 +133,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT, voice="+1.8882820870", pw="thisisnotapassword", ) @@ -150,7 +150,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT, voice="+1.8882820870", pw="thisisnotapassword", ) diff --git a/src/registrar/templates/domain_security_email.html b/src/registrar/templates/domain_security_email.html index e74ecf709..662eec152 100644 --- a/src/registrar/templates/domain_security_email.html +++ b/src/registrar/templates/domain_security_email.html @@ -40,7 +40,7 @@ + >{% if form.security_email.value is None or form.security_email.value == "dotgov@cisa.dhs.gov" or form.security_email.value == "registrar@dotgov.gov" or form.security_email.value == "help@get.gov" %}Add security email{% else %}Save{% endif %} {% endblock %} {# domain_content #} From 85f85d013ed5fe5c62d390b88afede1effe5f1be Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Mar 2025 12:09:45 -0600 Subject: [PATCH 08/73] Update domain.py --- src/registrar/models/domain.py | 39 ++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index fede6c96d..c5af4ef0a 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1677,20 +1677,31 @@ class Domain(TimeStampedModel, DomainHelper): .disclose= on the command before sending. if item is security email then make sure email is visible""" DF = epp.DiscloseField - fields = {} - disclose = False - match contact.contact_type: - case contact.ContactTypeChoices.SECURITY: - fields = {DF.EMAIL} - disclose = True - case contact.ContactTypeChoices.ADMINISTRATIVE: - fields = {DF.EMAIL, DF.VOICE, DF.ADDR} - disclose = True - logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose) - return epp.Disclose( - flag=disclose, - fields=fields, - ) + contact_disclose_map = { + contact.ContactTypeChoices.SECURITY: { + "fields": [DF.EMAIL], + "disclose": True + }, + contact.ContactTypeChoices.ADMINISTRATIVE: { + "fields": [DF.EMAIL, DF.VOICE, DF.ADDR], + "types": {DF.ADDR: "loc"}, + "disclose": True + }, + contact.ContactTypeChoices.REGISTRANT: { + "fields": [], + "disclose": False, + }, + contact.ContactTypeChoices.TECHNICAL: { + "fields": [], + "disclose": False, + } + } + if contact.contact_type not in contact_disclose_map: + raise ValueError(f"_disclose_fields => Invalid or misconfigured contact type '{contact.contact_type}'") + + disclose_config = contact_disclose_map.get(contact.contact_type, {"fields": [], "disclose": False}) + logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_config.get("disclose")) + return epp.Disclose(**disclose_config) def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore return epp.PostalInfo( # type: ignore From 6cc477985cd4e5db1769ab828d4a798620fb0abc Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Mar 2025 12:21:46 -0600 Subject: [PATCH 09/73] Update domain.py --- src/registrar/models/domain.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index c5af4ef0a..21da23405 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1676,31 +1676,32 @@ class Domain(TimeStampedModel, DomainHelper): """creates a disclose object that can be added to a contact Create using .disclose= on the command before sending. if item is security email then make sure email is visible""" + # Helpful Note: 'flag' is what toggles disclosure DF = epp.DiscloseField contact_disclose_map = { contact.ContactTypeChoices.SECURITY: { "fields": [DF.EMAIL], - "disclose": True + "flag": True }, contact.ContactTypeChoices.ADMINISTRATIVE: { "fields": [DF.EMAIL, DF.VOICE, DF.ADDR], "types": {DF.ADDR: "loc"}, - "disclose": True + "flag": True }, contact.ContactTypeChoices.REGISTRANT: { "fields": [], - "disclose": False, + "flag": False, }, contact.ContactTypeChoices.TECHNICAL: { "fields": [], - "disclose": False, + "flag": False, } } if contact.contact_type not in contact_disclose_map: raise ValueError(f"_disclose_fields => Invalid or misconfigured contact type '{contact.contact_type}'") - disclose_config = contact_disclose_map.get(contact.contact_type, {"fields": [], "disclose": False}) - logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_config.get("disclose")) + disclose_config = contact_disclose_map.get(contact.contact_type, {"fields": [], "flag": False}) + logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_config.get("flag")) return epp.Disclose(**disclose_config) def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore From fa7e69251d64332986935bfdfc5f97add9549408 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 11 Mar 2025 12:22:37 -0600 Subject: [PATCH 10/73] cleanup part 2 --- src/registrar/models/domain.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 21da23405..a92823788 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1688,18 +1688,7 @@ class Domain(TimeStampedModel, DomainHelper): "types": {DF.ADDR: "loc"}, "flag": True }, - contact.ContactTypeChoices.REGISTRANT: { - "fields": [], - "flag": False, - }, - contact.ContactTypeChoices.TECHNICAL: { - "fields": [], - "flag": False, - } } - if contact.contact_type not in contact_disclose_map: - raise ValueError(f"_disclose_fields => Invalid or misconfigured contact type '{contact.contact_type}'") - disclose_config = contact_disclose_map.get(contact.contact_type, {"fields": [], "flag": False}) logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_config.get("flag")) return epp.Disclose(**disclose_config) From 50fb3adfa497c7bb1c623dd583558b131a08ddfe Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:08:53 -0700 Subject: [PATCH 11/73] Correct misdeleted line --- src/registrar/utility/email_invitations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index 729da3f11..74fa8a5ce 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -446,7 +446,7 @@ def _send_portfolio_admin_addition_emails_to_portfolio_admins(email: str, reques logger.warning( "Could not send email organization admin notification to %s " "for portfolio: %s", user.email, - portfolio, + portfolio.organization_name, exc_info=True, ) all_emails_sent = False From 548317eb0b66a31d07c5843f647256ce7ba07135 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:23:23 -0700 Subject: [PATCH 12/73] Update portfolio org email warning text --- src/registrar/views/portfolios.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 14e6234b8..82b212c48 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -846,7 +846,7 @@ class PortfolioOrganizationView(DetailView, FormMixin): if not send_portfolio_organization_update_email( editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Organization" ): - messages.warning(self.request, f"Could not send email notification to {user.email}.") + messages.warning(self.request, f"Could not send email notification to all organization admins.") return redirect(reverse("organization")) except Exception as e: messages.error( @@ -919,7 +919,7 @@ class PortfolioSeniorOfficialView(DetailView, FormMixin): if not send_portfolio_organization_update_email( editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Senior Official" ): - messages.warning(self.request, f"Could not send email notification to {user.email}.") + messages.warning(self.request, f"Could not send email notification to all organization admins.") return redirect(reverse("senior-official")) except Exception as e: messages.error( From 884e6a30b9046d9278a50fa374565256874e0aa7 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:29:18 -0700 Subject: [PATCH 13/73] Fix linting --- src/registrar/utility/email_invitations.py | 4 +++- src/registrar/views/portfolios.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index 74fa8a5ce..1604eeaf2 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -226,6 +226,7 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_i ) return all_admin_emails_sent + def send_portfolio_organization_update_email(editor, portfolio, updated_page): """ Sends an email notification to all portfolio admin when portfolio organization is updated. @@ -262,7 +263,7 @@ def send_portfolio_organization_update_email(editor, portfolio, updated_page): "editor": editor, "portfolio_admin": user, "date": date.today(), - "updated_info": "Organization" + "updated_info": "Organization", }, ) except EmailSendingError: @@ -275,6 +276,7 @@ def send_portfolio_organization_update_email(editor, portfolio, updated_page): all_emails_sent = False return all_emails_sent + def send_portfolio_member_permission_update_email(requestor, permissions: UserPortfolioPermission): """ Sends an email notification to a portfolio member when their permissions are updated. diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 82b212c48..084ec6edd 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -35,7 +35,7 @@ from registrar.utility.email_invitations import ( send_portfolio_invitation_remove_email, send_portfolio_member_permission_remove_email, send_portfolio_member_permission_update_email, - send_portfolio_organization_update_email + send_portfolio_organization_update_email, ) from registrar.utility.errors import MissingEmailError from registrar.utility.enums import DefaultUserValues @@ -841,7 +841,7 @@ class PortfolioOrganizationView(DetailView, FormMixin): self.object = self.get_object() form = self.get_form() if form.is_valid(): - user=request.user + user = request.user try: if not send_portfolio_organization_update_email( editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Organization" @@ -914,7 +914,7 @@ class PortfolioSeniorOfficialView(DetailView, FormMixin): self.object = self.get_object() form = self.get_form() if form.is_valid(): - user=request.user + user = request.user try: if not send_portfolio_organization_update_email( editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Senior Official" From b4aed440b88aa9f239334fb8b76aa5d67dd03ff0 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:34:33 -0700 Subject: [PATCH 14/73] Remove unused f strings --- src/registrar/views/portfolios.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 25d44cb78..d6002ba14 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -856,7 +856,7 @@ class PortfolioOrganizationView(DetailView, FormMixin): if not send_portfolio_organization_update_email( editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Organization" ): - messages.warning(self.request, f"Could not send email notification to all organization admins.") + messages.warning(self.request, "Could not send email notification to all organization admins.") return redirect(reverse("organization")) except Exception as e: messages.error( @@ -929,7 +929,7 @@ class PortfolioSeniorOfficialView(DetailView, FormMixin): if not send_portfolio_organization_update_email( editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Senior Official" ): - messages.warning(self.request, f"Could not send email notification to all organization admins.") + messages.warning(self.request, "Could not send email notification to all organization admins.") return redirect(reverse("senior-official")) except Exception as e: messages.error( From df930c949705be2bf83aecbf4bc3af4bf969ff92 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 11 Mar 2025 15:39:27 -0700 Subject: [PATCH 15/73] Change return value of posting to org forms --- src/registrar/utility/email_invitations.py | 4 +--- src/registrar/views/portfolios.py | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index 1604eeaf2..74fa8a5ce 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -226,7 +226,6 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_i ) return all_admin_emails_sent - def send_portfolio_organization_update_email(editor, portfolio, updated_page): """ Sends an email notification to all portfolio admin when portfolio organization is updated. @@ -263,7 +262,7 @@ def send_portfolio_organization_update_email(editor, portfolio, updated_page): "editor": editor, "portfolio_admin": user, "date": date.today(), - "updated_info": "Organization", + "updated_info": "Organization" }, ) except EmailSendingError: @@ -276,7 +275,6 @@ def send_portfolio_organization_update_email(editor, portfolio, updated_page): all_emails_sent = False return all_emails_sent - def send_portfolio_member_permission_update_email(requestor, permissions: UserPortfolioPermission): """ Sends an email notification to a portfolio member when their permissions are updated. diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index d6002ba14..67b470bea 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -857,7 +857,7 @@ class PortfolioOrganizationView(DetailView, FormMixin): editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Organization" ): messages.warning(self.request, "Could not send email notification to all organization admins.") - return redirect(reverse("organization")) + return self.form_valid(form) except Exception as e: messages.error( request, @@ -867,7 +867,7 @@ class PortfolioOrganizationView(DetailView, FormMixin): logger.error(f"An unexpected error occurred: {str(e)}.", exc_info=True) return None messages.success(self.request, "The portfolio organization information has been updated.") - return redirect(reverse("organization")) + return self.form_valid(form) else: return self.form_invalid(form) @@ -930,7 +930,7 @@ class PortfolioSeniorOfficialView(DetailView, FormMixin): editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Senior Official" ): messages.warning(self.request, "Could not send email notification to all organization admins.") - return redirect(reverse("senior-official")) + return self.form_valid(form) except Exception as e: messages.error( request, @@ -940,7 +940,7 @@ class PortfolioSeniorOfficialView(DetailView, FormMixin): logger.error(f"An unexpected error occurred: {str(e)}.", exc_info=True) return None messages.success(self.request, "The portfolio organization information has been updated.") - return redirect(reverse("senior-official")) + return self.form_valid(form) else: return self.form_invalid(form) From c91a0181fe984cc66e3e3c9796da58cf98044d55 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 11 Mar 2025 15:40:14 -0700 Subject: [PATCH 16/73] Remove unused form valid --- src/registrar/views/portfolios.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 67b470bea..0bc7911e0 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -857,7 +857,6 @@ class PortfolioOrganizationView(DetailView, FormMixin): editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Organization" ): messages.warning(self.request, "Could not send email notification to all organization admins.") - return self.form_valid(form) except Exception as e: messages.error( request, @@ -930,7 +929,6 @@ class PortfolioSeniorOfficialView(DetailView, FormMixin): editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Senior Official" ): messages.warning(self.request, "Could not send email notification to all organization admins.") - return self.form_valid(form) except Exception as e: messages.error( request, From e971ba60c7c5e7def0785d570864af3ed4d5aaae Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 11 Mar 2025 15:41:39 -0700 Subject: [PATCH 17/73] Add form validation for senior official --- src/registrar/views/portfolios.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 0bc7911e0..6bfc44286 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -865,7 +865,6 @@ class PortfolioOrganizationView(DetailView, FormMixin): ) logger.error(f"An unexpected error occurred: {str(e)}.", exc_info=True) return None - messages.success(self.request, "The portfolio organization information has been updated.") return self.form_valid(form) else: return self.form_invalid(form) @@ -937,11 +936,27 @@ class PortfolioSeniorOfficialView(DetailView, FormMixin): ) logger.error(f"An unexpected error occurred: {str(e)}.", exc_info=True) return None - messages.success(self.request, "The portfolio organization information has been updated.") return self.form_valid(form) else: return self.form_invalid(form) + def form_valid(self, form): + """Handle the case when the form is valid.""" + self.object = form.save(commit=False) + self.object.creator = self.request.user + self.object.save() + messages.success(self.request, "The senior official information for this portfolio has been updated.") + return super().form_valid(form) + + def form_invalid(self, form): + """Handle the case when the form is invalid.""" + return self.render_to_response(self.get_context_data(form=form)) + + def get_success_url(self): + """Redirect to the overview page for the portfolio.""" + return reverse("senior-official") + + @grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) class PortfolioMembersView(View): From 2ac4c047032d91bb9db36cafeaee21ef0d7c0c09 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 11 Mar 2025 15:52:28 -0700 Subject: [PATCH 18/73] Fix linting --- src/registrar/utility/email_invitations.py | 4 +++- src/registrar/views/portfolios.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index 74fa8a5ce..1604eeaf2 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -226,6 +226,7 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_i ) return all_admin_emails_sent + def send_portfolio_organization_update_email(editor, portfolio, updated_page): """ Sends an email notification to all portfolio admin when portfolio organization is updated. @@ -262,7 +263,7 @@ def send_portfolio_organization_update_email(editor, portfolio, updated_page): "editor": editor, "portfolio_admin": user, "date": date.today(), - "updated_info": "Organization" + "updated_info": "Organization", }, ) except EmailSendingError: @@ -275,6 +276,7 @@ def send_portfolio_organization_update_email(editor, portfolio, updated_page): all_emails_sent = False return all_emails_sent + def send_portfolio_member_permission_update_email(requestor, permissions: UserPortfolioPermission): """ Sends an email notification to a portfolio member when their permissions are updated. diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 6bfc44286..5f2532f8f 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -957,7 +957,6 @@ class PortfolioSeniorOfficialView(DetailView, FormMixin): return reverse("senior-official") - @grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM) class PortfolioMembersView(View): From 5f2574389e7b18d6675953af1cde95a529bd2e58 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Mar 2025 09:54:15 -0600 Subject: [PATCH 19/73] Cleanup for unit test --- src/registrar/models/domain.py | 27 ++++++++++------------- src/registrar/tests/common.py | 17 +++++++++----- src/registrar/tests/test_models_domain.py | 18 ++++++++++++--- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index a92823788..5397d1f7f 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1676,22 +1676,19 @@ class Domain(TimeStampedModel, DomainHelper): """creates a disclose object that can be added to a contact Create using .disclose= on the command before sending. if item is security email then make sure email is visible""" - # Helpful Note: 'flag' is what toggles disclosure DF = epp.DiscloseField - contact_disclose_map = { - contact.ContactTypeChoices.SECURITY: { - "fields": [DF.EMAIL], - "flag": True - }, - contact.ContactTypeChoices.ADMINISTRATIVE: { - "fields": [DF.EMAIL, DF.VOICE, DF.ADDR], - "types": {DF.ADDR: "loc"}, - "flag": True - }, - } - disclose_config = contact_disclose_map.get(contact.contact_type, {"fields": [], "flag": False}) - logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_config.get("flag")) - return epp.Disclose(**disclose_config) + disclose_fields = {"fields": [], "flag": False} + match contact.contact_type: + case contact.ContactTypeChoices.SECURITY: + hidden_security_emails = [email for email in DefaultEmail] + disclose_fields = { + "fields": [DF.EMAIL], + "flag": contact.email not in hidden_security_emails, + } + case contact.ContactTypeChoices.ADMINISTRATIVE: + disclose_fields = {"fields": [DF.EMAIL, DF.VOICE, DF.ADDR], "types": {DF.ADDR: "loc"}, "flag": True} + logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_fields.get("flag")) + return epp.Disclose(**disclose_fields) def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore return epp.PostalInfo( # type: ignore diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 8fbf052a4..500ecfe39 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -1941,14 +1941,19 @@ class MockEppLib(TestCase): self.mockedSendFunction = self.mockSendPatch.start() self.mockedSendFunction.side_effect = self.mockSend - def _convertPublicContactToEpp(self, contact: PublicContact, disclose_email=False, createContact=True): + def _convertPublicContactToEpp( + self, + contact: PublicContact, + disclose_email=False, + createContact=True, + disclose_fields=None, + disclose_types=None, + ): DF = common.DiscloseField - fields = {DF.EMAIL} + if disclose_fields is None: + disclose_fields = [DF.EMAIL] - di = common.Disclose( - flag=disclose_email, - fields=fields, - ) + di = common.Disclose(flag=disclose_email, fields=disclose_fields, types=disclose_types) # check docs here looks like we may have more than one address field but addr = common.ContactAddr( diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 93072f93b..27567a211 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -988,9 +988,21 @@ class TestRegistrantContacts(MockEppLib): for contact in contacts: expected_contact = contact[0] actual_contact = contact[1] - is_security = expected_contact.contact_type == "security" - expectedCreateCommand = self._convertPublicContactToEpp(expected_contact, disclose_email=is_security) - # Should only be disclosed if the type is security, as the email is valid + if expected_contact.contact_type == PublicContact.ContactTypeChoices.SECURITY: + expectedCreateCommand = self._convertPublicContactToEpp( + expected_contact, disclose_email=True, disclose_fields=["email"] + ) + elif expected_contact.contact_type == PublicContact.ContactTypeChoices.ADMINISTRATIVE: + expectedCreateCommand = self._convertPublicContactToEpp( + expected_contact, + disclose_email=True, + disclose_fields=["email", "voice", "addr"], + disclose_types={"addr": "loc"}, + ) + else: + expectedCreateCommand = self._convertPublicContactToEpp( + expected_contact, disclose_email=False, disclose_fields=[] + ) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) # The emails should match on both items self.assertEqual(expected_contact.email, actual_contact.email) From 947c4ed9e8a277de4c6a35b45d0151f3ff954861 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Wed, 12 Mar 2025 09:51:01 -0700 Subject: [PATCH 20/73] Readded all emails sent reference --- src/registrar/utility/email_invitations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index 1604eeaf2..f98ef5459 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -245,6 +245,7 @@ def send_portfolio_organization_update_email(editor, portfolio, updated_page): EmailSendingError: If there is an error while sending the email. """ editor_email = editor.email + all_emails_sent = True # Get each portfolio admin from list user_portfolio_permissions = UserPortfolioPermission.objects.filter( portfolio=portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] From 2e1f79434c2db97f6f8c20737e0b56b05db10c1b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:58:38 -0600 Subject: [PATCH 21/73] Unit tests --- src/registrar/tests/common.py | 4 +- src/registrar/tests/test_models_domain.py | 58 +++++++++++++++++++---- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 500ecfe39..cad041242 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -1424,9 +1424,9 @@ class MockEppLib(TestCase): ) mockDefaultTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData( - "defaultTech", "dotgov@cisa.dhs.gov" + "defaultTech", "help@get.gov" ) - mockDefaultSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData("defaultSec", "dotgov@cisa.dhs.gov") + mockDefaultSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData("defaultSec", "help@get.gov") mockSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData("securityContact", "security@mail.gov") mockTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData("technicalContact", "tech@mail.gov") mockAdministrativeContact = InfoDomainWithContacts.dummyInfoContactResultData("adminContact", "admin@mail.gov") diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 27567a211..08d2687ca 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1014,20 +1014,21 @@ class TestRegistrantContacts(MockEppLib): test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__ test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=False).__dict__ # Separated for linter - disclose_email_field = {common.DiscloseField.EMAIL} + disclose_email_field = [common.DiscloseField.EMAIL] + self.maxDiff = None expected_disclose = { "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), "disclose": common.Disclose(flag=True, fields=disclose_email_field, types=None), - "email": "dotgov@cisa.dhs.gov", + "email": "help@get.gov", "extensions": [], "fax": None, "id": "ThIq2NcRIDN7PauO", "ident": None, "notify_email": None, "postal_info": common.PostalInfo( - name="Registry Customer Service", + name="CSD/CB – Attn: .gov TLD", addr=common.ContactAddr( - street=["4200 Wilson Blvd.", None, None], + street=["1110 N. Glebe Rd", None, None], city="Arlington", pc="22201", cc="US", @@ -1043,16 +1044,16 @@ class TestRegistrantContacts(MockEppLib): expected_not_disclose = { "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), "disclose": common.Disclose(flag=False, fields=disclose_email_field, types=None), - "email": "dotgov@cisa.dhs.gov", + "email": "help@get.gov", "extensions": [], "fax": None, "id": "ThrECENCHI76PGLh", "ident": None, "notify_email": None, "postal_info": common.PostalInfo( - name="Registry Customer Service", + name="CSD/CB – Attn: .gov TLD", addr=common.ContactAddr( - street=["4200 Wilson Blvd.", None, None], + street=["1110 N. Glebe Rd", None, None], city="Arlington", pc="22201", cc="US", @@ -1070,6 +1071,45 @@ class TestRegistrantContacts(MockEppLib): self.assertEqual(test_disclose, expected_disclose) self.assertEqual(test_not_disclose, expected_not_disclose) + @less_console_noise_decorator + def test_convert_public_contact_with_custom_fields(self): + """Test converting a contact with custom disclosure fields.""" + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + dummy_contact = domain.get_default_administrative_contact() + DF = common.DiscloseField + + # Create contact with multiple disclosure fields + result = self._convertPublicContactToEpp( + dummy_contact, + disclose_email=True, + disclose_fields=[DF.EMAIL, DF.VOICE, DF.ADDR], + disclose_types={DF.ADDR: "loc"}, + ) + self.assertEqual(result.disclose.flag, True) + self.assertEqual(result.disclose.fields, [DF.EMAIL, DF.VOICE, DF.ADDR]) + self.assertIn(DF.EMAIL, result.disclose.fields) + self.assertIn(DF.VOICE, result.disclose.fields) + self.assertIn(DF.ADDR, result.disclose.fields) + self.assertEqual(result.disclose.types, {DF.ADDR: "loc"}) + + @less_console_noise + def test_convert_public_contact_with_empty_fields(self): + """Test converting a contact with empty disclosure fields.""" + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + dummy_contact = domain.get_default_security_contact() + + # Create contact with empty fields list + result = self._convertPublicContactToEpp( + dummy_contact, + disclose_email=True, + disclose_fields=[] + ) + + # Verify disclosure settings + self.assertEqual(result.disclose.flag, True) + self.assertEqual(result.disclose.fields, []) + self.assertIsNone(result.disclose.types) + def test_not_disclosed_on_default_security_contact(self): """ Scenario: Registrant creates a new domain with no security email @@ -1101,7 +1141,9 @@ class TestRegistrantContacts(MockEppLib): expectedTechContact.domain = domain expectedTechContact.registry_id = "defaultTech" domain.technical_contact = expectedTechContact - expectedCreateCommand = self._convertPublicContactToEpp(expectedTechContact, disclose_email=False) + expectedCreateCommand = self._convertPublicContactToEpp( + expectedTechContact, disclose_email=False, disclose_fields=[] + ) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) # Confirm that we are getting a default email self.assertEqual(domain.technical_contact.email, expectedTechContact.email) From 1863b5548a31e717cf2dae82c3762e856c80f718 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:36:22 -0600 Subject: [PATCH 22/73] fix unit tests and lint --- src/registrar/tests/common.py | 4 +--- src/registrar/tests/test_models_domain.py | 18 +++++++----------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index cad041242..68fc71a33 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -1423,9 +1423,7 @@ class MockEppLib(TestCase): ], ) - mockDefaultTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData( - "defaultTech", "help@get.gov" - ) + mockDefaultTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData("defaultTech", "help@get.gov") mockDefaultSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData("defaultSec", "help@get.gov") mockSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData("securityContact", "security@mail.gov") mockTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData("technicalContact", "tech@mail.gov") diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 08d2687ca..d8650d062 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1077,11 +1077,11 @@ class TestRegistrantContacts(MockEppLib): domain, _ = Domain.objects.get_or_create(name="freeman.gov") dummy_contact = domain.get_default_administrative_contact() DF = common.DiscloseField - + # Create contact with multiple disclosure fields result = self._convertPublicContactToEpp( - dummy_contact, - disclose_email=True, + dummy_contact, + disclose_email=True, disclose_fields=[DF.EMAIL, DF.VOICE, DF.ADDR], disclose_types={DF.ADDR: "loc"}, ) @@ -1092,19 +1092,15 @@ class TestRegistrantContacts(MockEppLib): self.assertIn(DF.ADDR, result.disclose.fields) self.assertEqual(result.disclose.types, {DF.ADDR: "loc"}) - @less_console_noise + @less_console_noise_decorator def test_convert_public_contact_with_empty_fields(self): """Test converting a contact with empty disclosure fields.""" domain, _ = Domain.objects.get_or_create(name="freeman.gov") dummy_contact = domain.get_default_security_contact() - + # Create contact with empty fields list - result = self._convertPublicContactToEpp( - dummy_contact, - disclose_email=True, - disclose_fields=[] - ) - + result = self._convertPublicContactToEpp(dummy_contact, disclose_email=True, disclose_fields=[]) + # Verify disclosure settings self.assertEqual(result.disclose.flag, True) self.assertEqual(result.disclose.fields, []) From 768867ffb5d53e1e77911c56791d381a09339d25 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:57:33 -0600 Subject: [PATCH 23/73] Linter --- src/registrar/models/domain.py | 6 +++--- src/registrar/tests/test_models_domain.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 5397d1f7f..328ed7aee 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1682,13 +1682,13 @@ class Domain(TimeStampedModel, DomainHelper): case contact.ContactTypeChoices.SECURITY: hidden_security_emails = [email for email in DefaultEmail] disclose_fields = { - "fields": [DF.EMAIL], + "fields": {DF.EMAIL}, "flag": contact.email not in hidden_security_emails, } case contact.ContactTypeChoices.ADMINISTRATIVE: - disclose_fields = {"fields": [DF.EMAIL, DF.VOICE, DF.ADDR], "types": {DF.ADDR: "loc"}, "flag": True} + disclose_fields = {"fields": {DF.EMAIL, DF.VOICE, DF.ADDR}, "types": {DF.ADDR: "loc"}, "flag": True} logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_fields.get("flag")) - return epp.Disclose(**disclose_fields) + return epp.Disclose(**disclose_fields) # type: ignore def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore return epp.PostalInfo( # type: ignore diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index d8650d062..c5fabb6bc 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1014,7 +1014,7 @@ class TestRegistrantContacts(MockEppLib): test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__ test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=False).__dict__ # Separated for linter - disclose_email_field = [common.DiscloseField.EMAIL] + disclose_email_field = {common.DiscloseField.EMAIL} self.maxDiff = None expected_disclose = { "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), @@ -1082,11 +1082,11 @@ class TestRegistrantContacts(MockEppLib): result = self._convertPublicContactToEpp( dummy_contact, disclose_email=True, - disclose_fields=[DF.EMAIL, DF.VOICE, DF.ADDR], + disclose_fields={DF.EMAIL, DF.VOICE, DF.ADDR}, disclose_types={DF.ADDR: "loc"}, ) self.assertEqual(result.disclose.flag, True) - self.assertEqual(result.disclose.fields, [DF.EMAIL, DF.VOICE, DF.ADDR]) + self.assertEqual(result.disclose.fields, {DF.EMAIL, DF.VOICE, DF.ADDR}) self.assertIn(DF.EMAIL, result.disclose.fields) self.assertIn(DF.VOICE, result.disclose.fields) self.assertIn(DF.ADDR, result.disclose.fields) @@ -1099,7 +1099,7 @@ class TestRegistrantContacts(MockEppLib): dummy_contact = domain.get_default_security_contact() # Create contact with empty fields list - result = self._convertPublicContactToEpp(dummy_contact, disclose_email=True, disclose_fields=[]) + result = self._convertPublicContactToEpp(dummy_contact, disclose_email=True, disclose_fields={}) # Verify disclosure settings self.assertEqual(result.disclose.flag, True) From 3d72463806c9b5f065abb610a66a0bea856a04d3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Mar 2025 12:15:18 -0600 Subject: [PATCH 24/73] Update common.py --- src/registrar/tests/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 68fc71a33..101be51ef 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -1949,7 +1949,7 @@ class MockEppLib(TestCase): ): DF = common.DiscloseField if disclose_fields is None: - disclose_fields = [DF.EMAIL] + disclose_fields = {DF.EMAIL} di = common.Disclose(flag=disclose_email, fields=disclose_fields, types=disclose_types) From ca82f103d8e6f08e4032ed47255743ee3ae7d0b2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Mar 2025 12:54:46 -0600 Subject: [PATCH 25/73] Get better debug info --- src/registrar/models/domain.py | 26 ++++++------------ src/registrar/models/public_contact.py | 38 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 328ed7aee..410d763ba 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1350,20 +1350,6 @@ class Domain(TimeStampedModel, DomainHelper): ) return street_dict - def _request_contact_info(self, contact: PublicContact): - try: - req = commands.InfoContact(id=contact.registry_id) - return registry.send(req, cleaned=True).res_data[0] - except RegistryError as error: - logger.error( - "Registry threw error for contact id %s contact type is %s, error code is\n %s full error is %s", # noqa - contact.registry_id, - contact.contact_type, - error.code, - error, - ) - raise error - def generic_contact_getter(self, contact_type_choice: PublicContact.ContactTypeChoices) -> PublicContact | None: """Retrieves the desired PublicContact from the registry. This abstracts the caching and EPP retrieval for @@ -1686,7 +1672,11 @@ class Domain(TimeStampedModel, DomainHelper): "flag": contact.email not in hidden_security_emails, } case contact.ContactTypeChoices.ADMINISTRATIVE: - disclose_fields = {"fields": {DF.EMAIL, DF.VOICE, DF.ADDR}, "types": {DF.ADDR: "loc"}, "flag": True} + disclose_fields = { + "fields": {DF.EMAIL, DF.VOICE, DF.ADDR}, + "types": {DF.ADDR: "loc"}, + "flag": True, + } logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_fields.get("flag")) return epp.Disclose(**disclose_fields) # type: ignore @@ -1773,13 +1763,13 @@ class Domain(TimeStampedModel, DomainHelper): """Try to fetch info about a contact. Create it if it does not exist.""" logger.info("_get_or_create_contact() -> Fetching contact info") try: - return self._request_contact_info(contact) + return contact.get_contact_info_from_epp() except RegistryError as e: if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST: logger.info("_get_or_create_contact()-> contact doesn't exist so making it") contact.domain = self contact.save() # this will call the function based on type of contact - return self._request_contact_info(contact=contact) + return contact.get_contact_info_from_epp() else: logger.error( "Registry threw error for contact id %s" @@ -2230,7 +2220,7 @@ class Domain(TimeStampedModel, DomainHelper): contact_type=PublicContact.ContactTypeChoices.REGISTRANT, ) # Grabs the expanded contact - full_object = self._request_contact_info(contact) + full_object = contact.get_contact_info_from_epp() # Maps it to type PublicContact mapped_object = self.map_epp_contact_to_public_contact(full_object, contact.registry_id, contact.contact_type) return self._get_or_create_public_contact(mapped_object) diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index 6c123474d..7a091a0a0 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -1,3 +1,4 @@ +import logging from datetime import datetime from random import choices from string import ascii_uppercase, ascii_lowercase, digits @@ -7,7 +8,13 @@ from django.db import models from registrar.utility.enums import DefaultEmail from .utility.time_stamped_model import TimeStampedModel +from epplibwrapper import ( + CLIENT as registry, + commands, + RegistryError, +) +logger = logging.getLogger(__name__) def get_id(): """Generate a 16 character registry ID with a low probability of collision.""" @@ -87,6 +94,37 @@ class PublicContact(TimeStampedModel): ) pw = models.CharField(null=False, help_text="Contact's authorization code. 16 characters minimum.") + def get_contact_info_from_epp(self): + """Grabs the resultant contact information in epp for this public contact + by using the InfoContact command. + Returns `registry.send(req, cleaned=True).res_data[0]`.""" + try: + req = commands.InfoContact(id=self.registry_id) + return registry.send(req, cleaned=True).res_data[0] + except RegistryError as error: + logger.error( + "Registry threw error for contact id %s contact type is %s, error code is\n %s full error is %s", # noqa + self.registry_id, + self.contact_type, + error.code, + error, + ) + raise error + + # NOTE: REMOVE THIS BEFORE MERGING, USED FOR PR REVIEW ONLY + def debug_contact_info_epp(self): + results = self.get_contact_info_from_epp() + logger.info("Contact Info from EPP:") + logger.info("=====================") + for key, value in results.items(): + logger.info(f"{key}: {value}") + + logger.info("Contact Info on PublicContact model (compare against EPP):") + logger.info("=====================") + for key in results.keys(): + if value_on_model := getattr(self, key) is not None: + logger.info(f"{key}: {value_on_model}") + @classmethod def get_default_registrant(cls): return cls( From 5265ce80cc8956ce708d05a4ca1cc7c9064bbef1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:04:36 -0600 Subject: [PATCH 26/73] Update test_models_domain.py --- src/registrar/tests/test_models_domain.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index c5fabb6bc..e10b16843 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -990,18 +990,18 @@ class TestRegistrantContacts(MockEppLib): actual_contact = contact[1] if expected_contact.contact_type == PublicContact.ContactTypeChoices.SECURITY: expectedCreateCommand = self._convertPublicContactToEpp( - expected_contact, disclose_email=True, disclose_fields=["email"] + expected_contact, disclose_email=True, disclose_fields={"email"} ) elif expected_contact.contact_type == PublicContact.ContactTypeChoices.ADMINISTRATIVE: expectedCreateCommand = self._convertPublicContactToEpp( expected_contact, disclose_email=True, - disclose_fields=["email", "voice", "addr"], + disclose_fields={"email", "voice", "addr"}, disclose_types={"addr": "loc"}, ) else: expectedCreateCommand = self._convertPublicContactToEpp( - expected_contact, disclose_email=False, disclose_fields=[] + expected_contact, disclose_email=False, disclose_fields={} ) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) # The emails should match on both items @@ -1103,7 +1103,7 @@ class TestRegistrantContacts(MockEppLib): # Verify disclosure settings self.assertEqual(result.disclose.flag, True) - self.assertEqual(result.disclose.fields, []) + self.assertEqual(result.disclose.fields, {}) self.assertIsNone(result.disclose.types) def test_not_disclosed_on_default_security_contact(self): From eff89aae21cb1ba8446aaf25e76da3449f1869eb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:13:37 -0600 Subject: [PATCH 27/73] Update public_contact.py --- src/registrar/models/public_contact.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index 7a091a0a0..72abbb210 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -94,13 +94,14 @@ class PublicContact(TimeStampedModel): ) pw = models.CharField(null=False, help_text="Contact's authorization code. 16 characters minimum.") - def get_contact_info_from_epp(self): + def get_contact_info_from_epp(self, get_result_as_dict=False): """Grabs the resultant contact information in epp for this public contact by using the InfoContact command. Returns `registry.send(req, cleaned=True).res_data[0]`.""" try: req = commands.InfoContact(id=self.registry_id) - return registry.send(req, cleaned=True).res_data[0] + result = registry.send(req, cleaned=True).res_data[0] + return result if not get_result_as_dict else vars(result) except RegistryError as error: logger.error( "Registry threw error for contact id %s contact type is %s, error code is\n %s full error is %s", # noqa @@ -112,8 +113,9 @@ class PublicContact(TimeStampedModel): raise error # NOTE: REMOVE THIS BEFORE MERGING, USED FOR PR REVIEW ONLY - def debug_contact_info_epp(self): - results = self.get_contact_info_from_epp() + @classmethod + def debug_contact_info_epp(cls): + results = cls.get_contact_info_from_epp(get_result_as_dict=True) logger.info("Contact Info from EPP:") logger.info("=====================") for key, value in results.items(): From 2faeded1ea78f97cb827fc07f89b55190dcd9dc8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:14:45 -0600 Subject: [PATCH 28/73] Update public_contact.py --- src/registrar/models/public_contact.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index 72abbb210..a02c9b11a 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -97,7 +97,7 @@ class PublicContact(TimeStampedModel): def get_contact_info_from_epp(self, get_result_as_dict=False): """Grabs the resultant contact information in epp for this public contact by using the InfoContact command. - Returns `registry.send(req, cleaned=True).res_data[0]`.""" + Returns a commands.InfoContactResultData object, or a dict if get_result_as_dict is True.""" try: req = commands.InfoContact(id=self.registry_id) result = registry.send(req, cleaned=True).res_data[0] @@ -124,7 +124,7 @@ class PublicContact(TimeStampedModel): logger.info("Contact Info on PublicContact model (compare against EPP):") logger.info("=====================") for key in results.keys(): - if value_on_model := getattr(self, key) is not None: + if value_on_model := getattr(cls, key) is not None: logger.info(f"{key}: {value_on_model}") @classmethod From 8884baaa15794adc41b3be08711fb381c88a002b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:32:23 -0600 Subject: [PATCH 29/73] Update test_models_domain.py --- src/registrar/tests/test_models_domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index e10b16843..cde46c518 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1001,7 +1001,7 @@ class TestRegistrantContacts(MockEppLib): ) else: expectedCreateCommand = self._convertPublicContactToEpp( - expected_contact, disclose_email=False, disclose_fields={} + expected_contact, disclose_email=False, disclose_fields=[] ) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) # The emails should match on both items From 5e4879785013d81dc45de0dc797e30d7c40db6c8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:34:34 -0600 Subject: [PATCH 30/73] Final fix --- src/registrar/models/public_contact.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index a02c9b11a..e9fb89825 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -113,9 +113,8 @@ class PublicContact(TimeStampedModel): raise error # NOTE: REMOVE THIS BEFORE MERGING, USED FOR PR REVIEW ONLY - @classmethod - def debug_contact_info_epp(cls): - results = cls.get_contact_info_from_epp(get_result_as_dict=True) + def debug_contact_info_epp(self): + results = self.get_contact_info_from_epp(get_result_as_dict=True) logger.info("Contact Info from EPP:") logger.info("=====================") for key, value in results.items(): @@ -124,7 +123,7 @@ class PublicContact(TimeStampedModel): logger.info("Contact Info on PublicContact model (compare against EPP):") logger.info("=====================") for key in results.keys(): - if value_on_model := getattr(cls, key) is not None: + if value_on_model := getattr(self, key) is not None: logger.info(f"{key}: {value_on_model}") @classmethod From e16e2cac0f2888e74ca361cc154fb2ba0d4ba825 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:04:11 -0600 Subject: [PATCH 31/73] The final lintdown --- src/registrar/models/public_contact.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index e9fb89825..a4c2c10d0 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -16,6 +16,7 @@ from epplibwrapper import ( logger = logging.getLogger(__name__) + def get_id(): """Generate a 16 character registry ID with a low probability of collision.""" day = datetime.today().strftime("%A")[:2] @@ -111,7 +112,7 @@ class PublicContact(TimeStampedModel): error, ) raise error - + # NOTE: REMOVE THIS BEFORE MERGING, USED FOR PR REVIEW ONLY def debug_contact_info_epp(self): results = self.get_contact_info_from_epp(get_result_as_dict=True) @@ -119,12 +120,6 @@ class PublicContact(TimeStampedModel): logger.info("=====================") for key, value in results.items(): logger.info(f"{key}: {value}") - - logger.info("Contact Info on PublicContact model (compare against EPP):") - logger.info("=====================") - for key in results.keys(): - if value_on_model := getattr(self, key) is not None: - logger.info(f"{key}: {value_on_model}") @classmethod def get_default_registrant(cls): From a6be508b63cc830352b3f13b4fd3eb7c5e13eab3 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:24:21 -0700 Subject: [PATCH 32/73] Add test for portfolio organization update --- src/registrar/tests/test_views_portfolio.py | 45 ++++++++++++++++++++- src/registrar/utility/email.py | 1 - 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 2065c2d35..be2129c2f 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -2,7 +2,7 @@ from django.urls import reverse from api.tests.common import less_console_noise_decorator from registrar.config import settings from registrar.models import Portfolio, SeniorOfficial -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, patch, ANY from django_webtest import WebTest # type: ignore from django.core.handlers.wsgi import WSGIRequest from registrar.models import ( @@ -398,11 +398,52 @@ class TestPortfolio(WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) success_result_page = portfolio_org_name_page.form.submit() - self.assertEqual(success_result_page.status_code, 200) + self.assertEqual(success_result_page.status_code, 302) self.assertContains(success_result_page, "6 Downing st") self.assertContains(success_result_page, "London") + @boto3_mocking.patching + @less_console_noise_decorator + @patch("registrar.utility.email_invitations.send_templated_email") + def test_org_update_sends_admin_email(self, mock_send_templated_email): + """Updating organization information emails organization admin.""" + with override_flag("organization_feature", active=True): + self.app.set_user(self.user.username) + self.admin, _ = User.objects.get_or_create(email="mayor@igorville.com", first_name="Hello", last_name="World") + + portfolio_additional_permissions = [ + UserPortfolioPermissionChoices.VIEW_PORTFOLIO, + UserPortfolioPermissionChoices.EDIT_PORTFOLIO, + ] + portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create( + user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions + ) + portfolio_permission_admin, _ = UserPortfolioPermission.objects.get_or_create( + user=self.admin, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions, + roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + + self.portfolio.address_line1 = "1600 Penn Ave" + self.portfolio.save() + portfolio_org_name_page = self.app.get(reverse("organization")) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + portfolio_org_name_page.form["address_line1"] = "6 Downing st" + portfolio_org_name_page.form["city"] = "London" + portfolio_org_name_page.form["zipcode"] = "11111" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_result_page = portfolio_org_name_page.form.submit() + self.assertEqual(success_result_page.status_code, 302) + + # Verify that the notification emails were sent to domain manager + mock_send_templated_email.assert_called_once_with( + "emails/portfolio_org_update_notification.txt", + "emails/portfolio_org_update_notification_subject.txt", + to_address=self.admin.email, + context=ANY, + ) + @less_console_noise_decorator def test_portfolio_in_session_when_organization_feature_active(self): """When organization_feature flag is true and user has a portfolio, diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 94e87a96b..31b8e9462 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -48,7 +48,6 @@ def send_templated_email( # noqa SES client could not be accessed No valid recipient addresses are provided """ - if context is None: context = {} From 4b0bcfc84c8f9c49174425120b6b17c51ed48a5d Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:25:30 -0700 Subject: [PATCH 33/73] Readd whitespace --- src/registrar/utility/email.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 31b8e9462..94e87a96b 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -48,6 +48,7 @@ def send_templated_email( # noqa SES client could not be accessed No valid recipient addresses are provided """ + if context is None: context = {} From 38133552c1d94db98da5332f9850a563b5a9f04d Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:34:36 -0700 Subject: [PATCH 34/73] Revert test --- src/registrar/tests/test_views_portfolio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index be2129c2f..3e1ca4198 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -398,7 +398,7 @@ class TestPortfolio(WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) success_result_page = portfolio_org_name_page.form.submit() - self.assertEqual(success_result_page.status_code, 302) + self.assertEqual(success_result_page.status_code, 200) self.assertContains(success_result_page, "6 Downing st") self.assertContains(success_result_page, "London") From 8be43207e4db237a61a028b58de83291bf85b986 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:37:02 -0700 Subject: [PATCH 35/73] Fix incomplete test --- src/registrar/tests/test_views_portfolio.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 3e1ca4198..09654edb2 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -398,11 +398,18 @@ class TestPortfolio(WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) success_result_page = portfolio_org_name_page.form.submit() + # Form will not validate with missing required field (zipcode) self.assertEqual(success_result_page.status_code, 200) self.assertContains(success_result_page, "6 Downing st") self.assertContains(success_result_page, "London") + #Form validates and redirects with all required fields + portfolio_org_name_page.form["zipcode"] = "11111" + success_result_page = portfolio_org_name_page.form.submit() + self.assertEqual(success_result_page.status_code, 302) + self.assertContains(success_result_page, "11111") + @boto3_mocking.patching @less_console_noise_decorator @patch("registrar.utility.email_invitations.send_templated_email") From 9214c4237b1b535315b8b0caca35462acaf29018 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:39:04 -0700 Subject: [PATCH 36/73] Fix linting --- src/registrar/tests/test_views_portfolio.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 09654edb2..2d8810aa1 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -404,7 +404,7 @@ class TestPortfolio(WebTest): self.assertContains(success_result_page, "6 Downing st") self.assertContains(success_result_page, "London") - #Form validates and redirects with all required fields + # Form validates and redirects with all required fields portfolio_org_name_page.form["zipcode"] = "11111" success_result_page = portfolio_org_name_page.form.submit() self.assertEqual(success_result_page.status_code, 302) @@ -417,8 +417,10 @@ class TestPortfolio(WebTest): """Updating organization information emails organization admin.""" with override_flag("organization_feature", active=True): self.app.set_user(self.user.username) - self.admin, _ = User.objects.get_or_create(email="mayor@igorville.com", first_name="Hello", last_name="World") - + self.admin, _ = User.objects.get_or_create( + email="mayor@igorville.com", first_name="Hello", last_name="World" + ) + portfolio_additional_permissions = [ UserPortfolioPermissionChoices.VIEW_PORTFOLIO, UserPortfolioPermissionChoices.EDIT_PORTFOLIO, @@ -427,8 +429,10 @@ class TestPortfolio(WebTest): user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions ) portfolio_permission_admin, _ = UserPortfolioPermission.objects.get_or_create( - user=self.admin, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions, - roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + user=self.admin, + portfolio=self.portfolio, + additional_permissions=portfolio_additional_permissions, + roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], ) self.portfolio.address_line1 = "1600 Penn Ave" @@ -442,7 +446,7 @@ class TestPortfolio(WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) success_result_page = portfolio_org_name_page.form.submit() self.assertEqual(success_result_page.status_code, 302) - + # Verify that the notification emails were sent to domain manager mock_send_templated_email.assert_called_once_with( "emails/portfolio_org_update_notification.txt", From 6ce0add0fa09730da80f7b1bc8478fe985a16491 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:45:51 -0700 Subject: [PATCH 37/73] Clean test suite --- src/registrar/tests/test_views_portfolio.py | 27 +++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 2d8810aa1..5d6e0de17 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -376,8 +376,8 @@ class TestPortfolio(WebTest): self.assertContains(page, "Non-Federal Agency") @less_console_noise_decorator - def test_domain_org_name_address_form(self): - """Submitting changes works on the org name address page.""" + def test_org_form_invalid_update(self): + """Organization form will not redirect on invalid formsets.""" with override_flag("organization_feature", active=True): self.app.set_user(self.user.username) portfolio_additional_permissions = [ @@ -404,10 +404,33 @@ class TestPortfolio(WebTest): self.assertContains(success_result_page, "6 Downing st") self.assertContains(success_result_page, "London") + @less_console_noise_decorator + def test_org_form_invalid_update(self): + """Organization form will redirect on invalid formsets.""" + with override_flag("organization_feature", active=True): + self.app.set_user(self.user.username) + portfolio_additional_permissions = [ + UserPortfolioPermissionChoices.VIEW_PORTFOLIO, + UserPortfolioPermissionChoices.EDIT_PORTFOLIO, + ] + portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create( + user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions + ) + + self.portfolio.address_line1 = "1600 Penn Ave" + self.portfolio.save() + portfolio_org_name_page = self.app.get(reverse("organization")) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + # Form validates and redirects with all required fields + portfolio_org_name_page.form["address_line1"] = "6 Downing st" + portfolio_org_name_page.form["city"] = "London" portfolio_org_name_page.form["zipcode"] = "11111" + success_result_page = portfolio_org_name_page.form.submit() self.assertEqual(success_result_page.status_code, 302) + self.assertContains(success_result_page, "6 Downing st") + self.assertContains(success_result_page, "London") self.assertContains(success_result_page, "11111") @boto3_mocking.patching From ecd2018b9328f1ce35ea3400efa9542042ddfae9 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:46:06 -0700 Subject: [PATCH 38/73] Fix linting --- src/registrar/tests/test_views_portfolio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 5d6e0de17..0a7569bbf 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -406,8 +406,8 @@ class TestPortfolio(WebTest): @less_console_noise_decorator def test_org_form_invalid_update(self): - """Organization form will redirect on invalid formsets.""" - with override_flag("organization_feature", active=True): + """Organization form will redirect on invalid formsets.""" + with override_flag("organization_feature", active=True): self.app.set_user(self.user.username) portfolio_additional_permissions = [ UserPortfolioPermissionChoices.VIEW_PORTFOLIO, From ff1fb3982c08d64bd923d07602c15898588f65cf Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:48:14 -0700 Subject: [PATCH 39/73] Update test names --- src/registrar/tests/test_views_portfolio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 0a7569bbf..357b63594 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -405,8 +405,8 @@ class TestPortfolio(WebTest): self.assertContains(success_result_page, "London") @less_console_noise_decorator - def test_org_form_invalid_update(self): - """Organization form will redirect on invalid formsets.""" + def test_org_form_valid_update(self): + """Organization form will redirect on valid formsets.""" with override_flag("organization_feature", active=True): self.app.set_user(self.user.username) portfolio_additional_permissions = [ From 106976552968d391dfa099a4101a936097c2bf1c Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:51:30 -0700 Subject: [PATCH 40/73] Fix unit test --- src/registrar/tests/test_views_portfolio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 357b63594..dfc36dd2b 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -427,6 +427,7 @@ class TestPortfolio(WebTest): portfolio_org_name_page.form["city"] = "London" portfolio_org_name_page.form["zipcode"] = "11111" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) success_result_page = portfolio_org_name_page.form.submit() self.assertEqual(success_result_page.status_code, 302) self.assertContains(success_result_page, "6 Downing st") From 8be4b6d1b3fc73ac2a90ba6b433ce226268b9c0a Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:49:45 -0700 Subject: [PATCH 41/73] Update unit test --- src/registrar/tests/test_views_portfolio.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index dfc36dd2b..f63275aa9 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -430,9 +430,6 @@ class TestPortfolio(WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) success_result_page = portfolio_org_name_page.form.submit() self.assertEqual(success_result_page.status_code, 302) - self.assertContains(success_result_page, "6 Downing st") - self.assertContains(success_result_page, "London") - self.assertContains(success_result_page, "11111") @boto3_mocking.patching @less_console_noise_decorator From a92e08061229b8ab77d6c8c322692785cfc92347 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 14 Mar 2025 13:36:11 -0600 Subject: [PATCH 42/73] Update src/registrar/models/domain.py --- src/registrar/models/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 410d763ba..f49e9a20c 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1663,7 +1663,7 @@ class Domain(TimeStampedModel, DomainHelper): .disclose= on the command before sending. if item is security email then make sure email is visible""" DF = epp.DiscloseField - disclose_fields = {"fields": [], "flag": False} + disclose_fields = {"fields": {}, "flag": False} match contact.contact_type: case contact.ContactTypeChoices.SECURITY: hidden_security_emails = [email for email in DefaultEmail] From 182f0ed693e546570c8284093c26b163ad36b402 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 14 Mar 2025 14:38:03 -0600 Subject: [PATCH 43/73] code cleanup --- src/registrar/models/domain.py | 50 +++++++++++++------ src/registrar/models/public_contact.py | 20 +------- .../templates/domain_security_email.html | 2 +- src/registrar/tests/test_models_domain.py | 4 +- src/registrar/views/domain.py | 7 +++ 5 files changed, 45 insertions(+), 38 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index f49e9a20c..8d560aadb 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1350,6 +1350,24 @@ class Domain(TimeStampedModel, DomainHelper): ) return street_dict + def _request_contact_info(self, contact: PublicContact, get_result_as_dict=False): + """Grabs the resultant contact information in epp for this public contact + by using the InfoContact command. + Returns a commands.InfoContactResultData object, or a dict if get_result_as_dict is True.""" + try: + req = commands.InfoContact(id=contact.registry_id) + result = registry.send(req, cleaned=True).res_data[0] + return result if not get_result_as_dict else vars(result) + except RegistryError as error: + logger.error( + "Registry threw error for contact id %s contact type is %s, error code is\n %s full error is %s", # noqa + contact.registry_id, + contact.contact_type, + error.code, + error, + ) + raise error + def generic_contact_getter(self, contact_type_choice: PublicContact.ContactTypeChoices) -> PublicContact | None: """Retrieves the desired PublicContact from the registry. This abstracts the caching and EPP retrieval for @@ -1664,19 +1682,19 @@ class Domain(TimeStampedModel, DomainHelper): if item is security email then make sure email is visible""" DF = epp.DiscloseField disclose_fields = {"fields": {}, "flag": False} - match contact.contact_type: - case contact.ContactTypeChoices.SECURITY: - hidden_security_emails = [email for email in DefaultEmail] - disclose_fields = { - "fields": {DF.EMAIL}, - "flag": contact.email not in hidden_security_emails, - } - case contact.ContactTypeChoices.ADMINISTRATIVE: - disclose_fields = { - "fields": {DF.EMAIL, DF.VOICE, DF.ADDR}, - "types": {DF.ADDR: "loc"}, - "flag": True, - } + if contact.contact_type == contact.ContactTypeChoices.SECURITY: + hidden_security_emails = [email for email in DefaultEmail] + disclose_fields = { + "fields": {DF.EMAIL}, + "flag": contact.email not in hidden_security_emails, + } + elif contact.contact_type == contact.ContactTypeChoices.ADMINISTRATIVE: + disclose_fields = { + "fields": {DF.EMAIL, DF.VOICE, DF.ADDR}, + "types": {DF.ADDR: "loc"}, + "flag": True, + } + logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_fields.get("flag")) return epp.Disclose(**disclose_fields) # type: ignore @@ -1763,13 +1781,13 @@ class Domain(TimeStampedModel, DomainHelper): """Try to fetch info about a contact. Create it if it does not exist.""" logger.info("_get_or_create_contact() -> Fetching contact info") try: - return contact.get_contact_info_from_epp() + return self._request_contact_info(contact) except RegistryError as e: if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST: logger.info("_get_or_create_contact()-> contact doesn't exist so making it") contact.domain = self contact.save() # this will call the function based on type of contact - return contact.get_contact_info_from_epp() + return self._request_contact_info(contact) else: logger.error( "Registry threw error for contact id %s" @@ -2220,7 +2238,7 @@ class Domain(TimeStampedModel, DomainHelper): contact_type=PublicContact.ContactTypeChoices.REGISTRANT, ) # Grabs the expanded contact - full_object = contact.get_contact_info_from_epp() + full_object = self._request_contact_info(contact) # Maps it to type PublicContact mapped_object = self.map_epp_contact_to_public_contact(full_object, contact.registry_id, contact.contact_type) return self._get_or_create_public_contact(mapped_object) diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index a4c2c10d0..03fbe0e2b 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -95,27 +95,9 @@ class PublicContact(TimeStampedModel): ) pw = models.CharField(null=False, help_text="Contact's authorization code. 16 characters minimum.") - def get_contact_info_from_epp(self, get_result_as_dict=False): - """Grabs the resultant contact information in epp for this public contact - by using the InfoContact command. - Returns a commands.InfoContactResultData object, or a dict if get_result_as_dict is True.""" - try: - req = commands.InfoContact(id=self.registry_id) - result = registry.send(req, cleaned=True).res_data[0] - return result if not get_result_as_dict else vars(result) - except RegistryError as error: - logger.error( - "Registry threw error for contact id %s contact type is %s, error code is\n %s full error is %s", # noqa - self.registry_id, - self.contact_type, - error.code, - error, - ) - raise error - # NOTE: REMOVE THIS BEFORE MERGING, USED FOR PR REVIEW ONLY def debug_contact_info_epp(self): - results = self.get_contact_info_from_epp(get_result_as_dict=True) + results = self.domain._request_contact_info(self, get_result_as_dict=True) logger.info("Contact Info from EPP:") logger.info("=====================") for key, value in results.items(): diff --git a/src/registrar/templates/domain_security_email.html b/src/registrar/templates/domain_security_email.html index 662eec152..23d0c509e 100644 --- a/src/registrar/templates/domain_security_email.html +++ b/src/registrar/templates/domain_security_email.html @@ -40,7 +40,7 @@ + >{% if form.security_email.value is None or form.security_email.value|lower in default_emails %}Add security email{% else %}Save{% endif %} {% endblock %} {# domain_content #} diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index cde46c518..2b7bff831 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1001,7 +1001,7 @@ class TestRegistrantContacts(MockEppLib): ) else: expectedCreateCommand = self._convertPublicContactToEpp( - expected_contact, disclose_email=False, disclose_fields=[] + expected_contact, disclose_email=False, disclose_fields={} ) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) # The emails should match on both items @@ -1138,7 +1138,7 @@ class TestRegistrantContacts(MockEppLib): expectedTechContact.registry_id = "defaultTech" domain.technical_contact = expectedTechContact expectedCreateCommand = self._convertPublicContactToEpp( - expectedTechContact, disclose_email=False, disclose_fields=[] + expectedTechContact, disclose_email=False, disclose_fields={} ) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) # Confirm that we are getting a default email diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 0b13dda69..ac858da01 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -1172,6 +1172,13 @@ class DomainSecurityEmailView(DomainFormBaseView): return initial initial["security_email"] = security_contact.email return initial + + def get_context_data(self, **kwargs): + """Adds the default emails list to the context""" + context = super().get_context_data(**kwargs) + # use "formset" instead of "form" for the key + context["default_emails"] = [email for email in DefaultEmail] + return context def get_success_url(self): """Redirect to the security email page for the domain.""" From 0e2dc2b4f26ed240003108fca4fd75a8d24f1380 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 14 Mar 2025 14:42:00 -0600 Subject: [PATCH 44/73] lint --- src/registrar/models/domain.py | 4 ++-- src/registrar/models/public_contact.py | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 8d560aadb..badb95740 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1367,7 +1367,7 @@ class Domain(TimeStampedModel, DomainHelper): error, ) raise error - + def generic_contact_getter(self, contact_type_choice: PublicContact.ContactTypeChoices) -> PublicContact | None: """Retrieves the desired PublicContact from the registry. This abstracts the caching and EPP retrieval for @@ -1781,7 +1781,7 @@ class Domain(TimeStampedModel, DomainHelper): """Try to fetch info about a contact. Create it if it does not exist.""" logger.info("_get_or_create_contact() -> Fetching contact info") try: - return self._request_contact_info(contact) + return self._request_contact_info(contact=contact) except RegistryError as e: if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST: logger.info("_get_or_create_contact()-> contact doesn't exist so making it") diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index 03fbe0e2b..fe12b22c5 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -8,11 +8,7 @@ from django.db import models from registrar.utility.enums import DefaultEmail from .utility.time_stamped_model import TimeStampedModel -from epplibwrapper import ( - CLIENT as registry, - commands, - RegistryError, -) + logger = logging.getLogger(__name__) @@ -95,7 +91,6 @@ class PublicContact(TimeStampedModel): ) pw = models.CharField(null=False, help_text="Contact's authorization code. 16 characters minimum.") - # NOTE: REMOVE THIS BEFORE MERGING, USED FOR PR REVIEW ONLY def debug_contact_info_epp(self): results = self.domain._request_contact_info(self, get_result_as_dict=True) logger.info("Contact Info from EPP:") From e8ac747ddcb93f65ce85008713d3b04a3e4886f1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 14 Mar 2025 14:43:48 -0600 Subject: [PATCH 45/73] Update public_contact.py --- src/registrar/models/public_contact.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index fe12b22c5..d3167960d 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -91,7 +91,8 @@ class PublicContact(TimeStampedModel): ) pw = models.CharField(null=False, help_text="Contact's authorization code. 16 characters minimum.") - def debug_contact_info_epp(self): + def print_contact_info_epp(self): + """Prints registry data for this PublicContact for easier debugging""" results = self.domain._request_contact_info(self, get_result_as_dict=True) logger.info("Contact Info from EPP:") logger.info("=====================") From d9a1cb2db3d5f9b28f6259bd6a93f799fccac791 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 14 Mar 2025 14:45:49 -0600 Subject: [PATCH 46/73] Update src/registrar/views/domain.py --- src/registrar/views/domain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index ac858da01..254fdce27 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -1176,7 +1176,6 @@ class DomainSecurityEmailView(DomainFormBaseView): def get_context_data(self, **kwargs): """Adds the default emails list to the context""" context = super().get_context_data(**kwargs) - # use "formset" instead of "form" for the key context["default_emails"] = [email for email in DefaultEmail] return context From 24eadb8135319eba78e97cb03de680df41bc6ec8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 14 Mar 2025 14:48:04 -0600 Subject: [PATCH 47/73] Update domain.py --- src/registrar/models/domain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index badb95740..47236188b 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1781,13 +1781,13 @@ class Domain(TimeStampedModel, DomainHelper): """Try to fetch info about a contact. Create it if it does not exist.""" logger.info("_get_or_create_contact() -> Fetching contact info") try: - return self._request_contact_info(contact=contact) + return self._request_contact_info(contact) except RegistryError as e: if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST: logger.info("_get_or_create_contact()-> contact doesn't exist so making it") contact.domain = self contact.save() # this will call the function based on type of contact - return self._request_contact_info(contact) + return self._request_contact_info(contact=contact) else: logger.error( "Registry threw error for contact id %s" From 297ad843f4a8b9f8666cb3e7abddce835a3b4ab5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 14 Mar 2025 14:50:03 -0600 Subject: [PATCH 48/73] Update test_models_domain.py --- src/registrar/tests/test_models_domain.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 2b7bff831..400924355 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1087,9 +1087,6 @@ class TestRegistrantContacts(MockEppLib): ) self.assertEqual(result.disclose.flag, True) self.assertEqual(result.disclose.fields, {DF.EMAIL, DF.VOICE, DF.ADDR}) - self.assertIn(DF.EMAIL, result.disclose.fields) - self.assertIn(DF.VOICE, result.disclose.fields) - self.assertIn(DF.ADDR, result.disclose.fields) self.assertEqual(result.disclose.types, {DF.ADDR: "loc"}) @less_console_noise_decorator From 07260cc8dda1b8d2588eec11c464595da7e058a9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:30:21 -0600 Subject: [PATCH 49/73] lint --- src/registrar/templates/domain_security_email.html | 2 +- src/registrar/views/domain.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/registrar/templates/domain_security_email.html b/src/registrar/templates/domain_security_email.html index 23d0c509e..66c9ae255 100644 --- a/src/registrar/templates/domain_security_email.html +++ b/src/registrar/templates/domain_security_email.html @@ -40,7 +40,7 @@ + >{% if form.security_email.value is None or form.security_email.value == "dotgov@cisa.dhs.gov" or form.security_email.value == "registrar@dotgov.gov" or form.security_email.value == "help@get.gov"%}Add security email{% else %}Save{% endif %} {% endblock %} {# domain_content #} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 254fdce27..0b13dda69 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -1172,12 +1172,6 @@ class DomainSecurityEmailView(DomainFormBaseView): return initial initial["security_email"] = security_contact.email return initial - - def get_context_data(self, **kwargs): - """Adds the default emails list to the context""" - context = super().get_context_data(**kwargs) - context["default_emails"] = [email for email in DefaultEmail] - return context def get_success_url(self): """Redirect to the security email page for the domain.""" From d2e1035ffc7f1b0bf0627c3b6ed37ce3aacf56b6 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 17 Mar 2025 13:31:57 -0700 Subject: [PATCH 50/73] Separate unit tests into email and view tests --- src/registrar/tests/test_email_invitations.py | 53 ++++++++++++++++++- src/registrar/utility/email_invitations.py | 2 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_email_invitations.py b/src/registrar/tests/test_email_invitations.py index 2331d35e8..949d06831 100644 --- a/src/registrar/tests/test_email_invitations.py +++ b/src/registrar/tests/test_email_invitations.py @@ -1,5 +1,5 @@ import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import patch, MagicMock, ANY from datetime import date from registrar.models.domain import Domain from registrar.models.portfolio import Portfolio @@ -20,6 +20,7 @@ from registrar.utility.email_invitations import ( send_portfolio_invitation_remove_email, send_portfolio_member_permission_remove_email, send_portfolio_member_permission_update_email, + send_portfolio_organization_update_email, ) from api.tests.common import less_console_noise_decorator @@ -1112,3 +1113,53 @@ class TestSendPortfolioInvitationRemoveEmail(unittest.TestCase): # Assertions mock_logger.warning.assert_not_called() # Function should fail before logging email failure + + +class TestSendPortfolioOrganizationUpdateEmail(unittest.TestCase): + """Unit tests for send_portfolio_organization_update_email function.""" + def setUp(self): + """Set up test data.""" + self.email = "removed.admin@example.com" + self.requestor_email = "requestor@example.com" + self.portfolio = MagicMock(spec=Portfolio, name="Portfolio") + self.portfolio.organization_name = "Test Organization" + + # Mock portfolio admin users + self.admin_user1 = MagicMock(spec=User) + self.admin_user1.email = "admin1@example.com" + + self.admin_user2 = MagicMock(spec=User) + self.admin_user2.email = "admin2@example.com" + + self.portfolio_admin1 = MagicMock(spec=UserPortfolioPermission) + self.portfolio_admin1.user = self.admin_user1 + self.portfolio_admin1.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + + self.portfolio_admin2 = MagicMock(spec=UserPortfolioPermission) + self.portfolio_admin2.user = self.admin_user2 + self.portfolio_admin2.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + + @patch("registrar.utility.email_invitations.send_templated_email") + @patch("registrar.utility.email_invitations.UserPortfolioPermission.objects.filter") + def test_send_portfolio_organization_update_email(self, mock_filter, mock_send_templated_email): + """Test send_portfolio_organization_update_email sends templated email.""" + # Mock data + editor = self.admin_user1 + updated_page = "Organization" + + mock_filter.return_value.exclude.return_value = [self.portfolio_admin2] + mock_send_templated_email.return_value = None # No exception means success + + # Call function + result = send_portfolio_organization_update_email(editor, self.portfolio, updated_page) + + mock_filter.assert_called_once_with( + portfolio=self.portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + mock_send_templated_email.assert_any_call( + "emails/portfolio_org_update_notification.txt", + "emails/portfolio_org_update_notification_subject.txt", + to_address=self.admin_user2.email, + context=ANY, + ) + self.assertTrue(result) \ No newline at end of file diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index f98ef5459..72f6c7391 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -264,7 +264,7 @@ def send_portfolio_organization_update_email(editor, portfolio, updated_page): "editor": editor, "portfolio_admin": user, "date": date.today(), - "updated_info": "Organization", + "updated_info": updated_page, }, ) except EmailSendingError: From 3dac65df79f1db5e1ce423df4a26a3f43526b9df Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 17 Mar 2025 13:37:58 -0700 Subject: [PATCH 51/73] Refactor portfolio view tests --- src/registrar/tests/test_views_portfolio.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 8d539e5ad..053c9a566 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -433,8 +433,8 @@ class TestPortfolio(WebTest): @boto3_mocking.patching @less_console_noise_decorator - @patch("registrar.utility.email_invitations.send_templated_email") - def test_org_update_sends_admin_email(self, mock_send_templated_email): + @patch("registrar.views.portfolios.send_portfolio_organization_update_email") + def test_org_update_sends_admin_email(self, mock_send_organization_update_email): """Updating organization information emails organization admin.""" with override_flag("organization_feature", active=True): self.app.set_user(self.user.username) @@ -469,12 +469,7 @@ class TestPortfolio(WebTest): self.assertEqual(success_result_page.status_code, 302) # Verify that the notification emails were sent to domain manager - mock_send_templated_email.assert_called_once_with( - "emails/portfolio_org_update_notification.txt", - "emails/portfolio_org_update_notification_subject.txt", - to_address=self.admin.email, - context=ANY, - ) + mock_send_organization_update_email.assert_called_once() @less_console_noise_decorator def test_portfolio_in_session_when_organization_feature_active(self): From 0231877cd9af541c14d61b8f4e28144a8072e95a Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:40:57 -0700 Subject: [PATCH 52/73] Rename email methods --- src/registrar/tests/test_email_invitations.py | 10 +++++----- src/registrar/tests/test_views_portfolio.py | 2 +- src/registrar/utility/email_invitations.py | 2 +- src/registrar/views/portfolios.py | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/registrar/tests/test_email_invitations.py b/src/registrar/tests/test_email_invitations.py index 949d06831..a36c5ab9f 100644 --- a/src/registrar/tests/test_email_invitations.py +++ b/src/registrar/tests/test_email_invitations.py @@ -20,7 +20,7 @@ from registrar.utility.email_invitations import ( send_portfolio_invitation_remove_email, send_portfolio_member_permission_remove_email, send_portfolio_member_permission_update_email, - send_portfolio_organization_update_email, + send_portfolio_update_emails_to_portfolio_admins, ) from api.tests.common import less_console_noise_decorator @@ -1116,7 +1116,7 @@ class TestSendPortfolioInvitationRemoveEmail(unittest.TestCase): class TestSendPortfolioOrganizationUpdateEmail(unittest.TestCase): - """Unit tests for send_portfolio_organization_update_email function.""" + """Unit tests for send_portfolio_update_emails_to_portfolio_admins function.""" def setUp(self): """Set up test data.""" self.email = "removed.admin@example.com" @@ -1141,8 +1141,8 @@ class TestSendPortfolioOrganizationUpdateEmail(unittest.TestCase): @patch("registrar.utility.email_invitations.send_templated_email") @patch("registrar.utility.email_invitations.UserPortfolioPermission.objects.filter") - def test_send_portfolio_organization_update_email(self, mock_filter, mock_send_templated_email): - """Test send_portfolio_organization_update_email sends templated email.""" + def test_send_portfolio_update_emails_to_portfolio_admins(self, mock_filter, mock_send_templated_email): + """Test send_portfolio_update_emails_to_portfolio_admins sends templated email.""" # Mock data editor = self.admin_user1 updated_page = "Organization" @@ -1151,7 +1151,7 @@ class TestSendPortfolioOrganizationUpdateEmail(unittest.TestCase): mock_send_templated_email.return_value = None # No exception means success # Call function - result = send_portfolio_organization_update_email(editor, self.portfolio, updated_page) + result = send_portfolio_update_emails_to_portfolio_admins(editor, self.portfolio, updated_page) mock_filter.assert_called_once_with( portfolio=self.portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 053c9a566..4199b53ea 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -433,7 +433,7 @@ class TestPortfolio(WebTest): @boto3_mocking.patching @less_console_noise_decorator - @patch("registrar.views.portfolios.send_portfolio_organization_update_email") + @patch("registrar.views.portfolios.send_portfolio_update_emails_to_portfolio_admins") def test_org_update_sends_admin_email(self, mock_send_organization_update_email): """Updating organization information emails organization admin.""" with override_flag("organization_feature", active=True): diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index 72f6c7391..6f3a8151b 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -227,7 +227,7 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_i return all_admin_emails_sent -def send_portfolio_organization_update_email(editor, portfolio, updated_page): +def send_portfolio_update_emails_to_portfolio_admins(editor, portfolio, updated_page): """ Sends an email notification to all portfolio admin when portfolio organization is updated. diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index e7ae69818..a190f9fb2 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -35,7 +35,7 @@ from registrar.utility.email_invitations import ( send_portfolio_invitation_remove_email, send_portfolio_member_permission_remove_email, send_portfolio_member_permission_update_email, - send_portfolio_organization_update_email, + send_portfolio_update_emails_to_portfolio_admins, ) from registrar.utility.errors import MissingEmailError from registrar.utility.enums import DefaultUserValues @@ -853,7 +853,7 @@ class PortfolioOrganizationView(DetailView, FormMixin): if form.is_valid(): user = request.user try: - if not send_portfolio_organization_update_email( + if not send_portfolio_update_emails_to_portfolio_admins( editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Organization" ): messages.warning(self.request, "Could not send email notification to all organization admins.") @@ -924,7 +924,7 @@ class PortfolioSeniorOfficialView(DetailView, FormMixin): if form.is_valid(): user = request.user try: - if not send_portfolio_organization_update_email( + if not send_portfolio_update_emails_to_portfolio_admins( editor=user, portfolio=self.request.session.get("portfolio"), updated_page="Senior Official" ): messages.warning(self.request, "Could not send email notification to all organization admins.") From 19fdcc4c915874b0fb2ad2bedc6f245b37c9e467 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:48:09 -0700 Subject: [PATCH 53/73] Fix linting --- src/registrar/tests/test_email_invitations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_email_invitations.py b/src/registrar/tests/test_email_invitations.py index a36c5ab9f..550ba12cf 100644 --- a/src/registrar/tests/test_email_invitations.py +++ b/src/registrar/tests/test_email_invitations.py @@ -1117,6 +1117,7 @@ class TestSendPortfolioInvitationRemoveEmail(unittest.TestCase): class TestSendPortfolioOrganizationUpdateEmail(unittest.TestCase): """Unit tests for send_portfolio_update_emails_to_portfolio_admins function.""" + def setUp(self): """Set up test data.""" self.email = "removed.admin@example.com" @@ -1162,4 +1163,4 @@ class TestSendPortfolioOrganizationUpdateEmail(unittest.TestCase): to_address=self.admin_user2.email, context=ANY, ) - self.assertTrue(result) \ No newline at end of file + self.assertTrue(result) From e7234c4ba4cacccc83ec7c59644593591d0aa62c Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:48:22 -0700 Subject: [PATCH 54/73] Email org editor when org updated --- src/registrar/tests/test_email_invitations.py | 2 +- src/registrar/utility/email_invitations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_email_invitations.py b/src/registrar/tests/test_email_invitations.py index 550ba12cf..df99ae081 100644 --- a/src/registrar/tests/test_email_invitations.py +++ b/src/registrar/tests/test_email_invitations.py @@ -1148,7 +1148,7 @@ class TestSendPortfolioOrganizationUpdateEmail(unittest.TestCase): editor = self.admin_user1 updated_page = "Organization" - mock_filter.return_value.exclude.return_value = [self.portfolio_admin2] + mock_filter.return_value = [self.portfolio_admin1, self.portfolio_admin2] mock_send_templated_email.return_value = None # No exception means success # Call function diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index 6f3a8151b..8e3d98418 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -249,7 +249,7 @@ def send_portfolio_update_emails_to_portfolio_admins(editor, portfolio, updated_ # Get each portfolio admin from list user_portfolio_permissions = UserPortfolioPermission.objects.filter( portfolio=portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] - ).exclude(user__email=editor_email) + ) for user_portfolio_permission in user_portfolio_permissions: # Send email to each portfolio_admin user = user_portfolio_permission.user From 6e50339ffa8728dc0a7643c52191a7302b16579c Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:49:05 -0700 Subject: [PATCH 55/73] Test org editor also emailed when org updated --- src/registrar/tests/test_email_invitations.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/registrar/tests/test_email_invitations.py b/src/registrar/tests/test_email_invitations.py index df99ae081..152530cc7 100644 --- a/src/registrar/tests/test_email_invitations.py +++ b/src/registrar/tests/test_email_invitations.py @@ -1157,6 +1157,12 @@ class TestSendPortfolioOrganizationUpdateEmail(unittest.TestCase): mock_filter.assert_called_once_with( portfolio=self.portfolio, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] ) + mock_send_templated_email.assert_any_call( + "emails/portfolio_org_update_notification.txt", + "emails/portfolio_org_update_notification_subject.txt", + to_address=self.admin_user1.email, + context=ANY, + ) mock_send_templated_email.assert_any_call( "emails/portfolio_org_update_notification.txt", "emails/portfolio_org_update_notification_subject.txt", From 66a55ec6e62349ee06c2a25f421774803b96f318 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 17 Mar 2025 15:05:19 -0700 Subject: [PATCH 56/73] Remove unused import --- src/registrar/tests/test_views_portfolio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 4199b53ea..d196ce47c 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -2,7 +2,7 @@ from django.urls import reverse from api.tests.common import less_console_noise_decorator from registrar.config import settings from registrar.models import Portfolio, SeniorOfficial -from unittest.mock import MagicMock, patch, ANY +from unittest.mock import MagicMock, patch from django_webtest import WebTest # type: ignore from django.core.handlers.wsgi import WSGIRequest from registrar.models import ( From a32479eddb2c0a94a5e0c39b7cb37e36f7d11844 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 17 Mar 2025 15:11:01 -0700 Subject: [PATCH 57/73] Remove unused variable --- src/registrar/utility/email_invitations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index 8e3d98418..9fa8c39fa 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -244,7 +244,6 @@ def send_portfolio_update_emails_to_portfolio_admins(editor, portfolio, updated_ MissingEmailError: If the requestor has no email associated with their account. EmailSendingError: If there is an error while sending the email. """ - editor_email = editor.email all_emails_sent = True # Get each portfolio admin from list user_portfolio_permissions = UserPortfolioPermission.objects.filter( From 2a7ff01905a4f309470df37410f5d6568c72179a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:31:33 -0600 Subject: [PATCH 58/73] Invert disclose logic + PR suggestions --- src/registrar/models/domain.py | 23 +++++----- src/registrar/tests/common.py | 7 +-- src/registrar/tests/test_models_domain.py | 56 ++++++++++++++--------- src/registrar/utility/enums.py | 3 ++ 4 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 149f34907..5d1bc246a 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1681,20 +1681,19 @@ class Domain(TimeStampedModel, DomainHelper): """creates a disclose object that can be added to a contact Create using .disclose= on the command before sending. if item is security email then make sure email is visible""" + # You can find each enum here: + # https://github.com/cisagov/epplib/blob/master/epplib/models/common.py#L32 DF = epp.DiscloseField - disclose_fields = {"fields": {}, "flag": False} - if contact.contact_type == contact.ContactTypeChoices.SECURITY: - hidden_security_emails = [email for email in DefaultEmail] - disclose_fields = { - "fields": {DF.EMAIL}, - "flag": contact.email not in hidden_security_emails, - } + all_disclose_fields = {field for field in DF if field != DF.NOTIFY_EMAIL} + disclose_fields = {"fields": all_disclose_fields, "flag": False} + if ( + contact.contact_type == contact.ContactTypeChoices.SECURITY and + contact.email not in [email for email in DefaultEmail] + ): + disclose_fields["fields"] -= {DF.EMAIL} elif contact.contact_type == contact.ContactTypeChoices.ADMINISTRATIVE: - disclose_fields = { - "fields": {DF.EMAIL, DF.VOICE, DF.ADDR}, - "types": {DF.ADDR: "loc"}, - "flag": True, - } + disclose_fields["fields"] -= {DF.EMAIL, DF.VOICE, DF.ADDR} + disclose_fields["types"] = {DF.ADDR: "loc"} logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_fields.get("flag")) return epp.Disclose(**disclose_fields) # type: ignore diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 101be51ef..0271a2460 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -1942,16 +1942,17 @@ class MockEppLib(TestCase): def _convertPublicContactToEpp( self, contact: PublicContact, - disclose_email=False, + disclose=False, createContact=True, disclose_fields=None, disclose_types=None, ): DF = common.DiscloseField if disclose_fields is None: - disclose_fields = {DF.EMAIL} + fields = {DF.NOTIFY_EMAIL, DF.EMAIL} + disclose_fields = {field for field in DF} - fields - di = common.Disclose(flag=disclose_email, fields=disclose_fields, types=disclose_types) + di = common.Disclose(flag=disclose, fields=disclose_fields, types=disclose_types) # check docs here looks like we may have more than one address field but addr = common.ContactAddr( diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 400924355..018d081c9 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -722,6 +722,8 @@ class TestRegistrantContacts(MockEppLib): self.domain, _ = Domain.objects.get_or_create(name="security.gov") # Creates a domain with an associated contact self.domain_contact, _ = Domain.objects.get_or_create(name="freeman.gov") + DF = common.DiscloseField + self.all_disclose_fields = {field for field in DF if field != DF.NOTIFY_EMAIL} def tearDown(self): super().tearDown() @@ -758,7 +760,9 @@ class TestRegistrantContacts(MockEppLib): contact_type=PublicContact.ContactTypeChoices.SECURITY, ).registry_id expectedSecContact.registry_id = id - expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False) + expectedCreateCommand = self._convertPublicContactToEpp( + expectedSecContact, disclose=False, disclose_fields=self.all_disclose_fields + ) expectedUpdateDomain = commands.UpdateDomain( name=self.domain.name, add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")], @@ -788,7 +792,7 @@ class TestRegistrantContacts(MockEppLib): # self.domain.security_contact=expectedSecContact expectedSecContact.save() # no longer the default email it should be disclosed - expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True) + expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose=False) expectedUpdateDomain = commands.UpdateDomain( name=self.domain.name, add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")], @@ -813,7 +817,9 @@ class TestRegistrantContacts(MockEppLib): security_contact.registry_id = "fail" security_contact.save() self.domain.security_contact = security_contact - expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=False) + expectedCreateCommand = self._convertPublicContactToEpp( + security_contact, disclose=False, disclose_fields=self.all_disclose_fields + ) expectedUpdateDomain = commands.UpdateDomain( name=self.domain.name, add=[common.DomainContact(contact=security_contact.registry_id, type="security")], @@ -846,7 +852,7 @@ class TestRegistrantContacts(MockEppLib): new_contact.registry_id = "fail" new_contact.email = "" self.domain.security_contact = new_contact - firstCreateContactCall = self._convertPublicContactToEpp(old_contact, disclose_email=True) + firstCreateContactCall = self._convertPublicContactToEpp(old_contact, disclose=False) updateDomainAddCall = commands.UpdateDomain( name=self.domain.name, add=[common.DomainContact(contact=old_contact.registry_id, type="security")], @@ -856,7 +862,7 @@ class TestRegistrantContacts(MockEppLib): PublicContact.get_default_security().email, ) # this one triggers the fail - secondCreateContact = self._convertPublicContactToEpp(new_contact, disclose_email=True) + secondCreateContact = self._convertPublicContactToEpp(new_contact, disclose=False) updateDomainRemCall = commands.UpdateDomain( name=self.domain.name, rem=[common.DomainContact(contact=old_contact.registry_id, type="security")], @@ -864,7 +870,9 @@ class TestRegistrantContacts(MockEppLib): defaultSecID = PublicContact.objects.filter(domain=self.domain).get().registry_id default_security = PublicContact.get_default_security() default_security.registry_id = defaultSecID - createDefaultContact = self._convertPublicContactToEpp(default_security, disclose_email=False) + createDefaultContact = self._convertPublicContactToEpp( + default_security, disclose=False, disclose_fields=self.all_disclose_fields + ) updateDomainWDefault = commands.UpdateDomain( name=self.domain.name, add=[common.DomainContact(contact=defaultSecID, type="security")], @@ -892,15 +900,15 @@ class TestRegistrantContacts(MockEppLib): security_contact.email = "originalUserEmail@gmail.com" security_contact.registry_id = "fail" security_contact.save() - expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True) + expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose=False) expectedUpdateDomain = commands.UpdateDomain( name=self.domain.name, add=[common.DomainContact(contact=security_contact.registry_id, type="security")], ) security_contact.email = "changedEmail@email.com" security_contact.save() - expectedSecondCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True) - updateContact = self._convertPublicContactToEpp(security_contact, disclose_email=True, createContact=False) + expectedSecondCreateCommand = self._convertPublicContactToEpp(security_contact, disclose=False) + updateContact = self._convertPublicContactToEpp(security_contact, disclose=False, createContact=False) expected_calls = [ call(expectedCreateCommand, cleaned=True), call(expectedUpdateDomain, cleaned=True), @@ -989,19 +997,21 @@ class TestRegistrantContacts(MockEppLib): expected_contact = contact[0] actual_contact = contact[1] if expected_contact.contact_type == PublicContact.ContactTypeChoices.SECURITY: + disclose_fields = self.all_disclose_fields - {"email"} expectedCreateCommand = self._convertPublicContactToEpp( - expected_contact, disclose_email=True, disclose_fields={"email"} + expected_contact, disclose=False, disclose_fields=disclose_fields ) elif expected_contact.contact_type == PublicContact.ContactTypeChoices.ADMINISTRATIVE: + disclose_fields = self.all_disclose_fields - {"email", "voice", "addr"} expectedCreateCommand = self._convertPublicContactToEpp( expected_contact, - disclose_email=True, - disclose_fields={"email", "voice", "addr"}, + disclose=False, + disclose_fields=disclose_fields, disclose_types={"addr": "loc"}, ) else: expectedCreateCommand = self._convertPublicContactToEpp( - expected_contact, disclose_email=False, disclose_fields={} + expected_contact, disclose=False, disclose_fields=self.all_disclose_fields ) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) # The emails should match on both items @@ -1011,14 +1021,14 @@ class TestRegistrantContacts(MockEppLib): with less_console_noise(): domain, _ = Domain.objects.get_or_create(name="freeman.gov") dummy_contact = domain.get_default_security_contact() - test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__ - test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=False).__dict__ + test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose=False).__dict__ + test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose=False).__dict__ # Separated for linter - disclose_email_field = {common.DiscloseField.EMAIL} + disclose_email_field = self.all_disclose_fields - {common.DiscloseField.EMAIL} self.maxDiff = None expected_disclose = { "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), - "disclose": common.Disclose(flag=True, fields=disclose_email_field, types=None), + "disclose": common.Disclose(flag=False, fields=disclose_email_field, types=None), "email": "help@get.gov", "extensions": [], "fax": None, @@ -1081,7 +1091,7 @@ class TestRegistrantContacts(MockEppLib): # Create contact with multiple disclosure fields result = self._convertPublicContactToEpp( dummy_contact, - disclose_email=True, + disclose=True, disclose_fields={DF.EMAIL, DF.VOICE, DF.ADDR}, disclose_types={DF.ADDR: "loc"}, ) @@ -1096,7 +1106,7 @@ class TestRegistrantContacts(MockEppLib): dummy_contact = domain.get_default_security_contact() # Create contact with empty fields list - result = self._convertPublicContactToEpp(dummy_contact, disclose_email=True, disclose_fields={}) + result = self._convertPublicContactToEpp(dummy_contact, disclose=True, disclose_fields={}) # Verify disclosure settings self.assertEqual(result.disclose.flag, True) @@ -1116,7 +1126,9 @@ class TestRegistrantContacts(MockEppLib): expectedSecContact.domain = domain expectedSecContact.registry_id = "defaultSec" domain.security_contact = expectedSecContact - expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False) + expectedCreateCommand = self._convertPublicContactToEpp( + expectedSecContact, disclose=False, disclose_fields=self.all_disclose_fields + ) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) # Confirm that we are getting a default email self.assertEqual(domain.security_contact.email, expectedSecContact.email) @@ -1135,7 +1147,7 @@ class TestRegistrantContacts(MockEppLib): expectedTechContact.registry_id = "defaultTech" domain.technical_contact = expectedTechContact expectedCreateCommand = self._convertPublicContactToEpp( - expectedTechContact, disclose_email=False, disclose_fields={} + expectedTechContact, disclose=False, disclose_fields=self.all_disclose_fields ) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) # Confirm that we are getting a default email @@ -1155,7 +1167,7 @@ class TestRegistrantContacts(MockEppLib): expectedSecContact.domain = domain expectedSecContact.email = "security@mail.gov" domain.security_contact = expectedSecContact - expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True) + expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose=False) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) # Confirm that we are getting the desired email self.assertEqual(domain.security_contact.email, expectedSecContact.email) diff --git a/src/registrar/utility/enums.py b/src/registrar/utility/enums.py index 8b3aff7ad..b1bce0412 100644 --- a/src/registrar/utility/enums.py +++ b/src/registrar/utility/enums.py @@ -39,6 +39,9 @@ class DefaultEmail(StrEnum): """ PUBLIC_CONTACT_DEFAULT = "help@get.gov" + # We used to use this email for default public contacts. + # This is retained for data correctness, but it will be phased out. + # help@get.gov is the current email that we use for these now. OLD_PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov" LEGACY_DEFAULT = "registrar@dotgov.gov" From dce762bc4a13d44019534ee5365c75fbef92c5f3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:47:01 -0600 Subject: [PATCH 59/73] Update domain.py --- src/registrar/models/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 5d1bc246a..9b784059a 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1685,7 +1685,7 @@ class Domain(TimeStampedModel, DomainHelper): # https://github.com/cisagov/epplib/blob/master/epplib/models/common.py#L32 DF = epp.DiscloseField all_disclose_fields = {field for field in DF if field != DF.NOTIFY_EMAIL} - disclose_fields = {"fields": all_disclose_fields, "flag": False} + disclose_fields = {"fields": all_disclose_fields, "flag": False, "types": {DF.ADDR: "loc"}} if ( contact.contact_type == contact.ContactTypeChoices.SECURITY and contact.email not in [email for email in DefaultEmail] From 3b45083fccaeb22df60588e417605cda82ee6a9d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:00:26 -0600 Subject: [PATCH 60/73] Exclude unused disclosefields --- src/registrar/models/domain.py | 3 ++- src/registrar/tests/test_models_domain.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 9b784059a..ac96c91c6 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1684,7 +1684,8 @@ class Domain(TimeStampedModel, DomainHelper): # You can find each enum here: # https://github.com/cisagov/epplib/blob/master/epplib/models/common.py#L32 DF = epp.DiscloseField - all_disclose_fields = {field for field in DF if field != DF.NOTIFY_EMAIL} + excluded_disclose_fields = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT} + all_disclose_fields = {field for field in DF if field not in excluded_disclose_fields} disclose_fields = {"fields": all_disclose_fields, "flag": False, "types": {DF.ADDR: "loc"}} if ( contact.contact_type == contact.ContactTypeChoices.SECURITY and diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 018d081c9..1038d2e2d 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -723,7 +723,8 @@ class TestRegistrantContacts(MockEppLib): # Creates a domain with an associated contact self.domain_contact, _ = Domain.objects.get_or_create(name="freeman.gov") DF = common.DiscloseField - self.all_disclose_fields = {field for field in DF if field != DF.NOTIFY_EMAIL} + excluded_disclose_fields = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT} + self.all_disclose_fields = {field for field in DF if field not in excluded_disclose_fields} def tearDown(self): super().tearDown() From ec143ac92c1a1a087d5ec5b80d4452ea1f459ca8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:26:29 -0600 Subject: [PATCH 61/73] lint and fix tests --- src/registrar/models/domain.py | 12 +++++------- src/registrar/tests/common.py | 5 ++++- src/registrar/tests/test_models_domain.py | 15 ++++++++------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index ac96c91c6..a6a834775 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1681,20 +1681,18 @@ class Domain(TimeStampedModel, DomainHelper): """creates a disclose object that can be added to a contact Create using .disclose= on the command before sending. if item is security email then make sure email is visible""" - # You can find each enum here: + # You can find each enum here: # https://github.com/cisagov/epplib/blob/master/epplib/models/common.py#L32 DF = epp.DiscloseField excluded_disclose_fields = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT} - all_disclose_fields = {field for field in DF if field not in excluded_disclose_fields} + all_disclose_fields = {field for field in DF} - excluded_disclose_fields disclose_fields = {"fields": all_disclose_fields, "flag": False, "types": {DF.ADDR: "loc"}} - if ( - contact.contact_type == contact.ContactTypeChoices.SECURITY and - contact.email not in [email for email in DefaultEmail] - ): + if contact.contact_type == contact.ContactTypeChoices.SECURITY and contact.email not in [ + email for email in DefaultEmail + ]: disclose_fields["fields"] -= {DF.EMAIL} elif contact.contact_type == contact.ContactTypeChoices.ADMINISTRATIVE: disclose_fields["fields"] -= {DF.EMAIL, DF.VOICE, DF.ADDR} - disclose_fields["types"] = {DF.ADDR: "loc"} logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_fields.get("flag")) return epp.Disclose(**disclose_fields) # type: ignore diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 0271a2460..e2fb9097f 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -1949,9 +1949,12 @@ class MockEppLib(TestCase): ): DF = common.DiscloseField if disclose_fields is None: - fields = {DF.NOTIFY_EMAIL, DF.EMAIL} + fields = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT, DF.EMAIL} disclose_fields = {field for field in DF} - fields + if disclose_types is None: + disclose_types = {DF.ADDR: "loc"} + di = common.Disclose(flag=disclose, fields=disclose_fields, types=disclose_types) # check docs here looks like we may have more than one address field but diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 1038d2e2d..5c5b36640 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -724,7 +724,7 @@ class TestRegistrantContacts(MockEppLib): self.domain_contact, _ = Domain.objects.get_or_create(name="freeman.gov") DF = common.DiscloseField excluded_disclose_fields = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT} - self.all_disclose_fields = {field for field in DF if field not in excluded_disclose_fields} + self.all_disclose_fields = {field for field in DF} - excluded_disclose_fields def tearDown(self): super().tearDown() @@ -1026,10 +1026,10 @@ class TestRegistrantContacts(MockEppLib): test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose=False).__dict__ # Separated for linter disclose_email_field = self.all_disclose_fields - {common.DiscloseField.EMAIL} - self.maxDiff = None + DF = common.DiscloseField expected_disclose = { "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), - "disclose": common.Disclose(flag=False, fields=disclose_email_field, types=None), + "disclose": common.Disclose(flag=False, fields=disclose_email_field, types={DF.ADDR: "loc"}), "email": "help@get.gov", "extensions": [], "fax": None, @@ -1054,7 +1054,7 @@ class TestRegistrantContacts(MockEppLib): # Separated for linter expected_not_disclose = { "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), - "disclose": common.Disclose(flag=False, fields=disclose_email_field, types=None), + "disclose": common.Disclose(flag=False, fields=disclose_email_field, types={DF.ADDR: "loc"}), "email": "help@get.gov", "extensions": [], "fax": None, @@ -1094,11 +1094,11 @@ class TestRegistrantContacts(MockEppLib): dummy_contact, disclose=True, disclose_fields={DF.EMAIL, DF.VOICE, DF.ADDR}, - disclose_types={DF.ADDR: "loc"}, + disclose_types={}, ) self.assertEqual(result.disclose.flag, True) self.assertEqual(result.disclose.fields, {DF.EMAIL, DF.VOICE, DF.ADDR}) - self.assertEqual(result.disclose.types, {DF.ADDR: "loc"}) + self.assertEqual(result.disclose.types, {}) @less_console_noise_decorator def test_convert_public_contact_with_empty_fields(self): @@ -1110,9 +1110,10 @@ class TestRegistrantContacts(MockEppLib): result = self._convertPublicContactToEpp(dummy_contact, disclose=True, disclose_fields={}) # Verify disclosure settings + DF = common.DiscloseField self.assertEqual(result.disclose.flag, True) self.assertEqual(result.disclose.fields, {}) - self.assertIsNone(result.disclose.types) + self.assertEqual(result.disclose.types, {DF.ADDR: "loc"}) def test_not_disclosed_on_default_security_contact(self): """ From 774333a0efe4eea77b3f0d1419cbcb505d33f87a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:34:40 -0600 Subject: [PATCH 62/73] Update domain.py --- src/registrar/models/domain.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index a6a834775..6d700b884 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -880,6 +880,7 @@ class Domain(TimeStampedModel, DomainHelper): which inturn call this function) Will throw error if contact type is not the same as expectType Raises ValueError if expected type doesn't match the contact type""" + if expectedType != contact.contact_type: raise ValueError("Cannot set a contact with a different contact type, expected type was %s" % expectedType) @@ -892,7 +893,6 @@ class Domain(TimeStampedModel, DomainHelper): duplicate_contacts = PublicContact.objects.exclude(registry_id=contact.registry_id).filter( domain=self, contact_type=contact.contact_type ) - # if no record exists with this contact type # make contact in registry, duplicate and errors handled there errorCode = self._make_contact_in_registry(contact) @@ -1690,9 +1690,9 @@ class Domain(TimeStampedModel, DomainHelper): if contact.contact_type == contact.ContactTypeChoices.SECURITY and contact.email not in [ email for email in DefaultEmail ]: - disclose_fields["fields"] -= {DF.EMAIL} + disclose_fields["fields"] -= {DF.EMAIL} # type: ignore elif contact.contact_type == contact.ContactTypeChoices.ADMINISTRATIVE: - disclose_fields["fields"] -= {DF.EMAIL, DF.VOICE, DF.ADDR} + disclose_fields["fields"] -= {DF.EMAIL, DF.VOICE, DF.ADDR} # type: ignore logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_fields.get("flag")) return epp.Disclose(**disclose_fields) # type: ignore From a059f13e36984be5bd0a2a564c24d615b389ad97 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 19 Mar 2025 07:48:42 -0600 Subject: [PATCH 63/73] Update src/registrar/models/domain.py Co-authored-by: Alysia <109625347+allly-b@users.noreply.github.com> --- src/registrar/models/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 6d700b884..faa9ec6b6 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1678,7 +1678,7 @@ class Domain(TimeStampedModel, DomainHelper): return help_text def _disclose_fields(self, contact: PublicContact): - """creates a disclose object that can be added to a contact Create using + """creates a disclose object that can be added to a contact Create or Update using .disclose= on the command before sending. if item is security email then make sure email is visible""" # You can find each enum here: From 08b28399e362bed9eb6f2cfdbc278be090cb0a8e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 19 Mar 2025 08:45:12 -0600 Subject: [PATCH 64/73] Some PR suggestions Still awaiting on the DF.NAME field --- src/registrar/models/domain.py | 44 +++++++++++++++++------ src/registrar/models/public_contact.py | 8 ----- src/registrar/tests/test_models_domain.py | 4 +-- src/registrar/utility/csv_export.py | 2 +- src/registrar/utility/enums.py | 5 +++ src/registrar/views/domain.py | 6 ++-- 6 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index faa9ec6b6..e5afbc0e5 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -971,6 +971,24 @@ class Domain(TimeStampedModel, DomainHelper): logger.info("making technical contact") self._set_singleton_contact(contact, expectedType=contact.ContactTypeChoices.TECHNICAL) + def print_contact_info_epp(self, contact: PublicContact): + """Prints registry data for this PublicContact for easier debugging""" + results = self._request_contact_info(contact, get_result_as_dict=True) + logger.info(f"EPP info for {contact.display_contact_type()}:") + logger.info("---------------------") + for key, value in results.items(): + logger.info(f"{key}: {value}") + + def print_all_domain_contact_info_epp(self): + """Prints registry data for this domains security, registrant, technical, and administrative contacts.""" + logger.info(f"Contact info for {self}:") + logger.info("=====================") + + contacts = [self.security_contact, self.registrant_contact, self.technical_contact, self.administrative_contact] + for contact_type, contact in contacts: + if contact: + self.print_contact_info_epp(contact) + def is_active(self) -> bool: """Currently just returns if the state is created, because then it should be live, theoretically. @@ -1684,18 +1702,22 @@ class Domain(TimeStampedModel, DomainHelper): # You can find each enum here: # https://github.com/cisagov/epplib/blob/master/epplib/models/common.py#L32 DF = epp.DiscloseField - excluded_disclose_fields = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT} - all_disclose_fields = {field for field in DF} - excluded_disclose_fields - disclose_fields = {"fields": all_disclose_fields, "flag": False, "types": {DF.ADDR: "loc"}} - if contact.contact_type == contact.ContactTypeChoices.SECURITY and contact.email not in [ - email for email in DefaultEmail - ]: - disclose_fields["fields"] -= {DF.EMAIL} # type: ignore - elif contact.contact_type == contact.ContactTypeChoices.ADMINISTRATIVE: - disclose_fields["fields"] -= {DF.EMAIL, DF.VOICE, DF.ADDR} # type: ignore + all_disclose_fields = {field for field in DF} + disclose_args = {"fields": all_disclose_fields, "flag": False, "types": {DF.ADDR: "loc"}} - logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose_fields.get("flag")) - return epp.Disclose(**disclose_fields) # type: ignore + fields_to_remove = {DF.NOTIFY_EMAIL, DF.VAT, DF.IDENT} + if contact.contact_type == contact.ContactTypeChoices.SECURITY: + if contact.email not in DefaultEmail.get_all_emails(): + fields_to_remove.add(DF.EMAIL) + elif contact.contact_type == contact.ContactTypeChoices.ADMINISTRATIVE: + fields_to_remove.update({DF.EMAIL, DF.VOICE, DF.ADDR}) + + disclose_fields = disclose_args["fields"] + for field in fields_to_remove: + disclose_fields.remove(field) + + logger.debug("Updated domain contact %s to disclose: %s", contact.email, disclose_args.get("flag")) + return epp.Disclose(**disclose_args) # type: ignore def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore return epp.PostalInfo( # type: ignore diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index d3167960d..58ad5be92 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -91,14 +91,6 @@ class PublicContact(TimeStampedModel): ) pw = models.CharField(null=False, help_text="Contact's authorization code. 16 characters minimum.") - def print_contact_info_epp(self): - """Prints registry data for this PublicContact for easier debugging""" - results = self.domain._request_contact_info(self, get_result_as_dict=True) - logger.info("Contact Info from EPP:") - logger.info("=====================") - for key, value in results.items(): - logger.info(f"{key}: {value}") - @classmethod def get_default_registrant(cls): return cls( diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 5c5b36640..22dee5c04 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1107,12 +1107,12 @@ class TestRegistrantContacts(MockEppLib): dummy_contact = domain.get_default_security_contact() # Create contact with empty fields list - result = self._convertPublicContactToEpp(dummy_contact, disclose=True, disclose_fields={}) + result = self._convertPublicContactToEpp(dummy_contact, disclose=True, disclose_fields={DF.EMAIL}) # Verify disclosure settings DF = common.DiscloseField self.assertEqual(result.disclose.flag, True) - self.assertEqual(result.disclose.fields, {}) + self.assertEqual(result.disclose.fields, {DF.EMAIL}) self.assertEqual(result.disclose.types, {DF.ADDR: "loc"}) def test_not_disclosed_on_default_security_contact(self): diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 5660cb41d..7f06a4f44 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -740,7 +740,7 @@ class DomainExport(BaseExport): domain_type = f"{human_readable_domain_org_type} - {human_readable_domain_federal_type}" security_contact_email = model.get("security_contact_email") - invalid_emails = [email for email in DefaultEmail] + invalid_emails = DefaultEmail.get_all_emails() if ( not security_contact_email or not isinstance(security_contact_email, str) diff --git a/src/registrar/utility/enums.py b/src/registrar/utility/enums.py index b1bce0412..fca5202d7 100644 --- a/src/registrar/utility/enums.py +++ b/src/registrar/utility/enums.py @@ -45,6 +45,11 @@ class DefaultEmail(StrEnum): OLD_PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov" LEGACY_DEFAULT = "registrar@dotgov.gov" + @classmethod + def get_all_emails(cls): + return [email for email in cls] + + class DefaultUserValues(StrEnum): """Stores default values for a default user. diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 0b13dda69..863d36bf5 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -398,7 +398,7 @@ class DomainView(DomainBaseView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - default_emails = [email for email in DefaultEmail] + default_emails = DefaultEmail.get_all_emails() context["hidden_security_emails"] = default_emails @@ -456,7 +456,7 @@ class DomainRenewalView(DomainBaseView): context = super().get_context_data(**kwargs) - default_emails = [email for email in DefaultEmail] + default_emails = DefaultEmail.get_all_emails() context["hidden_security_emails"] = default_emails @@ -1166,7 +1166,7 @@ class DomainSecurityEmailView(DomainFormBaseView): initial = super().get_initial() security_contact = self.object.security_contact - invalid_emails = [email for email in DefaultEmail] + invalid_emails = DefaultEmail.get_all_emails() if security_contact is None or security_contact.email in invalid_emails: initial["security_email"] = None return initial From 557f8cab5a2ba9d644394d96431996876e44d804 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 19 Mar 2025 10:52:57 -0600 Subject: [PATCH 65/73] Update domain.py --- src/registrar/models/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index e5afbc0e5..40ef65199 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -985,7 +985,7 @@ class Domain(TimeStampedModel, DomainHelper): logger.info("=====================") contacts = [self.security_contact, self.registrant_contact, self.technical_contact, self.administrative_contact] - for contact_type, contact in contacts: + for contact in contacts: if contact: self.print_contact_info_epp(contact) From b0bcaff2eee159da86a3688c1d337b2ba218bcb8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 19 Mar 2025 10:55:04 -0600 Subject: [PATCH 66/73] Update domain.py --- src/registrar/models/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 40ef65199..6fa90e862 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -974,7 +974,7 @@ class Domain(TimeStampedModel, DomainHelper): def print_contact_info_epp(self, contact: PublicContact): """Prints registry data for this PublicContact for easier debugging""" results = self._request_contact_info(contact, get_result_as_dict=True) - logger.info(f"EPP info for {contact.display_contact_type()}:") + logger.info(f"EPP info for {contact.contact_type}:") logger.info("---------------------") for key, value in results.items(): logger.info(f"{key}: {value}") From b48b6fc572548946a162581e9edcb95ce4cb4030 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:10:04 -0600 Subject: [PATCH 67/73] Fix unit test --- src/registrar/models/domain.py | 2 +- src/registrar/tests/test_models_domain.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 6fa90e862..fbe7fef90 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -974,6 +974,7 @@ class Domain(TimeStampedModel, DomainHelper): def print_contact_info_epp(self, contact: PublicContact): """Prints registry data for this PublicContact for easier debugging""" results = self._request_contact_info(contact, get_result_as_dict=True) + logger.info("---------------------") logger.info(f"EPP info for {contact.contact_type}:") logger.info("---------------------") for key, value in results.items(): @@ -983,7 +984,6 @@ class Domain(TimeStampedModel, DomainHelper): """Prints registry data for this domains security, registrant, technical, and administrative contacts.""" logger.info(f"Contact info for {self}:") logger.info("=====================") - contacts = [self.security_contact, self.registrant_contact, self.technical_contact, self.administrative_contact] for contact in contacts: if contact: diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 22dee5c04..5ca32140c 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1106,11 +1106,11 @@ class TestRegistrantContacts(MockEppLib): domain, _ = Domain.objects.get_or_create(name="freeman.gov") dummy_contact = domain.get_default_security_contact() + DF = common.DiscloseField # Create contact with empty fields list result = self._convertPublicContactToEpp(dummy_contact, disclose=True, disclose_fields={DF.EMAIL}) # Verify disclosure settings - DF = common.DiscloseField self.assertEqual(result.disclose.flag, True) self.assertEqual(result.disclose.fields, {DF.EMAIL}) self.assertEqual(result.disclose.types, {DF.ADDR: "loc"}) From ecaa855b8649b153937148fd8439ac6eae514dee Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:14:27 -0600 Subject: [PATCH 68/73] lint! --- src/registrar/utility/enums.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/utility/enums.py b/src/registrar/utility/enums.py index fca5202d7..6c2649eec 100644 --- a/src/registrar/utility/enums.py +++ b/src/registrar/utility/enums.py @@ -50,7 +50,6 @@ class DefaultEmail(StrEnum): return [email for email in cls] - class DefaultUserValues(StrEnum): """Stores default values for a default user. From 7c690ebc65525616f556b869310b2c22209b7a49 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:19:18 -0600 Subject: [PATCH 69/73] use difference update --- src/registrar/models/domain.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index fbe7fef90..33c358d24 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1712,9 +1712,7 @@ class Domain(TimeStampedModel, DomainHelper): elif contact.contact_type == contact.ContactTypeChoices.ADMINISTRATIVE: fields_to_remove.update({DF.EMAIL, DF.VOICE, DF.ADDR}) - disclose_fields = disclose_args["fields"] - for field in fields_to_remove: - disclose_fields.remove(field) + disclose_args["fields"].difference_update(fields_to_remove) logger.debug("Updated domain contact %s to disclose: %s", contact.email, disclose_args.get("flag")) return epp.Disclose(**disclose_args) # type: ignore From 117797a4b12bde19b091d3662c8646e0d9d118d5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:20:37 -0600 Subject: [PATCH 70/73] Update domain.py --- src/registrar/models/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 33c358d24..b054afb5b 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1712,7 +1712,7 @@ class Domain(TimeStampedModel, DomainHelper): elif contact.contact_type == contact.ContactTypeChoices.ADMINISTRATIVE: fields_to_remove.update({DF.EMAIL, DF.VOICE, DF.ADDR}) - disclose_args["fields"].difference_update(fields_to_remove) + disclose_args["fields"].difference_update(fields_to_remove) # type: ignore logger.debug("Updated domain contact %s to disclose: %s", contact.email, disclose_args.get("flag")) return epp.Disclose(**disclose_args) # type: ignore From c6586df46c54c22c99b7f41cc47ffad310e910fe Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:49:14 -0700 Subject: [PATCH 71/73] Fix linting --- src/registrar/tests/test_email_invitations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_email_invitations.py b/src/registrar/tests/test_email_invitations.py index 0dd2c29e5..d4ca5cdac 100644 --- a/src/registrar/tests/test_email_invitations.py +++ b/src/registrar/tests/test_email_invitations.py @@ -1318,4 +1318,4 @@ class TestSendPortfolioOrganizationUpdateEmail(unittest.TestCase): to_address=self.admin_user2.email, context=ANY, ) - self.assertTrue(result) \ No newline at end of file + self.assertTrue(result) From 130aedc58a43e0ce5cab73c72bc1a62580ad7f1f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 20 Mar 2025 17:01:44 -0600 Subject: [PATCH 72/73] Remove printers --- src/registrar/models/domain.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index b054afb5b..b1eed865c 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -971,24 +971,6 @@ class Domain(TimeStampedModel, DomainHelper): logger.info("making technical contact") self._set_singleton_contact(contact, expectedType=contact.ContactTypeChoices.TECHNICAL) - def print_contact_info_epp(self, contact: PublicContact): - """Prints registry data for this PublicContact for easier debugging""" - results = self._request_contact_info(contact, get_result_as_dict=True) - logger.info("---------------------") - logger.info(f"EPP info for {contact.contact_type}:") - logger.info("---------------------") - for key, value in results.items(): - logger.info(f"{key}: {value}") - - def print_all_domain_contact_info_epp(self): - """Prints registry data for this domains security, registrant, technical, and administrative contacts.""" - logger.info(f"Contact info for {self}:") - logger.info("=====================") - contacts = [self.security_contact, self.registrant_contact, self.technical_contact, self.administrative_contact] - for contact in contacts: - if contact: - self.print_contact_info_epp(contact) - def is_active(self) -> bool: """Currently just returns if the state is created, because then it should be live, theoretically. From c3d3e49f715a0d8e8510b76346e6c11b91f2efe5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 21 Mar 2025 07:40:14 -0600 Subject: [PATCH 73/73] Update domain.py --- src/registrar/models/domain.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index b1eed865c..b054afb5b 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -971,6 +971,24 @@ class Domain(TimeStampedModel, DomainHelper): logger.info("making technical contact") self._set_singleton_contact(contact, expectedType=contact.ContactTypeChoices.TECHNICAL) + def print_contact_info_epp(self, contact: PublicContact): + """Prints registry data for this PublicContact for easier debugging""" + results = self._request_contact_info(contact, get_result_as_dict=True) + logger.info("---------------------") + logger.info(f"EPP info for {contact.contact_type}:") + logger.info("---------------------") + for key, value in results.items(): + logger.info(f"{key}: {value}") + + def print_all_domain_contact_info_epp(self): + """Prints registry data for this domains security, registrant, technical, and administrative contacts.""" + logger.info(f"Contact info for {self}:") + logger.info("=====================") + contacts = [self.security_contact, self.registrant_contact, self.technical_contact, self.administrative_contact] + for contact in contacts: + if contact: + self.print_contact_info_epp(contact) + def is_active(self) -> bool: """Currently just returns if the state is created, because then it should be live, theoretically.