From fb1735e23fec05bd016e833aca427dff4a2d2fff Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 22 Feb 2024 16:51:36 -0800 Subject: [PATCH 01/51] Update yaml to include metadata, readme with new directions an d pseudocode for s3 and SES work --- .github/workflows/daily-csv-upload.yaml | 9 +++ docs/developer/README.md | 9 +-- .../generate_current_metadata_report.py | 67 +++++++++++++++++++ src/registrar/utility/email.py | 4 ++ 4 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 src/registrar/management/commands/generate_current_metadata_report.py diff --git a/.github/workflows/daily-csv-upload.yaml b/.github/workflows/daily-csv-upload.yaml index 724a19457..84881398e 100644 --- a/.github/workflows/daily-csv-upload.yaml +++ b/.github/workflows/daily-csv-upload.yaml @@ -31,3 +31,12 @@ jobs: cf_space: ${{ secrets.CF_REPORT_ENV }} cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py generate_current_full_report' --name full" + - name: Generate current-metadata.csv + uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets[env.CF_USERNAME] }} + cf_password: ${{ secrets[env.CF_PASSWORD] }} + cf_org: cisa-dotgov + cf_space: ${{ secrets.CF_REPORT_ENV }} + cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py generate_current_metadata_report' --name metadata" + diff --git a/docs/developer/README.md b/docs/developer/README.md index dc4c9ddd2..7dc64ae56 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -330,11 +330,12 @@ To associate a S3 instance to your sandbox, follow these steps: 3. Click `Services` on the application nav bar 4. Add a new service (plus symbol) 5. Click `Marketplace Service` -6. On the `Select the service` dropdown, select `s3` -7. Under the dropdown on `Select Plan`, select `basic-sandbox` -8. Under `Service Instance` enter `getgov-s3` for the name +6. For Space, put in your sandbox initials +7. On the `Select the service` dropdown, select `s3` +8. Under the dropdown on `Select Plan`, select `basic-sandbox` +9. Under `Service Instance` enter `getgov-s3` for the name and leave the other fields empty -See this [resource](https://cloud.gov/docs/services/s3/) for information on associating an S3 instance with your sandbox through the CLI. +See this [resource](https://cloud.gov/docs/services/s3/) for information on associating an S3 instance with your sandbox through the CLI. The basic commands should be `cf bind-service getgov- ` and `cf restage getgov-`. ### Testing your S3 instance locally To test the S3 bucket associated with your sandbox, you will need to add four additional variables to your `.env` file. These are as follows: diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/generate_current_metadata_report.py new file mode 100644 index 000000000..d8f5a4693 --- /dev/null +++ b/src/registrar/management/commands/generate_current_metadata_report.py @@ -0,0 +1,67 @@ +"""Generates current-metadata.csv then uploads to S3 + sends email""" + +import logging +import os + +from django.core.management import BaseCommand +from registrar.utility import csv_export +from registrar.utility.s3_bucket import S3ClientHelper + + +logger = logging.getLogger(__name__) + +class Command(BaseCommand): + help = ( + "Generates and uploads a current-metadata.csv file to our S3 bucket " "which is based off of all existing Domains." + ) + + def add_arguments(self, parser): + """Add our two filename arguments.""" + parser.add_argument("--directory", default="migrationdata", help="Desired directory") + parser.add_argument( + "--checkpath", + default=True, + help="Flag that determines if we do a check for os.path.exists. Used for test cases", + ) + + def handle(self, **options): + """Grabs the directory then creates current-metadata.csv in that directory""" + file_name = "current-metadata.csv" + # Ensures a slash is added + directory = os.path.join(options.get("directory"), "") + check_path = options.get("checkpath") + + logger.info("Generating report...") + try: + self.generate_current_metadata_report(directory, file_name, check_path) + except Exception as err: + # TODO - #1317: Notify operations when auto report generation fails + raise err + else: + logger.info(f"Success! Created {file_name}") + + def generate_current_metadata_report(self, directory, file_name, check_path): + """Creates a current-full.csv file under the specified directory, + then uploads it to a AWS S3 bucket""" + s3_client = S3ClientHelper() + file_path = os.path.join(directory, file_name) + + # Generate a file locally for upload + with open(file_path, "w") as file: + csv_export.export_data_type_to_csv(file) + + if check_path and not os.path.exists(file_path): + raise FileNotFoundError(f"Could not find newly created file at '{file_path}'") + + # Upload this generated file for our S3 instance + s3_client.upload_file(file_path, file_name) + """ + We want to make sure to upload to s3 for back up + And now we also want to get the file and encrypt it so we can send it in an email + """ + # metadata_file = s3_client.get_file(file_name) + # metadata_file.encryptthisthingherewithpyzipper + # email.blasend_email(metadata_file) + + + diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 461637f23..e4e997d9d 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -41,6 +41,7 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr raise EmailSendingError("Could not access the SES client.") from exc try: + #if not attachment: ses_client.send_email( FromEmailAddress=settings.DEFAULT_FROM_EMAIL, Destination={"ToAddresses": [to_address]}, @@ -51,5 +52,8 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr }, }, ) + # else: # has attachment + # same as above but figure out how to attach a file + # via boto3 "boto3 SES file attachment" except Exception as exc: raise EmailSendingError("Could not send SES email.") from exc From 010853a5f038047d0ee5a0b1b09b4848e101f233 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 23 Feb 2024 15:26:53 -0800 Subject: [PATCH 02/51] Update readme directions for s3 --- docs/developer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/README.md b/docs/developer/README.md index 7dc64ae56..53e1182c4 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -335,7 +335,7 @@ To associate a S3 instance to your sandbox, follow these steps: 8. Under the dropdown on `Select Plan`, select `basic-sandbox` 9. Under `Service Instance` enter `getgov-s3` for the name and leave the other fields empty -See this [resource](https://cloud.gov/docs/services/s3/) for information on associating an S3 instance with your sandbox through the CLI. The basic commands should be `cf bind-service getgov- ` and `cf restage getgov-`. +See this [resource](https://cloud.gov/docs/services/s3/) for information on associating an S3 instance with your sandbox through the CLI. ### Testing your S3 instance locally To test the S3 bucket associated with your sandbox, you will need to add four additional variables to your `.env` file. These are as follows: From fdb7fb659464a199972e4948e6dd544088e6ac1a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 28 Feb 2024 09:17:59 -0800 Subject: [PATCH 03/51] Biz logic --- .../generate_current_metadata_report.py | 28 ++++++++-- src/registrar/utility/email.py | 54 +++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/generate_current_metadata_report.py index d8f5a4693..69d111f4c 100644 --- a/src/registrar/management/commands/generate_current_metadata_report.py +++ b/src/registrar/management/commands/generate_current_metadata_report.py @@ -2,10 +2,12 @@ import logging import os +import pyzipper from django.core.management import BaseCommand from registrar.utility import csv_export from registrar.utility.s3_bucket import S3ClientHelper +from ...utility.email import send_templated_email, EmailSendingError logger = logging.getLogger(__name__) @@ -59,9 +61,27 @@ class Command(BaseCommand): We want to make sure to upload to s3 for back up And now we also want to get the file and encrypt it so we can send it in an email """ - # metadata_file = s3_client.get_file(file_name) - # metadata_file.encryptthisthingherewithpyzipper - # email.blasend_email(metadata_file) + unencrypted_metadata_input = s3_client.get_file(file_name) + + # Encrypt metadata into a zip file + + # pre-setting zip file name + encrypted_metadata_output = 'encrypted_metadata.zip' + # set this to be an env var somewhere + password = b'somepasswordhere' + # encrypted_metadata is the encrypted output + encrypted_metadata = _encrypt_metadata(unencrypted_metadata_input, encrypted_metadata_output, password) + print("encrypted_metadata is:", encrypted_metadata) + + # Send the metadata file that is zipped + # Q: Would we set the vars I set in email.py here to pass in to the helper function or best way to invoke + # send_templated_email(encrypted_metadata, attachment=True) - + def _encrypt_metadata(input_file, output_file, password): + with open(input_file, 'rb') as f_in: + with pyzipper.AESZipFile(output_file, 'w', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES) as f_out: + f_out.setpassword(password) + f_out.writestr(input_file, f_in.read()) + return output_file + diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index e4e997d9d..199a6c304 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -40,6 +40,8 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr except Exception as exc: raise EmailSendingError("Could not access the SES client.") from exc + # Are we okay with passing in "attachment" var in as boolean parameter + # If so, TODO: add attachment boolean to other functions try: #if not attachment: ses_client.send_email( @@ -55,5 +57,57 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr # else: # has attachment # same as above but figure out how to attach a file # via boto3 "boto3 SES file attachment" + # we also want this to only send to the help email + + # from email.mime.multipart import MIMEMultipart + # from email.mime.text import MIMEText + # from email.mime.application import MIMEApplication + + # sender_email = 'sender@example.com' + # recipient_email = 'help@get.gov' + # subject = 'DOTGOV-Full Domain Metadata' + # body = 'Domain metadata email, should have an attachment included change here later.' + # attachment_path = 'path/to/attachment/file.pdf' + # aws_region = 'sesv2' + + # response = send_email_with_attachment(sender_email, recipient_email, subject, body, attachment_path, aws_region) + # print(response) except Exception as exc: raise EmailSendingError("Could not send SES email.") from exc + + +# def send_email_with_attachment(sender, recipient, subject, body, attachment_path, aws_region): + # # Create a multipart/mixed parent container + # msg = MIMEMultipart('mixed') + # msg['Subject'] = subject + # msg['From'] = sender_email + # msg['To'] = recipient_email + + # # Add the text part + # text_part = MIMEText(body, 'plain') + # msg.attach(text_part) + + # # Add the attachment part + # with open(attachment_path, 'rb') as attachment_file: + # attachment_data = attachment_file.read() + # attachment_part = MIMEApplication(attachment_data) + # attachment_part.add_header('Content-Disposition', f'attachment; filename="{attachment_path}"') + # msg.attach(attachment_part) + + # # Send the email + # response = ses_client.send_raw_email( + # Source=sender, + # Destinations=[recipient], + # RawMessage={'Data': msg.as_string()} + # ) + + # ses_client.send_email( + # FromEmailAddress=settings.DEFAULT_FROM_EMAIL, + # Destination={"ToAddresses": [to_address]}, + # Content={ + # "Simple": { + # "Subject": {"Data": subject}, + # "Body": {"Text": {"Data": email_body}}, + # }, + # }, + # ) \ No newline at end of file From de17d686e3f73234c81915dabab88961e4f37a55 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:30:23 -0700 Subject: [PATCH 04/51] Add basic on hold modal --- src/registrar/assets/js/get-gov-admin.js | 2 +- .../django/admin/domain_change_form.html | 84 +++++++++++++++++-- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index ff73acb65..096f7a626 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -29,7 +29,7 @@ function openInNewTab(el, removeAttribute = false){ */ (function (){ function createPhantomModalFormButtons(){ - let submitButtons = document.querySelectorAll('.usa-modal button[type="submit"]'); + let submitButtons = document.querySelectorAll('.usa-modal button[type="submit"].dja-form-placeholder'); form = document.querySelector("form") submitButtons.forEach((button) => { diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html index 67c5ac291..e38583eb8 100644 --- a/src/registrar/templates/django/admin/domain_change_form.html +++ b/src/registrar/templates/django/admin/domain_change_form.html @@ -11,18 +11,15 @@
{% if original.state != original.State.DELETED %} - + Extend expiration date | {% endif %} {% if original.state == original.State.READY %} - + + Place hold + {% elif original.state == original.State.ON_HOLD %} {% endif %} @@ -52,6 +49,8 @@ In addition, the modal element MUST be placed low in the DOM. The script loads slower on DJA than on other portions of the application, so this means that it will briefly "populate", causing unintended visual effects. {% endcomment %} + + {# Create a modal for the _extend_expiration_date button #}
- +
+ + {# Create a modal for the _on_hold button #} +
+
+
+ +
+

+ When a domain is on hold: +

+
    +
  • The domain (and any subdomains) won’t resolve in DNS. Any infrastructure (like websites) will go offline.
  • +
  • The domain will still appear in the registrar / admin.
  • +
  • Domain managers won’t be able to edit the domain.
  • +
+

+ This action can be reversed, if needed. +

+

+ {# Acts as a
#} +

+ Domain: {{ original.name }} + New status: {{ original.State.ON_HOLD }} +

+
+ + +
+ +
+
+ {# Create a modal for when a domain is marked as ineligible #} {{ block.super }} {% endblock %} \ No newline at end of file From f693557f93eeb0d057d46a23ed80f79c4a3ea7b0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Feb 2024 08:58:40 -0700 Subject: [PATCH 05/51] Add some modals --- src/registrar/admin.py | 2 + .../admin/domain_application_change_form.html | 87 +++++++++++++++++++ .../django/admin/domain_change_form.html | 78 +++++++++++++++-- 3 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 src/registrar/templates/django/admin/domain_application_change_form.html diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 92e477667..942ae6162 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -931,6 +931,8 @@ class DomainApplicationAdmin(ListHeaderAdmin): if self.value() == "0": return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None)) + change_form_template = "django/admin/domain_application_change_form.html" + # Columns list_display = [ "requested_domain", diff --git a/src/registrar/templates/django/admin/domain_application_change_form.html b/src/registrar/templates/django/admin/domain_application_change_form.html new file mode 100644 index 000000000..26abbec18 --- /dev/null +++ b/src/registrar/templates/django/admin/domain_application_change_form.html @@ -0,0 +1,87 @@ +{% extends 'admin/change_form.html' %} +{% load i18n static %} + +{% block submit_buttons_bottom %} + {% comment %} + Modals behave very weirdly in django admin. + They tend to "strip out" any injected form elements, leaving only the main form. + In addition, USWDS handles modals by first destroying the element, then repopulating it toward the end of the page. + In effect, this means that the modal is not, and cannot, be surrounded by any form element at compile time. + + The current workaround for this is to use javascript to inject a hidden input, and bind submit of that + element to the click of the confirmation button within this modal. + + This is controlled by the class `dja-form-placeholder` on the button. + + In addition, the modal element MUST be placed low in the DOM. The script loads slower on DJA than on other portions + of the application, so this means that it will briefly "populate", causing unintended visual effects. + {% endcomment %} +{# Create a modal for when a domain is marked as ineligible #} +
+
+
+ +
+

+ When a domain request is in ineligible status, the registrant's permissions within the registrar are restricted as follows: +

+
    +
  • They cannot edit the ineligible request or any other pending requests.
  • +
  • They cannot manage any of their approved domains.
  • +
  • They cannot initiate a new domain request.
  • +
+

+ This action can be reversed, if needed. +

+

+ {# Acts as a
#} +

+ Domain: {{ original.name }} + New status: {{ original.State }} +

+
+ + +
+ +
+
+{{ block.super }} +{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html index e38583eb8..9d7b1d5de 100644 --- a/src/registrar/templates/django/admin/domain_change_form.html +++ b/src/registrar/templates/django/admin/domain_change_form.html @@ -27,7 +27,9 @@ | {% endif %} {% if original.state != original.State.DELETED %} - + + Remove from registry + {% endif %} @@ -118,8 +120,8 @@
@@ -131,7 +133,7 @@ When a domain is on hold:

    -
  • The domain (and any subdomains) won’t resolve in DNS. Any infrastructure (like websites) will go offline.
  • +
  • The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.
  • The domain will still appear in the registrar / admin.
  • Domain managers won’t be able to edit the domain.
@@ -181,6 +183,72 @@
- {# Create a modal for when a domain is marked as ineligible #} + {# Create a modal for the _remove_domain button #} +
+
+
+ +
+

+ When a domain is removed from the registry: +

+
    +
  • The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.
  • +
  • The domain will still appear in the registrar / admin.
  • +
  • Domain managers won’t be able to edit the domain.
  • +
+

+ This action cannot be undone. +

+

+ {# Acts as a
#} +

+ Domain: {{ original.name }} + New status: {{ original.State.DELETED }} +

+
+ + +
+ +
+
{{ block.super }} {% endblock %} \ No newline at end of file From eeff5586d2b0de040ca2ed1614f94db0a9b3d618 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Feb 2024 09:20:33 -0700 Subject: [PATCH 06/51] Update styling --- src/registrar/assets/sass/_theme/_admin.scss | 9 +++++++++ .../django/admin/domain_change_form.html | 18 +++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index b57c6a015..46e4a10d4 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -302,3 +302,12 @@ input.admin-confirm-button { display: contents !important; } } + +.django-admin-modal .usa-prose ul > li { + list-style-type: inherit; + // Styling based off of the

styling in django admin + line-height: 1.5; + margin-bottom: 0; + margin-top: 0; + max-width: 68ex; +} diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html index 9d7b1d5de..61007c3d4 100644 --- a/src/registrar/templates/django/admin/domain_change_form.html +++ b/src/registrar/templates/django/admin/domain_change_form.html @@ -54,7 +54,7 @@ {# Create a modal for the _extend_expiration_date button #}

+ Domain: {{ original.name }} {# Acts as a
#}

- Domain: {{ original.name }} New status: {{ original.State.ON_HOLD }}

@@ -185,7 +185,7 @@
{# Create a modal for the _remove_domain button #}
    -
  • The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.
  • -
  • The domain will still appear in the registrar / admin.
  • -
  • Domain managers won’t be able to edit the domain.
  • +
  • The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.
  • +
  • The domain will still appear in the registrar / admin.
  • +
  • Domain managers won’t be able to edit the domain.

This action cannot be undone.

+ Domain: {{ original.name }} {# Acts as a
#}

- Domain: {{ original.name }} New status: {{ original.State.DELETED }}

@@ -251,4 +251,4 @@ {{ block.super }} -{% endblock %} \ No newline at end of file +{% endblock %} From 8148b9099656e51ca26a2f514003d237eba29340 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 29 Feb 2024 09:06:58 -0800 Subject: [PATCH 07/51] Update documentation and add in the pyzipper import and associated biz logic --- .../runbooks/update_python_dependencies.md | 4 +- src/Pipfile | 2 + src/Pipfile.lock | 561 +++++++++--------- src/docker-compose.yml | 2 + src/registrar/config/settings.py | 3 + .../generate_current_metadata_report.py | 24 +- src/requirements.txt | 33 +- 7 files changed, 325 insertions(+), 304 deletions(-) diff --git a/docs/operations/runbooks/update_python_dependencies.md b/docs/operations/runbooks/update_python_dependencies.md index b94c0f39f..04fb936c6 100644 --- a/docs/operations/runbooks/update_python_dependencies.md +++ b/docs/operations/runbooks/update_python_dependencies.md @@ -2,7 +2,7 @@ ======================== 1. Check the [Pipfile](../../../src/Pipfile) for pinned dependencies and manually adjust the version numbers - +2. Run `docker-compose stop` to spin down the current containers and images so we can start afresh 2. Run cd src @@ -16,6 +16,6 @@ 3. Change geventconnpool back to what it was originally within the Pipfile.lock and requirements.txt. This is done by either saving what it was originally or opening a PR and using that as a reference to undo changes to any mention of geventconnpool. Geventconnpool, when set as a requirement without the reference portion, is defaulting to get a commit from 2014 which then breaks the code, as we want the newest version from them. -4. (optional) Run `docker-compose stop` and `docker-compose build` to build a new image for local development with the updated dependencies. +4. Run `docker-compose build` to build a new image for local development with the updated dependencies. The reason for de-coupling the `build` and `lock` steps is to increase consistency between builds--a run of `build` will always get exactly the dependencies listed in `Pipfile.lock`, nothing more, nothing less. \ No newline at end of file diff --git a/src/Pipfile b/src/Pipfile index 51417d578..8e43d1bab 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -30,6 +30,7 @@ greenlet = "*" gevent = "*" fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"} geventconnpool = {git = "https://github.com/rasky/geventconnpool.git", ref = "1bbb93a714a331a069adf27265fe582d9ba7ecd4"} +pyzipper="*" [dev-packages] django-debug-toolbar = "*" @@ -45,3 +46,4 @@ types-cachetools = "*" boto3-mocking = "*" boto3-stubs = "*" django-model2puml = "*" + diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 7d511a0e5..c410630e1 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a672aeb8951fd850e90ad87c6f03cf71e2fc2b387d56fd3942361cb0b45bb449" + "sha256": "8c15011f6c6e0447e4ca675ce840fe6b67048e90255e7c083be357b373f96a47" }, "pipfile-spec": 6, "requires": {}, @@ -32,29 +32,29 @@ }, "boto3": { "hashes": [ - "sha256:65acfe7f1cf2a9b7df3d4edb87c8022e02685825bd1957e7bb678cc0d09f5e5f", - "sha256:73f5ec89cb3ddb3ed577317889fd2f2df783f66b6502a9a4239979607e33bf74" + "sha256:66303b5f26d92afb72656ff490b22ea72dfff8bf1a29e4a0c5d5f11ec56245dd", + "sha256:898ad2123b18cae8efd85adc56ac2d1925be54592aebc237020d4f16e9a9e7a9" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.37" + "version": "==1.34.52" }, "botocore": { "hashes": [ - "sha256:2a5bf33aacd2d970afd3d492e179e06ea98a5469030d5cfe7a2ad9995f7bb2ef", - "sha256:3c46ddb1679e6ef45ca78b48665398636bda532a07cd476e4b500697d13d9a99" + "sha256:05567d8aba344826060481ea309555432c96f0febe22bee7cf5a3b6d3a03cec8", + "sha256:187da93aec3f2e87d8a31eced16fa2cb9c71fe2d69b0a797f9f7a9220f5bf7ae" ], "markers": "python_version >= '3.8'", - "version": "==1.34.37" + "version": "==1.34.52" }, "cachetools": { "hashes": [ - "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2", - "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1" + "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945", + "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==5.3.2" + "version": "==5.3.3" }, "certifi": { "hashes": [ @@ -228,41 +228,41 @@ }, "cryptography": { "hashes": [ - "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380", - "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589", - "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea", - "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65", - "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a", - "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3", - "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008", - "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1", - "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2", - "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635", - "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2", - "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90", - "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee", - "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a", - "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242", - "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12", - "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2", - "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d", - "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be", - "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee", - "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6", - "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529", - "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929", - "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1", - "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6", - "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a", - "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446", - "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9", - "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888", - "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4", - "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33", - "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f" + "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", + "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576", + "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", + "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", + "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413", + "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", + "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", + "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", + "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd", + "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", + "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", + "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", + "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", + "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", + "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", + "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940", + "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400", + "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", + "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", + "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", + "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74", + "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", + "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", + "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", + "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", + "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", + "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", + "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", + "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", + "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", + "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", + "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7" ], "markers": "python_version >= '3.7'", - "version": "==42.0.2" + "version": "==42.0.5" }, "defusedxml": { "hashes": [ @@ -384,12 +384,12 @@ }, "faker": { "hashes": [ - "sha256:60e89e5c0b584e285a7db05eceba35011a241954afdab2853cb246c8a56700a2", - "sha256:b7f76bb1b2ac4cdc54442d955e36e477c387000f31ce46887fb9722a041be60b" + "sha256:117ce1a2805c1bc5ca753b3dc6f9d567732893b2294b827d3164261ee8f20267", + "sha256:458d93580de34403a8dec1e8d5e6be2fee96c4deca63b95d71df7a6a80a690de" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==23.1.0" + "version": "==23.3.0" }, "fred-epplib": { "git": "https://github.com/cisagov/epplib.git", @@ -404,61 +404,63 @@ }, "future": { "hashes": [ - "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307" + "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", + "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05" ], "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.18.3" + "version": "==1.0.0" }, "gevent": { "hashes": [ - "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a", - "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2", - "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535", - "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e", - "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653", - "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1", - "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c", - "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648", - "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599", - "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea", - "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6", - "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f", - "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9", - "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e", - "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34", - "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397", - "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507", - "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b", - "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd", - "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe", - "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a", - "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b", - "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771", - "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e", - "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69", - "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a", - "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011", - "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7", - "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71", - "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5", - "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae", - "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7", - "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39", - "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d", - "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599", - "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07", - "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904", - "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a", - "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543", - "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303" + "sha256:03aa5879acd6b7076f6a2a307410fb1e0d288b84b03cdfd8c74db8b4bc882fc5", + "sha256:117e5837bc74a1673605fb53f8bfe22feb6e5afa411f524c835b2ddf768db0de", + "sha256:141a2b24ad14f7b9576965c0c84927fc85f824a9bb19f6ec1e61e845d87c9cd8", + "sha256:14532a67f7cb29fb055a0e9b39f16b88ed22c66b96641df8c04bdc38c26b9ea5", + "sha256:1dffb395e500613e0452b9503153f8f7ba587c67dd4a85fc7cd7aa7430cb02cc", + "sha256:2955eea9c44c842c626feebf4459c42ce168685aa99594e049d03bedf53c2800", + "sha256:2ae3a25ecce0a5b0cd0808ab716bfca180230112bb4bc89b46ae0061d62d4afe", + "sha256:2e9ac06f225b696cdedbb22f9e805e2dd87bf82e8fa5e17756f94e88a9d37cf7", + "sha256:368a277bd9278ddb0fde308e6a43f544222d76ed0c4166e0d9f6b036586819d9", + "sha256:3adfb96637f44010be8abd1b5e73b5070f851b817a0b182e601202f20fa06533", + "sha256:3d5325ccfadfd3dcf72ff88a92fb8fc0b56cacc7225f0f4b6dcf186c1a6eeabc", + "sha256:432fc76f680acf7cf188c2ee0f5d3ab73b63c1f03114c7cd8a34cebbe5aa2056", + "sha256:44098038d5e2749b0784aabb27f1fcbb3f43edebedf64d0af0d26955611be8d6", + "sha256:5a1df555431f5cd5cc189a6ee3544d24f8c52f2529134685f1e878c4972ab026", + "sha256:6c47ae7d1174617b3509f5d884935e788f325eb8f1a7efc95d295c68d83cce40", + "sha256:6f947a9abc1a129858391b3d9334c45041c08a0f23d14333d5b844b6e5c17a07", + "sha256:782a771424fe74bc7e75c228a1da671578c2ba4ddb2ca09b8f959abdf787331e", + "sha256:7899a38d0ae7e817e99adb217f586d0a4620e315e4de577444ebeeed2c5729be", + "sha256:7b00f8c9065de3ad226f7979154a7b27f3b9151c8055c162332369262fc025d8", + "sha256:8f4b8e777d39013595a7740b4463e61b1cfe5f462f1b609b28fbc1e4c4ff01e5", + "sha256:90cbac1ec05b305a1b90ede61ef73126afdeb5a804ae04480d6da12c56378df1", + "sha256:918cdf8751b24986f915d743225ad6b702f83e1106e08a63b736e3a4c6ead789", + "sha256:9202f22ef811053077d01f43cc02b4aaf4472792f9fd0f5081b0b05c926cca19", + "sha256:94138682e68ec197db42ad7442d3cf9b328069c3ad8e4e5022e6b5cd3e7ffae5", + "sha256:968581d1717bbcf170758580f5f97a2925854943c45a19be4d47299507db2eb7", + "sha256:9d8d0642c63d453179058abc4143e30718b19a85cbf58c2744c9a63f06a1d388", + "sha256:a7ceb59986456ce851160867ce4929edaffbd2f069ae25717150199f8e1548b8", + "sha256:b9913c45d1be52d7a5db0c63977eebb51f68a2d5e6fd922d1d9b5e5fd758cc98", + "sha256:bde283313daf0b34a8d1bab30325f5cb0f4e11b5869dbe5bc61f8fe09a8f66f3", + "sha256:bf5b9c72b884c6f0c4ed26ef204ee1f768b9437330422492c319470954bc4cc7", + "sha256:ca80b121bbec76d7794fcb45e65a7eca660a76cc1a104ed439cdbd7df5f0b060", + "sha256:cdf66977a976d6a3cfb006afdf825d1482f84f7b81179db33941f2fc9673bb1d", + "sha256:d4faf846ed132fd7ebfbbf4fde588a62d21faa0faa06e6f468b7faa6f436b661", + "sha256:d7f87c2c02e03d99b95cfa6f7a776409083a9e4d468912e18c7680437b29222c", + "sha256:dd23df885318391856415e20acfd51a985cba6919f0be78ed89f5db9ff3a31cb", + "sha256:f5de3c676e57177b38857f6e3cdfbe8f38d1cd754b63200c0615eaa31f514b4f", + "sha256:f5e8e8d60e18d5f7fd49983f0c4696deeddaf6e608fbab33397671e2fcc6cc91", + "sha256:f7cac622e11b4253ac4536a654fe221249065d9a69feb6cdcd4d9af3503602e0", + "sha256:f8a04cf0c5b7139bc6368b461257d4a757ea2fe89b3773e494d235b7dd51119f", + "sha256:f8bb35ce57a63c9a6896c71a285818a3922d8ca05d150fd1fe49a7f57287b836", + "sha256:fbfdce91239fe306772faab57597186710d5699213f4df099d1612da7320d682" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==23.9.1" + "version": "==24.2.1" }, "geventconnpool": { "git": "https://github.com/rasky/geventconnpool.git", - "ref": "1bbb93a714a331a069adf27265fe582d9ba7ecd4" + "ref": null }, "greenlet": { "hashes": [ @@ -710,11 +712,11 @@ }, "marshmallow": { "hashes": [ - "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd", - "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9" + "sha256:20f53be28c6e374a711a16165fb22a8dc6003e3f7cda1285e3ca777b9193885b", + "sha256:e7997f83571c7fd476042c2c188e4ee8a78900ca5e74bd9c8097afa56624e9bd" ], "markers": "python_version >= '3.8'", - "version": "==3.20.2" + "version": "==3.21.0" }, "oic": { "hashes": [ @@ -742,10 +744,10 @@ }, "phonenumberslite": { "hashes": [ - "sha256:2b04a53401d01ab42564c1abc762fc9808ad398e71dacfa3b38d4321e112ecb3", - "sha256:74e3ee63dfa2bb562ce2e6ce74ce76ae74a2f81472005b80343235fb43426db4" + "sha256:137d53d5d78dca30bc2becf81a3e2ac74deb8f0997e9bbe44de515ece4bd92bd", + "sha256:e1f4359bff90c86d1b52db0e726d3334df00cc7d9c9c2ef66561d5f7a774d4ba" ], - "version": "==8.13.29" + "version": "==8.13.31" }, "psycopg2-binary": { "hashes": [ @@ -874,104 +876,104 @@ }, "pydantic": { "hashes": [ - "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f", - "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9" + "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a", + "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f" ], "markers": "python_version >= '3.8'", - "version": "==2.6.1" + "version": "==2.6.3" }, "pydantic-core": { "hashes": [ - "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379", - "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06", - "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05", - "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7", - "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753", - "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a", - "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731", - "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc", - "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380", - "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3", - "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c", - "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11", - "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990", - "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a", - "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2", - "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8", - "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97", - "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a", - "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8", - "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef", - "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77", - "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33", - "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82", - "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5", - "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b", - "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55", - "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e", - "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b", - "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7", - "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec", - "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc", - "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469", - "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b", - "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20", - "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e", - "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d", - "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f", - "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b", - "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039", - "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e", - "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2", - "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f", - "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b", - "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc", - "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8", - "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522", - "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e", - "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784", - "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a", - "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890", - "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485", - "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545", - "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f", - "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943", - "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878", - "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f", - "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17", - "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7", - "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286", - "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c", - "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb", - "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646", - "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978", - "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8", - "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15", - "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272", - "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2", - "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55", - "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf", - "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545", - "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4", - "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a", - "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804", - "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4", - "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0", - "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a", - "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113", - "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d", - "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25" + "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a", + "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed", + "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979", + "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff", + "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5", + "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45", + "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340", + "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad", + "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23", + "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6", + "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7", + "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241", + "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda", + "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187", + "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba", + "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c", + "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2", + "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c", + "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132", + "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf", + "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972", + "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db", + "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade", + "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4", + "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8", + "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f", + "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9", + "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48", + "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec", + "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d", + "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9", + "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb", + "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4", + "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89", + "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c", + "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9", + "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da", + "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac", + "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b", + "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf", + "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e", + "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137", + "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1", + "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b", + "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8", + "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e", + "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053", + "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01", + "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe", + "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd", + "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805", + "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183", + "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8", + "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99", + "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820", + "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074", + "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256", + "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8", + "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975", + "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad", + "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e", + "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca", + "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df", + "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b", + "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a", + "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a", + "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721", + "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a", + "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f", + "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2", + "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97", + "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6", + "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed", + "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc", + "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1", + "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe", + "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120", + "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f", + "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a" ], "markers": "python_version >= '3.8'", - "version": "==2.16.2" + "version": "==2.16.3" }, "pydantic-settings": { "hashes": [ - "sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c", - "sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a" + "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed", + "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091" ], "markers": "python_version >= '3.8'", - "version": "==2.1.0" + "version": "==2.2.1" }, "pyjwkest": { "hashes": [ @@ -996,6 +998,15 @@ "markers": "python_version >= '3.8'", "version": "==1.0.1" }, + "pyzipper": { + "hashes": [ + "sha256:0adca90a00c36a93fbe49bfa8c5add452bfe4ef85a1b8e3638739dd1c7b26bfc", + "sha256:6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87" + ], + "index": "pypi", + "markers": "python_version >= '3.4'", + "version": "==0.3.6" + }, "requests": { "hashes": [ "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", @@ -1015,11 +1026,11 @@ }, "setuptools": { "hashes": [ - "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", - "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" + "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56", + "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8" ], "markers": "python_version >= '3.8'", - "version": "==69.0.3" + "version": "==69.1.1" }, "six": { "hashes": [ @@ -1039,12 +1050,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", + "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.10.0" }, "urllib3": { "hashes": [ @@ -1073,45 +1084,45 @@ }, "zope.interface": { "hashes": [ - "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff", - "sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c", - "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac", - "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f", - "sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d", - "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309", - "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736", - "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179", - "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb", - "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941", - "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d", - "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92", - "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b", - "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41", - "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f", - "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3", - "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d", - "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8", - "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3", - "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1", - "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1", - "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40", - "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d", - "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1", - "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605", - "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7", - "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd", - "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43", - "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0", - "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b", - "sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379", - "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a", - "sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83", - "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56", - "sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9", - "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de" + "sha256:02adbab560683c4eca3789cc0ac487dcc5f5a81cc48695ec247f00803cafe2fe", + "sha256:14e02a6fc1772b458ebb6be1c276528b362041217b9ca37e52ecea2cbdce9fac", + "sha256:25e0af9663eeac6b61b231b43c52293c2cb7f0c232d914bdcbfd3e3bd5c182ad", + "sha256:2606955a06c6852a6cff4abeca38346ed01e83f11e960caa9a821b3626a4467b", + "sha256:396f5c94654301819a7f3a702c5830f0ea7468d7b154d124ceac823e2419d000", + "sha256:3b240883fb43160574f8f738e6d09ddbdbf8fa3e8cea051603d9edfd947d9328", + "sha256:3b6c62813c63c543a06394a636978b22dffa8c5410affc9331ce6cdb5bfa8565", + "sha256:4ae9793f114cee5c464cc0b821ae4d36e1eba961542c6086f391a61aee167b6f", + "sha256:4bce517b85f5debe07b186fc7102b332676760f2e0c92b7185dd49c138734b70", + "sha256:4d45d2ba8195850e3e829f1f0016066a122bfa362cc9dc212527fc3d51369037", + "sha256:4dd374927c00764fcd6fe1046bea243ebdf403fba97a937493ae4be2c8912c2b", + "sha256:506f5410b36e5ba494136d9fa04c548eaf1a0d9c442b0b0e7a0944db7620e0ab", + "sha256:59f7374769b326a217d0b2366f1c176a45a4ff21e8f7cebb3b4a3537077eff85", + "sha256:5ee9789a20b0081dc469f65ff6c5007e67a940d5541419ca03ef20c6213dd099", + "sha256:6fc711acc4a1c702ca931fdbf7bf7c86f2a27d564c85c4964772dadf0e3c52f5", + "sha256:75d2ec3d9b401df759b87bc9e19d1b24db73083147089b43ae748aefa63067ef", + "sha256:76e0531d86523be7a46e15d379b0e975a9db84316617c0efe4af8338dc45b80c", + "sha256:8af82afc5998e1f307d5e72712526dba07403c73a9e287d906a8aa2b1f2e33dd", + "sha256:8f5d2c39f3283e461de3655e03faf10e4742bb87387113f787a7724f32db1e48", + "sha256:97785604824981ec8c81850dd25c8071d5ce04717a34296eeac771231fbdd5cd", + "sha256:a3046e8ab29b590d723821d0785598e0b2e32b636a0272a38409be43e3ae0550", + "sha256:abb0b3f2cb606981c7432f690db23506b1db5899620ad274e29dbbbdd740e797", + "sha256:ac7c2046d907e3b4e2605a130d162b1b783c170292a11216479bb1deb7cadebe", + "sha256:af27b3fe5b6bf9cd01b8e1c5ddea0a0d0a1b8c37dc1c7452f1e90bf817539c6d", + "sha256:b386b8b9d2b6a5e1e4eadd4e62335571244cb9193b7328c2b6e38b64cfda4f0e", + "sha256:b66335bbdbb4c004c25ae01cc4a54fd199afbc1fd164233813c6d3c2293bb7e1", + "sha256:d54f66c511ea01b9ef1d1a57420a93fbb9d48a08ec239f7d9c581092033156d0", + "sha256:de125151a53ecdb39df3cb3deb9951ed834dd6a110a9e795d985b10bb6db4532", + "sha256:de7916380abaef4bb4891740879b1afcba2045aee51799dfd6d6ca9bdc71f35f", + "sha256:e2fefad268ff5c5b314794e27e359e48aeb9c8bb2cbb5748a071757a56f6bb8f", + "sha256:e7b2bed4eea047a949296e618552d3fed00632dc1b795ee430289bdd0e3717f3", + "sha256:e87698e2fea5ca2f0a99dff0a64ce8110ea857b640de536c76d92aaa2a91ff3a", + "sha256:ede888382882f07b9e4cd942255921ffd9f2901684198b88e247c7eabd27a000", + "sha256:f444de0565db46d26c9fa931ca14f497900a295bd5eba480fc3fad25af8c763e", + "sha256:fa994e8937e8ccc7e87395b7b35092818905cf27c651e3ff3e7f29729f5ce3ce", + "sha256:febceb04ee7dd2aef08c2ff3d6f8a07de3052fc90137c507b0ede3ea80c21440" ], "markers": "python_version >= '3.7'", - "version": "==6.1" + "version": "==6.2" } }, "develop": { @@ -1142,32 +1153,32 @@ }, "black": { "hashes": [ - "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8", - "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6", - "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62", - "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445", - "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c", - "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a", - "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9", - "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2", - "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6", - "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b", - "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4", - "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168", - "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d", - "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5", - "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024", - "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e", - "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b", - "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161", - "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717", - "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8", - "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac", - "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7" + "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8", + "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8", + "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd", + "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9", + "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31", + "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92", + "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f", + "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29", + "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4", + "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693", + "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218", + "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a", + "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23", + "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0", + "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982", + "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894", + "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540", + "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430", + "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b", + "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2", + "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6", + "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==24.1.1" + "version": "==24.2.0" }, "blinker": { "hashes": [ @@ -1179,12 +1190,12 @@ }, "boto3": { "hashes": [ - "sha256:65acfe7f1cf2a9b7df3d4edb87c8022e02685825bd1957e7bb678cc0d09f5e5f", - "sha256:73f5ec89cb3ddb3ed577317889fd2f2df783f66b6502a9a4239979607e33bf74" + "sha256:66303b5f26d92afb72656ff490b22ea72dfff8bf1a29e4a0c5d5f11ec56245dd", + "sha256:898ad2123b18cae8efd85adc56ac2d1925be54592aebc237020d4f16e9a9e7a9" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.37" + "version": "==1.34.52" }, "boto3-mocking": { "hashes": [ @@ -1197,28 +1208,28 @@ }, "boto3-stubs": { "hashes": [ - "sha256:97b5ca3d3145385acde5af46ca2da3fc74f433545034c36183f389e99771516e", - "sha256:c6618c7126bac0337c05e161e9c428febc57d6a24d7ff62de46e67761f402c57" + "sha256:644381a404fb5884154f7dcc40bb819f0c7f37de21b7a7b493585277b51c9a5f", + "sha256:823c41059f836d6877daaa1cbd20f813c8f1a78b9fdf290bc0b853127e127ba3" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.37" + "version": "==1.34.52" }, "botocore": { "hashes": [ - "sha256:2a5bf33aacd2d970afd3d492e179e06ea98a5469030d5cfe7a2ad9995f7bb2ef", - "sha256:3c46ddb1679e6ef45ca78b48665398636bda532a07cd476e4b500697d13d9a99" + "sha256:05567d8aba344826060481ea309555432c96f0febe22bee7cf5a3b6d3a03cec8", + "sha256:187da93aec3f2e87d8a31eced16fa2cb9c71fe2d69b0a797f9f7a9220f5bf7ae" ], "markers": "python_version >= '3.8'", - "version": "==1.34.37" + "version": "==1.34.52" }, "botocore-stubs": { "hashes": [ - "sha256:087cd42973edcb5527dc97eec87fa29fffecc39691249486e02045677d4a2dbe", - "sha256:d6bcea8a6872aa46d389027dc5c022241fd0a2047a8b858aa5005e6151ed30a7" + "sha256:8748b9fe01f66bb1e7b13f45e3336e2e2c5460d232816d45941573425459c66e", + "sha256:d0f4d9859d9f6affbe4b0b46e37fe729860eaab55ebefe7e09cf567396b2feda" ], "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==1.34.37" + "version": "==1.34.51" }, "click": { "hashes": [ @@ -1492,11 +1503,11 @@ }, "rich": { "hashes": [ - "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", - "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" + "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", + "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" ], "markers": "python_full_version >= '3.7.0'", - "version": "==13.7.0" + "version": "==13.7.1" }, "s3transfer": { "hashes": [ @@ -1532,11 +1543,11 @@ }, "stevedore": { "hashes": [ - "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d", - "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c" + "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9", + "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d" ], "markers": "python_version >= '3.8'", - "version": "==5.1.0" + "version": "==5.2.0" }, "tomli": { "hashes": [ @@ -1548,11 +1559,11 @@ }, "types-awscrt": { "hashes": [ - "sha256:06a859189a329ca8e66d56ceeef2391488e39b878fbd2141f115eab4d416fe22", - "sha256:f61a120d3e98ee1387bc5ca4b93437f258cc5c2af1f55f8634ec4cee5729f178" + "sha256:10245570c7285e949362b4ae710c54bf285d64a27453d42762477bcee5cd77a3", + "sha256:73be0a2720d6f76b924df6917d4edf4c9958f83e5c25bf7d9f0c1e9cdf836941" ], "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.20.3" + "version": "==0.20.4" }, "types-cachetools": { "hashes": [ @@ -1580,12 +1591,12 @@ }, "types-requests": { "hashes": [ - "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5", - "sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1" + "sha256:a82807ec6ddce8f00fe0e949da6d6bc1fbf1715420218a9640d695f70a9e5a9b", + "sha256:f1721dba8385958f504a5386240b92de4734e047a08a40751c1654d1ac3349c5" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.31.0.20240125" + "version": "==2.31.0.20240218" }, "types-s3transfer": { "hashes": [ @@ -1597,12 +1608,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", + "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.10.0" }, "urllib3": { "hashes": [ diff --git a/src/docker-compose.yml b/src/docker-compose.yml index fdf069f56..600347fa8 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -58,6 +58,8 @@ services: - AWS_S3_SECRET_ACCESS_KEY - AWS_S3_REGION - AWS_S3_BUCKET_NAME + # File encryption credentials + - SECRET_ENCRYPT_METADATA stdin_open: true tty: true ports: diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 009baa1c6..f763a71ce 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -74,6 +74,8 @@ secret_aws_s3_key_id = secret("access_key_id", None) or secret("AWS_S3_ACCESS_KE secret_aws_s3_key = secret("secret_access_key", None) or secret("AWS_S3_SECRET_ACCESS_KEY", None) secret_aws_s3_bucket_name = secret("bucket", None) or secret("AWS_S3_BUCKET_NAME", None) +secret_encrypt_metadata = secret("SECRET_ENCRYPT_METADATA", None) + secret_registry_cl_id = secret("REGISTRY_CL_ID") secret_registry_password = secret("REGISTRY_PASSWORD") secret_registry_cert = b64decode(secret("REGISTRY_CERT", "")) @@ -94,6 +96,7 @@ DEBUG = env_debug # Controls production specific feature toggles IS_PRODUCTION = env_is_production +SECRET_ENCRYPT_METADATA = secret_encrypt_metadata # Applications are modular pieces of code. # They are provided by Django, by third-parties, or by yourself. diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/generate_current_metadata_report.py index 69d111f4c..a27199cdb 100644 --- a/src/registrar/management/commands/generate_current_metadata_report.py +++ b/src/registrar/management/commands/generate_current_metadata_report.py @@ -5,6 +5,7 @@ import os import pyzipper from django.core.management import BaseCommand +from django.conf import settings from registrar.utility import csv_export from registrar.utility.s3_bucket import S3ClientHelper from ...utility.email import send_templated_email, EmailSendingError @@ -61,27 +62,28 @@ class Command(BaseCommand): We want to make sure to upload to s3 for back up And now we also want to get the file and encrypt it so we can send it in an email """ - unencrypted_metadata_input = s3_client.get_file(file_name) - # Encrypt metadata into a zip file # pre-setting zip file name encrypted_metadata_output = 'encrypted_metadata.zip' - # set this to be an env var somewhere - password = b'somepasswordhere' + + # Secret is encrypted into getgov-credentials + # TODO: Update secret in getgov-credentials via cloud.gov and my own .env when ready + # encrypted_metadata is the encrypted output - encrypted_metadata = _encrypt_metadata(unencrypted_metadata_input, encrypted_metadata_output, password) + encrypted_metadata = self._encrypt_metadata(s3_client.get_file(file_name), encrypted_metadata_output, str.encode(settings.SECRET_ENCRYPT_METADATA)) print("encrypted_metadata is:", encrypted_metadata) # Send the metadata file that is zipped # Q: Would we set the vars I set in email.py here to pass in to the helper function or best way to invoke # send_templated_email(encrypted_metadata, attachment=True) - def _encrypt_metadata(input_file, output_file, password): - with open(input_file, 'rb') as f_in: - with pyzipper.AESZipFile(output_file, 'w', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES) as f_out: - f_out.setpassword(password) - f_out.writestr(input_file, f_in.read()) - return output_file + def _encrypt_metadata(self, input_file, output_file, password): + # Using ZIP_DEFLATED bc it's a more common compression method supported by most zip utilities + # Could also use compression=pyzipper.ZIP_LZMA? + with pyzipper.AESZipFile(output_file, 'w', compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES) as f_out: + f_out.setpassword(password) + f_out.writestr('encrypted_metadata.txt', input_file) + return output_file diff --git a/src/requirements.txt b/src/requirements.txt index a6130a3bf..4b904cddd 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,14 +1,14 @@ -i https://pypi.python.org/simple annotated-types==0.6.0; python_version >= '3.8' asgiref==3.7.2; python_version >= '3.7' -boto3==1.34.37; python_version >= '3.8' -botocore==1.34.37; python_version >= '3.8' -cachetools==5.3.2; python_version >= '3.7' +boto3==1.34.52; python_version >= '3.8' +botocore==1.34.52; python_version >= '3.8' +cachetools==5.3.3; python_version >= '3.7' certifi==2024.2.2; python_version >= '3.6' cfenv==0.5.3 cffi==1.16.0; platform_python_implementation != 'PyPy' charset-normalizer==3.3.2; python_full_version >= '3.7.0' -cryptography==42.0.2; python_version >= '3.7' +cryptography==42.0.5; python_version >= '3.7' defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' dj-database-url==2.1.0 dj-email-url==1.0.6 @@ -23,12 +23,12 @@ django-login-required-middleware==0.9.0 django-phonenumber-field[phonenumberslite]==7.3.0; python_version >= '3.8' django-widget-tweaks==1.5.0; python_version >= '3.8' environs[django]==10.3.0; python_version >= '3.8' -faker==23.1.0; python_version >= '3.8' +faker==23.3.0; python_version >= '3.8' fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c furl==2.1.3 -future==0.18.3; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' -gevent==23.9.1; python_version >= '3.8' -geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4 +future==1.0.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' +gevent==24.2.1; python_version >= '3.8' +geventconnpool@ git+https://github.com/rasky/geventconnpool.git greenlet==3.0.3; python_version >= '3.7' gunicorn==21.2.0; python_version >= '3.5' idna==3.6; python_version >= '3.5' @@ -36,27 +36,28 @@ jmespath==1.0.1; python_version >= '3.7' lxml==5.1.0; python_version >= '3.6' mako==1.3.2; python_version >= '3.8' markupsafe==2.1.5; python_version >= '3.7' -marshmallow==3.20.2; python_version >= '3.8' +marshmallow==3.21.0; python_version >= '3.8' oic==1.6.1; python_version ~= '3.7' orderedmultidict==1.0.1 packaging==23.2; python_version >= '3.7' -phonenumberslite==8.13.29 +phonenumberslite==8.13.31 psycopg2-binary==2.9.9; python_version >= '3.7' pycparser==2.21 pycryptodomex==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -pydantic==2.6.1; python_version >= '3.8' -pydantic-core==2.16.2; python_version >= '3.8' -pydantic-settings==2.1.0; python_version >= '3.8' +pydantic==2.6.3; python_version >= '3.8' +pydantic-core==2.16.3; python_version >= '3.8' +pydantic-settings==2.2.1; python_version >= '3.8' pyjwkest==1.4.2 python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' python-dotenv==1.0.1; python_version >= '3.8' +pyzipper==0.3.6; python_version >= '3.4' requests==2.31.0; python_version >= '3.7' s3transfer==0.10.0; python_version >= '3.8' -setuptools==69.0.3; python_version >= '3.8' +setuptools==69.1.1; python_version >= '3.8' six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' sqlparse==0.4.4; python_version >= '3.5' -typing-extensions==4.9.0; python_version >= '3.8' +typing-extensions==4.10.0; python_version >= '3.8' urllib3==2.0.7; python_version >= '3.7' whitenoise==6.6.0; python_version >= '3.8' zope.event==5.0; python_version >= '3.7' -zope.interface==6.1; python_version >= '3.7' +zope.interface==6.2; python_version >= '3.7' From 7d65b2cf5cfea3a816e5fc44902f67a7d5993a4d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:15:21 -0700 Subject: [PATCH 08/51] JS changes --- src/registrar/assets/js/get-gov-admin.js | 66 ++++++++- .../admin/domain_application_change_form.html | 135 ++++++++++-------- 2 files changed, 135 insertions(+), 66 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 096f7a626..47351a608 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -35,14 +35,20 @@ function openInNewTab(el, removeAttribute = false){ let input = document.createElement("input"); input.type = "submit"; - input.name = button.name; - input.value = button.value; + + if(button.name){ + input.name = button.name; + } + + if(button.value){ + input.value = button.value; + } + input.style.display = "none" // Add the hidden input to the form form.appendChild(input); button.addEventListener("click", () => { - console.log("clicking") input.click(); }) }) @@ -50,6 +56,60 @@ function openInNewTab(el, removeAttribute = false){ createPhantomModalFormButtons(); })(); + +/** An IIFE for DomainApplication to hook a modal to a dropdown option. + * This intentionally does not interact with createPhantomModalFormButtons() +*/ +(function (){ + function displayModalOnDropdownClick(){ + // Grab the invisible element that will hook to the modal. + // This doesn't technically need to be done with one, but this is simpler to manage. + let linkClickedDisplaysModal = document.getElementById("invisible-ineligible-modal-toggler") + let statusDropdown = document.getElementById("id_status") + + // If these exist all at the same time, we're on the right page + if (linkClickedDisplaysModal && statusDropdown && statusDropdown.value){ + // Store the previous value in the event the user cancels. + // We only need to do this if we're on the correct page. + let previousValue = statusDropdown.value; + // Because the modal button does not have the class "dja-form-placeholder", + // it will not be affected by the createPhantomModalFormButtons() function. + let cancelButton = document.querySelector('button[name="_cancel_application_ineligible"]'); + if (cancelButton){ + console.log(`This is the previous val: ${previousValue}`) + cancelButton.addEventListener('click', function() { + // Revert the dropdown to its previous value + statusDropdown.value = previousValue; + }); + + // Add a change event listener to the dropdown. + statusDropdown.addEventListener('change', function() { + // Check if "Ineligible" is selected + if (this.value && this.value.toLowerCase() === "ineligible") { + // Display the modal. + linkClickedDisplaysModal.click() + } + + // Update previousValue if another option is selected and confirmed + previousValue = this.value; + console.log(`This is the previous val NOW: ${previousValue}`) + }); + + } else{ + console.error("displayModalOnDropdownClick() -> No cancel button defined.") + } + + } + } + + // Adds event listeners on the confirm and cancel modal buttons + function handleModalButtons(){ + + } + + displayModalOnDropdownClick(); +})(); + /** An IIFE for pages in DjangoAdmin which may need custom JS implementation. * Currently only appends target="_blank" to the domain_form object, * but this can be expanded. diff --git a/src/registrar/templates/django/admin/domain_application_change_form.html b/src/registrar/templates/django/admin/domain_application_change_form.html index 26abbec18..f6380fb82 100644 --- a/src/registrar/templates/django/admin/domain_application_change_form.html +++ b/src/registrar/templates/django/admin/domain_application_change_form.html @@ -1,6 +1,12 @@ {% extends 'admin/change_form.html' %} {% load i18n static %} +{% block field_sets %} + {# Create an invisible tag so that we can use a click event to toggle the modal. #} + + {{ block.super }} +{% endblock %} + {% block submit_buttons_bottom %} {% comment %} Modals behave very weirdly in django admin. @@ -16,72 +22,75 @@ In addition, the modal element MUST be placed low in the DOM. The script loads slower on DJA than on other portions of the application, so this means that it will briefly "populate", causing unintended visual effects. {% endcomment %} -{# Create a modal for when a domain is marked as ineligible #} -
-
-
- -
-

- When a domain request is in ineligible status, the registrant's permissions within the registrar are restricted as follows: -

-
    -
  • They cannot edit the ineligible request or any other pending requests.
  • -
  • They cannot manage any of their approved domains.
  • -
  • They cannot initiate a new domain request.
  • -
-

- This action can be reversed, if needed. -

-

- {# Acts as a
#} -

- Domain: {{ original.name }} - New status: {{ original.State }} -

-
+ {# Create a modal for when a domain is marked as ineligible #} +
+
+
+ +
+

+ When a domain request is in ineligible status, the registrant's permissions within the registrar are restricted as follows: +

+
    +
  • They cannot edit the ineligible request or any other pending requests.
  • +
  • They cannot manage any of their approved domains.
  • +
  • They cannot initiate a new domain request.
  • +
+

+ The restrictions will not take effect until you “save” the changes for this domain request. + This action can be reversed, if needed. +

+

+ {# Acts as a
#} +

+ Domain: {{ original.name }} + New status: {{ original.State }} +

+
- +
- -
{{ block.super }} {% endblock %} \ No newline at end of file From 7a7000cf9d358930280585cdb74c8f366ba815bd Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:31:08 -0700 Subject: [PATCH 09/51] Fix bug --- src/registrar/assets/js/get-gov-admin.js | 6 ------ .../django/admin/domain_application_change_form.html | 6 +++--- .../templates/django/admin/domain_change_form.html | 6 +++--- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 47351a608..ebcca16d7 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -91,7 +91,6 @@ function openInNewTab(el, removeAttribute = false){ } // Update previousValue if another option is selected and confirmed - previousValue = this.value; console.log(`This is the previous val NOW: ${previousValue}`) }); @@ -102,11 +101,6 @@ function openInNewTab(el, removeAttribute = false){ } } - // Adds event listeners on the confirm and cancel modal buttons - function handleModalButtons(){ - - } - displayModalOnDropdownClick(); })(); diff --git a/src/registrar/templates/django/admin/domain_application_change_form.html b/src/registrar/templates/django/admin/domain_application_change_form.html index f6380fb82..6e6ab3723 100644 --- a/src/registrar/templates/django/admin/domain_application_change_form.html +++ b/src/registrar/templates/django/admin/domain_application_change_form.html @@ -39,9 +39,9 @@ When a domain request is in ineligible status, the registrant's permissions within the registrar are restricted as follows:

    -
  • They cannot edit the ineligible request or any other pending requests.
  • -
  • They cannot manage any of their approved domains.
  • -
  • They cannot initiate a new domain request.
  • +
  • They cannot edit the ineligible request or any other pending requests.
  • +
  • They cannot manage any of their approved domains.
  • +
  • They cannot initiate a new domain request.

The restrictions will not take effect until you “save” the changes for this domain request. diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html index 61007c3d4..393983e32 100644 --- a/src/registrar/templates/django/admin/domain_change_form.html +++ b/src/registrar/templates/django/admin/domain_change_form.html @@ -133,9 +133,9 @@ When a domain is on hold:

    -
  • The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.
  • -
  • The domain will still appear in the registrar / admin.
  • -
  • Domain managers won’t be able to edit the domain.
  • +
  • The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.
  • +
  • The domain will still appear in the registrar / admin.
  • +
  • Domain managers won’t be able to edit the domain.

This action can be reversed, if needed. From 0ef72ad016a975f73922286723e0c5f0ebd3bd29 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 29 Feb 2024 12:05:19 -0700 Subject: [PATCH 10/51] Add custom desc on delete button --- src/registrar/admin.py | 12 +++++++++++ src/registrar/assets/js/get-gov-admin.js | 1 - .../admin/domain_application_change_form.html | 4 ++-- .../django/admin/domain_change_form.html | 4 ++-- .../admin/domain_delete_confirmation.html | 21 +++++++++++++++++++ 5 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 src/registrar/templates/django/admin/domain_delete_confirmation.html diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 942ae6162..fd6ce45a7 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1339,6 +1339,18 @@ class DomainAdmin(ListHeaderAdmin): # Table ordering ordering = ["name"] + def delete_view(self, request, object_id, extra_context=None): + """ + Custom delete_view to perform additional actions or customize the template. + """ + + # Set the delete template to a custom one + self.delete_confirmation_template = "django/admin/domain_delete_confirmation.html" + + response = super().delete_view(request, object_id, extra_context=extra_context) + + return response + def changeform_view(self, request, object_id=None, form_url="", extra_context=None): """Custom changeform implementation to pass in context information""" if extra_context is None: diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index ebcca16d7..8ecf2cbee 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -76,7 +76,6 @@ function openInNewTab(el, removeAttribute = false){ // it will not be affected by the createPhantomModalFormButtons() function. let cancelButton = document.querySelector('button[name="_cancel_application_ineligible"]'); if (cancelButton){ - console.log(`This is the previous val: ${previousValue}`) cancelButton.addEventListener('click', function() { // Revert the dropdown to its previous value statusDropdown.value = previousValue; diff --git a/src/registrar/templates/django/admin/domain_application_change_form.html b/src/registrar/templates/django/admin/domain_application_change_form.html index 6e6ab3723..f0e4cfe4f 100644 --- a/src/registrar/templates/django/admin/domain_application_change_form.html +++ b/src/registrar/templates/django/admin/domain_application_change_form.html @@ -48,10 +48,10 @@ This action can be reversed, if needed.

+ Domain: {{ original.requested_domain.name }} {# Acts as a
#}

- Domain: {{ original.name }} - New status: {{ original.State }} + New status: {{ original.ApplicationStatus.INELIGIBLE|capfirst }}

diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html index 393983e32..818522c8d 100644 --- a/src/registrar/templates/django/admin/domain_change_form.html +++ b/src/registrar/templates/django/admin/domain_change_form.html @@ -144,7 +144,7 @@ Domain: {{ original.name }} {# Acts as a
#}
- New status: {{ original.State.ON_HOLD }} + New status: {{ original.State.ON_HOLD|capfirst }}

@@ -211,7 +211,7 @@ Domain: {{ original.name }} {# Acts as a
#}
- New status: {{ original.State.DELETED }} + New status: {{ original.State.DELETED|capfirst }}

diff --git a/src/registrar/templates/django/admin/domain_delete_confirmation.html b/src/registrar/templates/django/admin/domain_delete_confirmation.html new file mode 100644 index 000000000..793a28c4c --- /dev/null +++ b/src/registrar/templates/django/admin/domain_delete_confirmation.html @@ -0,0 +1,21 @@ +{% extends 'admin/delete_confirmation.html' %} +{% load i18n static %} + +{% block content %} +{# TODO modify the "Are you sure?" to the text content below.. #} +{% comment %} +

Are you sure you want to remove this domain from the registry?

+{% endcomment %} +

Description

+

When a domain is removed from the registry:

+ +
    +
  • The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.
  • +
  • The domain will still appear in the registrar / admin.
  • +
  • Domain managers won’t be able to edit the domain.
  • +
+ +

This action cannot be undone.

+ +{{ block.super }} +{% endblock %} From 74f203448771cfbda77afce2279bd9f9b704a195 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 29 Feb 2024 22:17:48 -0800 Subject: [PATCH 11/51] Email business logic --- .../runbooks/update_python_dependencies.md | 6 +- .../generate_current_metadata_report.py | 20 ++-- src/registrar/utility/email.py | 110 ++++++++---------- 3 files changed, 64 insertions(+), 72 deletions(-) diff --git a/docs/operations/runbooks/update_python_dependencies.md b/docs/operations/runbooks/update_python_dependencies.md index 04fb936c6..468270d09 100644 --- a/docs/operations/runbooks/update_python_dependencies.md +++ b/docs/operations/runbooks/update_python_dependencies.md @@ -3,7 +3,7 @@ 1. Check the [Pipfile](../../../src/Pipfile) for pinned dependencies and manually adjust the version numbers 2. Run `docker-compose stop` to spin down the current containers and images so we can start afresh -2. Run +3. Run cd src docker-compose run app bash -c "pipenv lock && pipenv requirements > requirements.txt" @@ -13,9 +13,9 @@ It is necessary to use `bash -c` because `run pipenv requirements` will not recognize that it is running non-interactively and will include garbage formatting characters. The requirements.txt is used by Cloud.gov. It is needed to work around a bug in the CloudFoundry buildpack version of Pipenv that breaks on installing from a git repository. -3. Change geventconnpool back to what it was originally within the Pipfile.lock and requirements.txt. +4. Change geventconnpool back to what it was originally within the Pipfile.lock and requirements.txt. This is done by either saving what it was originally or opening a PR and using that as a reference to undo changes to any mention of geventconnpool. Geventconnpool, when set as a requirement without the reference portion, is defaulting to get a commit from 2014 which then breaks the code, as we want the newest version from them. -4. Run `docker-compose build` to build a new image for local development with the updated dependencies. +5. Run `docker-compose build` to build a new image for local development with the updated dependencies. The reason for de-coupling the `build` and `lock` steps is to increase consistency between builds--a run of `build` will always get exactly the dependencies listed in `Pipfile.lock`, nothing more, nothing less. \ No newline at end of file diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/generate_current_metadata_report.py index a27199cdb..1a33c2791 100644 --- a/src/registrar/management/commands/generate_current_metadata_report.py +++ b/src/registrar/management/commands/generate_current_metadata_report.py @@ -71,17 +71,23 @@ class Command(BaseCommand): # Secret is encrypted into getgov-credentials # TODO: Update secret in getgov-credentials via cloud.gov and my own .env when ready - # encrypted_metadata is the encrypted output + # Encrypt the metadata + # TODO: UPDATE SECRET_ENCRYPT_METADATA pw getgov-credentials on stable encrypted_metadata = self._encrypt_metadata(s3_client.get_file(file_name), encrypted_metadata_output, str.encode(settings.SECRET_ENCRYPT_METADATA)) print("encrypted_metadata is:", encrypted_metadata) - + print("the type is: ", type(encrypted_metadata)) # Send the metadata file that is zipped - # Q: Would we set the vars I set in email.py here to pass in to the helper function or best way to invoke - # send_templated_email(encrypted_metadata, attachment=True) - + # TODO: Make new .txt files + send_templated_email( + "emails/metadata_body.txt", + "emails/metadata_subject.txt", + to_address="rebecca.hsieh@truss.works", # TODO: Update to settings.DEFAULT_FROM_EMAIL once tested + file=encrypted_metadata, + ) + def _encrypt_metadata(self, input_file, output_file, password): - # Using ZIP_DEFLATED bc it's a more common compression method supported by most zip utilities - # Could also use compression=pyzipper.ZIP_LZMA? + # Using ZIP_DEFLATED bc it's a more common compression method supported by most zip utilities and faster + # We could also use compression=pyzipper.ZIP_LZMA if we are looking for smaller file size with pyzipper.AESZipFile(output_file, 'w', compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES) as f_out: f_out.setpassword(password) f_out.writestr('encrypted_metadata.txt', input_file) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 199a6c304..5f3e42eb5 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -4,6 +4,10 @@ import boto3 import logging from django.conf import settings from django.template.loader import get_template +from email.mime.base import MIMEBase +from email.mime.application import MIMEApplication +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText logger = logging.getLogger(__name__) @@ -15,7 +19,7 @@ class EmailSendingError(RuntimeError): pass -def send_templated_email(template_name: str, subject_template_name: str, to_address: str, context={}): +def send_templated_email(template_name: str, subject_template_name: str, to_address: str, context={}, file: str=None): """Send an email built from a template to one email address. template_name and subject_template_name are relative to the same template @@ -40,74 +44,56 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr except Exception as exc: raise EmailSendingError("Could not access the SES client.") from exc - # Are we okay with passing in "attachment" var in as boolean parameter - # If so, TODO: add attachment boolean to other functions try: - #if not attachment: - ses_client.send_email( - FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - Destination={"ToAddresses": [to_address]}, - Content={ - "Simple": { - "Subject": {"Data": subject}, - "Body": {"Text": {"Data": email_body}}, + if file is None: + ses_client.send_email( + FromEmailAddress=settings.DEFAULT_FROM_EMAIL, + Destination={"ToAddresses": [to_address]}, + Content={ + "Simple": { + "Subject": {"Data": subject}, + "Body": {"Text": {"Data": email_body}}, + }, }, - }, - ) - # else: # has attachment - # same as above but figure out how to attach a file - # via boto3 "boto3 SES file attachment" - # we also want this to only send to the help email - - # from email.mime.multipart import MIMEMultipart - # from email.mime.text import MIMEText - # from email.mime.application import MIMEApplication + ) + if file is not None: + # TODO: Update sender email when we figure out + ses_client = boto3.client( + "ses", + region_name=settings.AWS_REGION, + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + config=settings.BOTO_CONFIG, + ) - # sender_email = 'sender@example.com' - # recipient_email = 'help@get.gov' - # subject = 'DOTGOV-Full Domain Metadata' - # body = 'Domain metadata email, should have an attachment included change here later.' - # attachment_path = 'path/to/attachment/file.pdf' - # aws_region = 'sesv2' - - # response = send_email_with_attachment(sender_email, recipient_email, subject, body, attachment_path, aws_region) - # print(response) + #TODO: Update sender to settings.DEFAULT_FROM_EMAIL + response = send_email_with_attachment(settings.DEFAULT_FROM_EMAIL, to_address, subject, email_body, file, ses_client) + print("Response from send_email_with_attachment_is:", response) except Exception as exc: raise EmailSendingError("Could not send SES email.") from exc +def send_email_with_attachment(sender, recipient, subject, body, attachment_file, ses_client): + # Create a multipart/mixed parent container + msg = MIMEMultipart('mixed') + msg['Subject'] = subject + msg['From'] = sender + msg['To'] = recipient -# def send_email_with_attachment(sender, recipient, subject, body, attachment_path, aws_region): - # # Create a multipart/mixed parent container - # msg = MIMEMultipart('mixed') - # msg['Subject'] = subject - # msg['From'] = sender_email - # msg['To'] = recipient_email + # Add the text part + text_part = MIMEText(body, 'plain') + msg.attach(text_part) - # # Add the text part - # text_part = MIMEText(body, 'plain') - # msg.attach(text_part) + # Add the attachment part - # # Add the attachment part - # with open(attachment_path, 'rb') as attachment_file: - # attachment_data = attachment_file.read() - # attachment_part = MIMEApplication(attachment_data) - # attachment_part.add_header('Content-Disposition', f'attachment; filename="{attachment_path}"') - # msg.attach(attachment_part) + # set it into this "type" + attachment_part = MIMEApplication(attachment_file) + # Adding attachment header + filename that the attachment will be called + attachment_part.add_header('Content-Disposition', f'attachment; filename="encrypted_metadata.zip"') + msg.attach(attachment_part) - # # Send the email - # response = ses_client.send_raw_email( - # Source=sender, - # Destinations=[recipient], - # RawMessage={'Data': msg.as_string()} - # ) - - # ses_client.send_email( - # FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - # Destination={"ToAddresses": [to_address]}, - # Content={ - # "Simple": { - # "Subject": {"Data": subject}, - # "Body": {"Text": {"Data": email_body}}, - # }, - # }, - # ) \ No newline at end of file + response = ses_client.send_raw_email( + Source=sender, + Destinations=[recipient], + RawMessage={"Data": msg.as_string()} + ) + return response From 8408ea0f68452a7c4840991ca6195c39bf5945f1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 1 Mar 2024 14:37:20 -0700 Subject: [PATCH 12/51] Add some test cases --- src/registrar/tests/test_admin.py | 143 +++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index d76f12f35..3c5861ee1 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -230,6 +230,20 @@ class TestDomainAdmin(MockEppLib, WebTest): ) mock_add_message.assert_has_calls([expected_call], 1) + def test_custom_delete_confirmation_page(self): + """Tests if we override the delete confirmation page for custom content""" + # Create a ready domain with a preset expiration date + domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + + domain_change_page = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk])) + + self.assertContains(domain_change_page, "fake.gov") + # click the "Manage" link + confirmation_page = domain_change_page.click("Delete", index=0) + + content_slice = "

When a domain is removed from the registry:

" + self.assertContains(confirmation_page, content_slice) + def test_short_org_name_in_domains_list(self): """ Make sure the short name is displaying in admin on the list page @@ -309,6 +323,17 @@ class TestDomainAdmin(MockEppLib, WebTest): self.assertEqual(response.status_code, 200) self.assertContains(response, domain.name) self.assertContains(response, "Remove from registry") + + # The contents of the modal should exist before and after the post. + # Check for the header + self.assertContains(response, "Are you sure you want to remove this domain from the registry?") + + # Check for some of its body + self.assertContains(response, "When a domain is removed from the registry:") + + # Check for some of the button content + self.assertContains(response, "Yes, remove from registry") + # Test the info dialog request = self.factory.post( "/admin/registrar/domain/{}/change/".format(domain.pk), @@ -325,8 +350,60 @@ class TestDomainAdmin(MockEppLib, WebTest): extra_tags="", fail_silently=False, ) + + # The modal should still exist + self.assertContains(response, "Are you sure you want to remove this domain from the registry?") + self.assertContains(response, "When a domain is removed from the registry:") + self.assertContains(response, "Yes, remove from registry") + self.assertEqual(domain.state, Domain.State.DELETED) + def test_on_hold_is_successful_web_test(self): + """ + Scenario: Domain on_hold is successful through webtest + """ + with less_console_noise(): + domain = create_ready_domain() + + response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk])) + + # Check the contents of the modal + # Check for the header + self.assertContains(response, "Are you sure you want to place this domain on hold?") + + # Check for some of its body + self.assertContains(response, "When a domain is on hold:") + + # Check for some of the button content + self.assertContains(response, "Yes, place hold") + + # Grab the form to submit + form = response.forms["domain_form"] + + # Submit the form + response = form.submit("_place_client_hold") + + # Follow the response + response = response.follow() + + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + self.assertContains(response, "Remove hold") + + # The modal should still exist + # Check for the header + self.assertContains(response, "Are you sure you want to place this domain on hold?") + + # Check for some of its body + self.assertContains(response, "When a domain is on hold:") + + # Check for some of the button content + self.assertContains(response, "Yes, place hold") + + # Web test has issues grabbing up to date data from the db, so we can test + # the returned view instead + self.assertContains(response, '
On hold
') + def test_deletion_ready_fsm_failure(self): """ Scenario: Domain deletion is unsuccessful @@ -440,6 +517,7 @@ class TestDomainAdmin(MockEppLib, WebTest): class TestDomainApplicationAdminForm(TestCase): + def setUp(self): # Create a test application with an initial state of started self.application = completed_application() @@ -1101,7 +1179,10 @@ class TestDomainApplicationAdmin(MockEppLib): application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + request = self.factory.post( + "/admin/registrar/domainapplication/{}/change/".format(application.pk), + follow=True + ) with boto3_mocking.clients.handler_for("sesv2", self.mock_client): # Modify the application's property @@ -1113,6 +1194,66 @@ class TestDomainApplicationAdmin(MockEppLib): # Test that approved domain exists and equals requested domain self.assertEqual(application.creator.status, "restricted") + def test_user_sets_restricted_status_modal(self): + """Tests the modal for when a user sets the status to restricted""" + with less_console_noise(): + # make sure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + p = "userpass" + self.client.login(username="staffuser", password=p) + response = self.client.get( + "/admin/registrar/domainapplication/{}/change/".format(application.pk), + follow=True, + ) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, application.requested_domain.name) + + + # Check that the modal has the right content + # Check for the header + self.assertContains(response, "Are you sure you want to select ineligible status?") + + # Check for some of its body + self.assertContains(response, "When a domain request is in ineligible status") + + # Check for some of the button content + self.assertContains(response, "Yes, select ineligible status") + + # Create a mock request + request = self.factory.post( + "/admin/registrar/domainapplication/{}/change/".format(application.pk), + follow=True + ) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + # Modify the application's property + application.status = DomainApplication.ApplicationStatus.INELIGIBLE + + # Use the model admin's save_model method + self.admin.save_model(request, application, form=None, change=True) + + # Test that approved domain exists and equals requested domain + self.assertEqual(application.creator.status, "restricted") + + # 'Get' to the application again + response = self.client.get( + "/admin/registrar/domainapplication/{}/change/".format(application.pk), + follow=True, + ) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, application.requested_domain.name) + + # The modal should be unchanged + self.assertContains(response, "Are you sure you want to select ineligible status?") + self.assertContains(response, "When a domain request is in ineligible status") + self.assertContains(response, "Yes, select ineligible status") + def test_readonly_when_restricted_creator(self): with less_console_noise(): application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) From 73673abae1484b84cdf1bd3e3f34ad88bc8c678a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 4 Mar 2024 08:37:52 -0700 Subject: [PATCH 13/51] Minor JS refactor --- src/registrar/assets/js/get-gov-admin.js | 61 +++++++++++++----------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 8ecf2cbee..e632903bb 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -61,46 +61,51 @@ function openInNewTab(el, removeAttribute = false){ * This intentionally does not interact with createPhantomModalFormButtons() */ (function (){ - function displayModalOnDropdownClick(){ - // Grab the invisible element that will hook to the modal. - // This doesn't technically need to be done with one, but this is simpler to manage. - let linkClickedDisplaysModal = document.getElementById("invisible-ineligible-modal-toggler") - let statusDropdown = document.getElementById("id_status") + function displayModalOnDropdownClick(linkClickedDisplaysModal, statusDropdown, cancelButton, valueToCheck){ // If these exist all at the same time, we're on the right page if (linkClickedDisplaysModal && statusDropdown && statusDropdown.value){ - // Store the previous value in the event the user cancels. - // We only need to do this if we're on the correct page. - let previousValue = statusDropdown.value; - // Because the modal button does not have the class "dja-form-placeholder", - // it will not be affected by the createPhantomModalFormButtons() function. - let cancelButton = document.querySelector('button[name="_cancel_application_ineligible"]'); + if (cancelButton){ + // Store the previous value in the event the user cancels. + // We only need to do this if cancel button is specified. + let previousValue = statusDropdown.value; cancelButton.addEventListener('click', function() { // Revert the dropdown to its previous value statusDropdown.value = previousValue; }); - - // Add a change event listener to the dropdown. - statusDropdown.addEventListener('change', function() { - // Check if "Ineligible" is selected - if (this.value && this.value.toLowerCase() === "ineligible") { - // Display the modal. - linkClickedDisplaysModal.click() - } - - // Update previousValue if another option is selected and confirmed - console.log(`This is the previous val NOW: ${previousValue}`) - }); - - } else{ - console.error("displayModalOnDropdownClick() -> No cancel button defined.") + }else { + console.log("displayModalOnDropdownClick() -> Cancel button was null") } - + + // Add a change event listener to the dropdown. + statusDropdown.addEventListener('change', function() { + // Check if "Ineligible" is selected + if (this.value && this.value.toLowerCase() === valueToCheck) { + // Display the modal. + linkClickedDisplaysModal.click() + } + }); + }else{ + console.error("displayModalOnDropdownClick() -> Some inputs are null") } } - displayModalOnDropdownClick(); + // When the status dropdown is clicked and is set to "ineligible", toggle a confirmation dropdown. + function hookModalToIneligibleStatus(){ + // Grab the invisible element that will hook to the modal. + // This doesn't technically need to be done with one, but this is simpler to manage. + let modalButton = document.getElementById("invisible-ineligible-modal-toggler") + let statusDropdown = document.getElementById("id_status") + + // Because the modal button does not have the class "dja-form-placeholder", + // it will not be affected by the createPhantomModalFormButtons() function. + let cancelButton = document.querySelector('button[name="_cancel_application_ineligible"]'); + let valueToCheck = "ineligible" + displayModalOnDropdownClick(modalButton, statusDropdown, cancelButton, valueToCheck); + } + + hookModalToIneligibleStatus() })(); /** An IIFE for pages in DjangoAdmin which may need custom JS implementation. From ac1886a2b5db7e3469c142c6ba7e545440f7246f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:28:44 -0700 Subject: [PATCH 14/51] Override for table view --- src/registrar/admin.py | 4 +++- src/registrar/assets/js/get-gov-admin.js | 2 -- .../domain_delete_selected_confirmation.html | 21 +++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/registrar/templates/django/admin/domain_delete_selected_confirmation.html diff --git a/src/registrar/admin.py b/src/registrar/admin.py index fd6ce45a7..2ffc29cda 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1339,6 +1339,9 @@ class DomainAdmin(ListHeaderAdmin): # Table ordering ordering = ["name"] + # Override for the delete confirmation page on the domain table (bulk delete action) + delete_selected_confirmation_template = "django/admin/domain_delete_selected_confirmation.html" + def delete_view(self, request, object_id, extra_context=None): """ Custom delete_view to perform additional actions or customize the template. @@ -1346,7 +1349,6 @@ class DomainAdmin(ListHeaderAdmin): # Set the delete template to a custom one self.delete_confirmation_template = "django/admin/domain_delete_confirmation.html" - response = super().delete_view(request, object_id, extra_context=extra_context) return response diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index e632903bb..4ed1a0d28 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -86,8 +86,6 @@ function openInNewTab(el, removeAttribute = false){ linkClickedDisplaysModal.click() } }); - }else{ - console.error("displayModalOnDropdownClick() -> Some inputs are null") } } diff --git a/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html b/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html new file mode 100644 index 000000000..b8d9eb25c --- /dev/null +++ b/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html @@ -0,0 +1,21 @@ +{% extends 'admin/delete_selected_confirmation.html' %} +{% load i18n static %} + +{% block content %} +{# TODO modify the "Are you sure?" to the text content below.. #} +{% comment %} +

Are you sure you want to remove this domain from the registry?

+{% endcomment %} +

Description

+

When a domain is removed from the registry:

+ +
    +
  • The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.
  • +
  • The domain will still appear in the registrar / admin.
  • +
  • Domain managers won’t be able to edit the domain.
  • +
+ +

This action cannot be undone.

+ +{{ block.super }} +{% endblock %} \ No newline at end of file From 27cd468bcafb303590546909316272bd2dc718fb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:33:25 -0700 Subject: [PATCH 15/51] Add better confirmation messages --- src/registrar/assets/sass/_theme/_admin.scss | 9 +++++ .../admin/domain_delete_confirmation.html | 38 ++++++++++-------- .../domain_delete_selected_confirmation.html | 39 +++++++++++-------- 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 46e4a10d4..9f6db0c46 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -311,3 +311,12 @@ input.admin-confirm-button { margin-top: 0; max-width: 68ex; } + +.django-admin-custom-bullets { + // Set list-style-type to inherit without modifying text size + list-style-type: inherit; +} + +.usa-summary-box__dhs-color { + color: $dhs-blue-70; +} diff --git a/src/registrar/templates/django/admin/domain_delete_confirmation.html b/src/registrar/templates/django/admin/domain_delete_confirmation.html index 793a28c4c..5a9bef5b0 100644 --- a/src/registrar/templates/django/admin/domain_delete_confirmation.html +++ b/src/registrar/templates/django/admin/domain_delete_confirmation.html @@ -2,20 +2,26 @@ {% load i18n static %} {% block content %} -{# TODO modify the "Are you sure?" to the text content below.. #} -{% comment %} -

Are you sure you want to remove this domain from the registry?

-{% endcomment %} -

Description

-

When a domain is removed from the registry:

- -
    -
  • The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.
  • -
  • The domain will still appear in the registrar / admin.
  • -
  • Domain managers won’t be able to edit the domain.
  • -
- -

This action cannot be undone.

- -{{ block.super }} +
+
+

+ When a domain is deleted: +

+
+
    +
  • The domain will no longer appear in the registrar / admin.
  • +
  • It will be removed from the registry.
  • +
  • The domain and its subdomains won’t resolve in DNS.
  • +
  • Any infrastructure (like websites) will go offline.
  • +
+

You should probably remove this domain from the registry instead of deleting it.

+

This action cannot be undone.

+
+
+
+ {{ block.super }} {% endblock %} diff --git a/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html b/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html index b8d9eb25c..61eb20f85 100644 --- a/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html +++ b/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html @@ -1,21 +1,28 @@ {% extends 'admin/delete_selected_confirmation.html' %} {% load i18n static %} + {% block content %} -{# TODO modify the "Are you sure?" to the text content below.. #} -{% comment %} -

Are you sure you want to remove this domain from the registry?

-{% endcomment %} -

Description

-

When a domain is removed from the registry:

- -
    -
  • The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.
  • -
  • The domain will still appear in the registrar / admin.
  • -
  • Domain managers won’t be able to edit the domain.
  • -
- -

This action cannot be undone.

- -{{ block.super }} +
+
+

+ When a domain is deleted: +

+
+
    +
  • The domain will no longer appear in the registrar / admin.
  • +
  • It will be removed from the registry.
  • +
  • The domain and its subdomains won’t resolve in DNS.
  • +
  • Any infrastructure (like websites) will go offline.
  • +
+

You should probably remove these domains from the registry instead of deleting them.

+

This action cannot be undone.

+
+
+
+ {{ block.super }} {% endblock %} \ No newline at end of file From d8bb86fe641e87af2eaf92209cfd2b6975f2c8c8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:48:57 -0700 Subject: [PATCH 16/51] Update domain_delete_selected_confirmation.html --- .../django/admin/domain_delete_selected_confirmation.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html b/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html index 61eb20f85..3e0a32a4d 100644 --- a/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html +++ b/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html @@ -19,7 +19,7 @@
  • The domain and its subdomains won’t resolve in DNS.
  • Any infrastructure (like websites) will go offline.
  • -

    You should probably remove these domains from the registry instead of deleting them.

    +

    You should probably remove these domains from the registry instead.

    This action cannot be undone.

    From 7e4dc38b40e7ce92cf9ed426f49a7e4c6ebc345d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:50:03 -0700 Subject: [PATCH 17/51] Change bullet list style --- src/registrar/assets/sass/_theme/_admin.scss | 2 +- .../templates/django/admin/domain_delete_confirmation.html | 2 +- .../django/admin/domain_delete_selected_confirmation.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 9f6db0c46..88675cb32 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -312,7 +312,7 @@ input.admin-confirm-button { max-width: 68ex; } -.django-admin-custom-bullets { +.django-admin-custom-bullets ul > li { // Set list-style-type to inherit without modifying text size list-style-type: inherit; } diff --git a/src/registrar/templates/django/admin/domain_delete_confirmation.html b/src/registrar/templates/django/admin/domain_delete_confirmation.html index 5a9bef5b0..2836c32f7 100644 --- a/src/registrar/templates/django/admin/domain_delete_confirmation.html +++ b/src/registrar/templates/django/admin/domain_delete_confirmation.html @@ -11,7 +11,7 @@

    When a domain is deleted:

    -
    +
    • The domain will no longer appear in the registrar / admin.
    • It will be removed from the registry.
    • diff --git a/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html b/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html index 3e0a32a4d..6872ea9af 100644 --- a/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html +++ b/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html @@ -12,7 +12,7 @@

      When a domain is deleted:

      -
      +
      • The domain will no longer appear in the registrar / admin.
      • It will be removed from the registry.
      • From cc1555ab9a76f42455cd995d8cf45daeda1fb785 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:08:52 -0700 Subject: [PATCH 18/51] Unit tests --- src/registrar/assets/sass/_theme/_admin.scss | 5 ----- .../django/admin/domain_delete_confirmation.html | 2 +- .../domain_delete_selected_confirmation.html | 2 +- src/registrar/tests/test_admin.py | 15 ++++++--------- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 88675cb32..cbec4d1f2 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -312,11 +312,6 @@ input.admin-confirm-button { max-width: 68ex; } -.django-admin-custom-bullets ul > li { - // Set list-style-type to inherit without modifying text size - list-style-type: inherit; -} - .usa-summary-box__dhs-color { color: $dhs-blue-70; } diff --git a/src/registrar/templates/django/admin/domain_delete_confirmation.html b/src/registrar/templates/django/admin/domain_delete_confirmation.html index 2836c32f7..5a9bef5b0 100644 --- a/src/registrar/templates/django/admin/domain_delete_confirmation.html +++ b/src/registrar/templates/django/admin/domain_delete_confirmation.html @@ -11,7 +11,7 @@

        When a domain is deleted:

        -
        +
        • The domain will no longer appear in the registrar / admin.
        • It will be removed from the registry.
        • diff --git a/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html b/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html index 6872ea9af..3e0a32a4d 100644 --- a/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html +++ b/src/registrar/templates/django/admin/domain_delete_selected_confirmation.html @@ -12,7 +12,7 @@

          When a domain is deleted:

          -
          +
          • The domain will no longer appear in the registrar / admin.
          • It will be removed from the registry.
          • diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 3c5861ee1..f85396f10 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -241,7 +241,7 @@ class TestDomainAdmin(MockEppLib, WebTest): # click the "Manage" link confirmation_page = domain_change_page.click("Delete", index=0) - content_slice = "

            When a domain is removed from the registry:

            " + content_slice = "When a domain is deleted:" self.assertContains(confirmation_page, content_slice) def test_short_org_name_in_domains_list(self): @@ -350,7 +350,7 @@ class TestDomainAdmin(MockEppLib, WebTest): extra_tags="", fail_silently=False, ) - + # The modal should still exist self.assertContains(response, "Are you sure you want to remove this domain from the registry?") self.assertContains(response, "When a domain is removed from the registry:") @@ -364,7 +364,7 @@ class TestDomainAdmin(MockEppLib, WebTest): """ with less_console_noise(): domain = create_ready_domain() - + response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk])) # Check the contents of the modal @@ -389,7 +389,7 @@ class TestDomainAdmin(MockEppLib, WebTest): self.assertEqual(response.status_code, 200) self.assertContains(response, domain.name) self.assertContains(response, "Remove hold") - + # The modal should still exist # Check for the header self.assertContains(response, "Are you sure you want to place this domain on hold?") @@ -1180,8 +1180,7 @@ class TestDomainApplicationAdmin(MockEppLib): # Create a mock request request = self.factory.post( - "/admin/registrar/domainapplication/{}/change/".format(application.pk), - follow=True + "/admin/registrar/domainapplication/{}/change/".format(application.pk), follow=True ) with boto3_mocking.clients.handler_for("sesv2", self.mock_client): @@ -1214,7 +1213,6 @@ class TestDomainApplicationAdmin(MockEppLib): self.assertEqual(response.status_code, 200) self.assertContains(response, application.requested_domain.name) - # Check that the modal has the right content # Check for the header self.assertContains(response, "Are you sure you want to select ineligible status?") @@ -1227,8 +1225,7 @@ class TestDomainApplicationAdmin(MockEppLib): # Create a mock request request = self.factory.post( - "/admin/registrar/domainapplication/{}/change/".format(application.pk), - follow=True + "/admin/registrar/domainapplication/{}/change/".format(application.pk), follow=True ) with boto3_mocking.clients.handler_for("sesv2", self.mock_client): # Modify the application's property From 747af8834791b1fa1fee8c0694088eb3964fbef2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:18:59 -0700 Subject: [PATCH 19/51] Add missing unit test + linting --- src/registrar/tests/common.py | 28 ++++++++++++++++++++++++---- src/registrar/tests/test_admin.py | 25 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index ee1ab8b68..1825d38fd 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -97,7 +97,7 @@ def less_console_noise(output_stream=None): class GenericTestHelper(TestCase): """A helper class that contains various helper functions for TestCases""" - def __init__(self, admin, model=None, url=None, user=None, factory=None, **kwargs): + def __init__(self, admin, model=None, url=None, user=None, factory=None, client=None, **kwargs): """ Parameters: admin (ModelAdmin): The Django ModelAdmin instance associated with the model. @@ -112,6 +112,7 @@ class GenericTestHelper(TestCase): self.admin = admin self.model = model self.url = url + self.client = client def assert_table_sorted(self, o_index, sort_fields): """ @@ -147,9 +148,7 @@ class GenericTestHelper(TestCase): dummy_request.user = self.user # Mock a user request - middleware = SessionMiddleware(lambda req: req) - middleware.process_request(dummy_request) - dummy_request.session.save() + dummy_request = self._mock_user_request_for_factory(dummy_request) expected_sort_order = list(self.model.objects.order_by(*sort_fields)) @@ -160,6 +159,27 @@ class GenericTestHelper(TestCase): self.assertEqual(expected_sort_order, returned_sort_order) + def _mock_user_request_for_factory(self, request): + """Adds sessionmiddleware when using factory to associate session information""" + middleware = SessionMiddleware(lambda req: req) + middleware.process_request(request) + request.session.save() + return request + + def get_table_delete_confirmation_page(self, selected_across: str, index: str): + """ + Grabs the response for the delete confirmation page (generated from the actions toolbar). + selected_across and index must both be numbers encoded as str, e.g. "0" rather than 0 + """ + + response = self.client.post( + self.url, + {"action": "delete_selected", "select_across": selected_across, "index": index, "_selected_action": "23"}, + follow=True, + ) + print(f"what is the response? {response}") + return response + class MockUserLogin: def __init__(self, get_response): diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f85396f10..7e032ff5c 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -61,6 +61,16 @@ class TestDomainAdmin(MockEppLib, WebTest): self.factory = RequestFactory() self.app.set_user(self.superuser.username) self.client.force_login(self.superuser) + + # Contains some test tools + self.test_helper = GenericTestHelper( + factory=self.factory, + user=self.superuser, + admin=self.admin, + url=reverse("admin:registrar_domain_changelist"), + model=Domain, + client=self.client, + ) super().setUp() @skip("TODO for another ticket. This test case is grabbing old db data.") @@ -244,6 +254,21 @@ class TestDomainAdmin(MockEppLib, WebTest): content_slice = "When a domain is deleted:" self.assertContains(confirmation_page, content_slice) + def test_custom_delete_confirmation_page_table(self): + """Tests if we override the delete confirmation page for custom content on the table""" + # Create a ready domain + domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + + # Get the index. The post expects the index to be encoded as a string + index = f"{domain.id}" + + # Simulate selecting a single record, then clicking "Delete selected domains" + response = self.test_helper.get_table_delete_confirmation_page("0", index) + + # Check that our content exists + content_slice = "When a domain is deleted:" + self.assertContains(response, content_slice) + def test_short_org_name_in_domains_list(self): """ Make sure the short name is displaying in admin on the list page From f427076598e42c27b9f4bb65fb9b6181dbb88f60 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 4 Mar 2024 16:45:05 -0800 Subject: [PATCH 20/51] Fix emailing functionality and update subject and body and file names --- .../generate_current_metadata_report.py | 59 +++++++++++-------- .../templates/emails/metadata_body.txt | 1 + .../templates/emails/metadata_subject.txt | 2 + src/registrar/utility/email.py | 37 ++++++------ 4 files changed, 55 insertions(+), 44 deletions(-) create mode 100644 src/registrar/templates/emails/metadata_body.txt create mode 100644 src/registrar/templates/emails/metadata_subject.txt diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/generate_current_metadata_report.py index 1a33c2791..023a19f10 100644 --- a/src/registrar/management/commands/generate_current_metadata_report.py +++ b/src/registrar/management/commands/generate_current_metadata_report.py @@ -4,6 +4,8 @@ import logging import os import pyzipper +from datetime import datetime + from django.core.management import BaseCommand from django.conf import settings from registrar.utility import csv_export @@ -13,9 +15,11 @@ from ...utility.email import send_templated_email, EmailSendingError logger = logging.getLogger(__name__) + class Command(BaseCommand): help = ( - "Generates and uploads a current-metadata.csv file to our S3 bucket " "which is based off of all existing Domains." + "Generates and uploads a current-metadata.csv file to our S3 bucket " + "which is based off of all existing Domains." ) def add_arguments(self, parser): @@ -26,7 +30,7 @@ class Command(BaseCommand): default=True, help="Flag that determines if we do a check for os.path.exists. Used for test cases", ) - + def handle(self, **options): """Grabs the directory then creates current-metadata.csv in that directory""" file_name = "current-metadata.csv" @@ -58,38 +62,43 @@ class Command(BaseCommand): # Upload this generated file for our S3 instance s3_client.upload_file(file_path, file_name) - """ - We want to make sure to upload to s3 for back up - And now we also want to get the file and encrypt it so we can send it in an email - """ - # Encrypt metadata into a zip file + # Set zip file name + current_date = datetime.now().strftime("%m%d%Y") + current_filename = f"domain-metadata-{current_date}.zip" + # Pre-set zip file name + encrypted_metadata_output = current_filename - # pre-setting zip file name - encrypted_metadata_output = 'encrypted_metadata.zip' + # Set context for the subject + current_date_str = datetime.now().strftime("%Y-%m-%d") - # Secret is encrypted into getgov-credentials # TODO: Update secret in getgov-credentials via cloud.gov and my own .env when ready - - # Encrypt the metadata - # TODO: UPDATE SECRET_ENCRYPT_METADATA pw getgov-credentials on stable - encrypted_metadata = self._encrypt_metadata(s3_client.get_file(file_name), encrypted_metadata_output, str.encode(settings.SECRET_ENCRYPT_METADATA)) - print("encrypted_metadata is:", encrypted_metadata) - print("the type is: ", type(encrypted_metadata)) + + # Encrypt the metadata + encrypted_metadata_in_bytes = self._encrypt_metadata( + s3_client.get_file(file_name), encrypted_metadata_output, str.encode(settings.SECRET_ENCRYPT_METADATA) + ) + # Send the metadata file that is zipped - # TODO: Make new .txt files send_templated_email( - "emails/metadata_body.txt", - "emails/metadata_subject.txt", - to_address="rebecca.hsieh@truss.works", # TODO: Update to settings.DEFAULT_FROM_EMAIL once tested - file=encrypted_metadata, + template_name="emails/metadata_body.txt", + subject_template_name="emails/metadata_subject.txt", + to_address=settings.DEFAULT_FROM_EMAIL, + # to_address="rebecca.hsieh@truss.works ", # TODO: Update to settings.DEFAULT_FROM_EMAIL once tested + context={"current_date_str": current_date_str}, + file=encrypted_metadata_in_bytes, ) def _encrypt_metadata(self, input_file, output_file, password): + current_date = datetime.now().strftime("%m%d%Y") + current_filename = f"domain-metadata-{current_date}.txt" # Using ZIP_DEFLATED bc it's a more common compression method supported by most zip utilities and faster # We could also use compression=pyzipper.ZIP_LZMA if we are looking for smaller file size - with pyzipper.AESZipFile(output_file, 'w', compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES) as f_out: + with pyzipper.AESZipFile( + output_file, "w", compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES + ) as f_out: f_out.setpassword(password) - f_out.writestr('encrypted_metadata.txt', input_file) - return output_file - + f_out.writestr(current_filename, input_file) + with open(output_file, "rb") as file_data: + attachment_in_bytes = file_data.read() + return attachment_in_bytes diff --git a/src/registrar/templates/emails/metadata_body.txt b/src/registrar/templates/emails/metadata_body.txt new file mode 100644 index 000000000..adf0a186c --- /dev/null +++ b/src/registrar/templates/emails/metadata_body.txt @@ -0,0 +1 @@ +An export of all .gov metadata. diff --git a/src/registrar/templates/emails/metadata_subject.txt b/src/registrar/templates/emails/metadata_subject.txt new file mode 100644 index 000000000..5fdece7ef --- /dev/null +++ b/src/registrar/templates/emails/metadata_subject.txt @@ -0,0 +1,2 @@ +Domain metadata - {{current_date_str}} + diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 5f3e42eb5..a81a41716 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -2,6 +2,7 @@ import boto3 import logging +from datetime import datetime from django.conf import settings from django.template.loader import get_template from email.mime.base import MIMEBase @@ -19,7 +20,7 @@ class EmailSendingError(RuntimeError): pass -def send_templated_email(template_name: str, subject_template_name: str, to_address: str, context={}, file: str=None): +def send_templated_email(template_name: str, subject_template_name: str, to_address: str, context={}, file: str = None): """Send an email built from a template to one email address. template_name and subject_template_name are relative to the same template @@ -56,8 +57,7 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr }, }, ) - if file is not None: - # TODO: Update sender email when we figure out + else: ses_client = boto3.client( "ses", region_name=settings.AWS_REGION, @@ -65,35 +65,34 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, config=settings.BOTO_CONFIG, ) - - #TODO: Update sender to settings.DEFAULT_FROM_EMAIL - response = send_email_with_attachment(settings.DEFAULT_FROM_EMAIL, to_address, subject, email_body, file, ses_client) + # Define the subject line with the current date + response = send_email_with_attachment( + settings.DEFAULT_FROM_EMAIL, to_address, subject, email_body, file, ses_client + ) + # TODO: Remove this print statement print("Response from send_email_with_attachment_is:", response) except Exception as exc: raise EmailSendingError("Could not send SES email.") from exc + def send_email_with_attachment(sender, recipient, subject, body, attachment_file, ses_client): # Create a multipart/mixed parent container - msg = MIMEMultipart('mixed') - msg['Subject'] = subject - msg['From'] = sender - msg['To'] = recipient + msg = MIMEMultipart("mixed") + msg["Subject"] = subject + msg["From"] = sender + msg["To"] = recipient # Add the text part - text_part = MIMEText(body, 'plain') + text_part = MIMEText(body, "plain") msg.attach(text_part) # Add the attachment part - - # set it into this "type" attachment_part = MIMEApplication(attachment_file) # Adding attachment header + filename that the attachment will be called - attachment_part.add_header('Content-Disposition', f'attachment; filename="encrypted_metadata.zip"') + current_date = datetime.now().strftime("%m%d%Y") + current_filename = f"domain-metadata-{current_date}.zip" + attachment_part.add_header("Content-Disposition", f'attachment; filename="{current_filename}"') msg.attach(attachment_part) - response = ses_client.send_raw_email( - Source=sender, - Destinations=[recipient], - RawMessage={"Data": msg.as_string()} - ) + response = ses_client.send_raw_email(Source=sender, Destinations=[recipient], RawMessage={"Data": msg.as_string()}) return response From 9ca8322510c185006dc1d43f3468420204fd456a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 5 Mar 2024 11:52:48 -0800 Subject: [PATCH 21/51] Update docs, csv set up, and add unit test --- .../runbooks/rotate_application_secrets.md | 6 ++++ .../generate_current_metadata_report.py | 8 ++--- src/registrar/tests/test_emails.py | 33 ++++++++++++++++++- src/registrar/utility/email.py | 4 +-- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/docs/operations/runbooks/rotate_application_secrets.md b/docs/operations/runbooks/rotate_application_secrets.md index a776e60b8..f7a5004ef 100644 --- a/docs/operations/runbooks/rotate_application_secrets.md +++ b/docs/operations/runbooks/rotate_application_secrets.md @@ -117,3 +117,9 @@ You'll need to give the new certificate to the registry vendor _before_ rotating ## REGISTRY_HOSTNAME This is the hostname at which the registry can be found. + +## SECRET_METADATA_KEY + +This is in reference to the key for the metadata email that is sent daily. Reach out to product team members or leads with access to security passwords if the passcode is needed. + +To change the password, use a password generator to generate a password, then update the user credentials per the above instructions. Be sure to update the `KDBX` file in Google Drive with this password change. diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/generate_current_metadata_report.py index 023a19f10..2478f9e6b 100644 --- a/src/registrar/management/commands/generate_current_metadata_report.py +++ b/src/registrar/management/commands/generate_current_metadata_report.py @@ -72,7 +72,7 @@ class Command(BaseCommand): # Set context for the subject current_date_str = datetime.now().strftime("%Y-%m-%d") - # TODO: Update secret in getgov-credentials via cloud.gov and my own .env when ready + # TODO: Update secret in getgov-credentials via cloud.gov and my own .env when merging # Encrypt the metadata encrypted_metadata_in_bytes = self._encrypt_metadata( @@ -83,15 +83,15 @@ class Command(BaseCommand): send_templated_email( template_name="emails/metadata_body.txt", subject_template_name="emails/metadata_subject.txt", - to_address=settings.DEFAULT_FROM_EMAIL, - # to_address="rebecca.hsieh@truss.works ", # TODO: Update to settings.DEFAULT_FROM_EMAIL once tested + # to_address=settings.DEFAULT_FROM_EMAIL, # TODO: Uncomment this when ready to merge + to_address="rebecca.hsieh@truss.works ", context={"current_date_str": current_date_str}, file=encrypted_metadata_in_bytes, ) def _encrypt_metadata(self, input_file, output_file, password): current_date = datetime.now().strftime("%m%d%Y") - current_filename = f"domain-metadata-{current_date}.txt" + current_filename = f"domain-metadata-{current_date}.csv" # Using ZIP_DEFLATED bc it's a more common compression method supported by most zip utilities and faster # We could also use compression=pyzipper.ZIP_LZMA if we are looking for smaller file size with pyzipper.AESZipFile( diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index f2a94a186..292fe5b1c 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -5,7 +5,8 @@ from unittest.mock import MagicMock from django.test import TestCase from .common import completed_application, less_console_noise - +from datetime import datetime +from registrar.utility import email import boto3_mocking # type: ignore @@ -182,3 +183,33 @@ class TestEmails(TestCase): self.assertNotIn("Anything else", body) # spacing should be right between adjacent elements self.assertRegex(body, r"5557\n\n----") + + @boto3_mocking.patching + def test_send_email_with_attachment(self): + with boto3_mocking.clients.handler_for("ses", self.mock_client_class): + sender_email = "sender@example.com" + recipient_email = "recipient@example.com" + subject = "Test Subject" + body = "Test Body" + attachment_file = b"Attachment file content" + current_date = datetime.now().strftime("%m%d%Y") + current_filename = f"domain-metadata-{current_date}.zip" + + response = email.send_email_with_attachment( + sender_email, recipient_email, subject, body, attachment_file, self.mock_client + ) + print("response is", response) + # Assert that the `send_raw_email` method of the mocked SES client was called with the expected params + self.mock_client.send_raw_email.assert_called_once() + + # Get the args passed to the `send_raw_email` method + call_args = self.mock_client.send_raw_email.call_args[1] + print("call_args is", call_args) + + # Assert that the attachment filename is correct + self.assertEqual(call_args["RawMessage"]["Data"].count(f'filename="{current_filename}"'), 1) + + # Assert that the attachment content is encrypted + self.assertIn("Content-Type: application/octet-stream", call_args["RawMessage"]["Data"]) + self.assertIn("Content-Transfer-Encoding: base64", call_args["RawMessage"]["Data"]) + self.assertIn("Content-Disposition: attachment;", call_args["RawMessage"]["Data"]) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index a81a41716..ddd211041 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -65,11 +65,11 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, config=settings.BOTO_CONFIG, ) - # Define the subject line with the current date response = send_email_with_attachment( settings.DEFAULT_FROM_EMAIL, to_address, subject, email_body, file, ses_client ) - # TODO: Remove this print statement + # TODO: Remove this print statement when ready to merge, + # leaving rn for getting error codes in case print("Response from send_email_with_attachment_is:", response) except Exception as exc: raise EmailSendingError("Could not send SES email.") from exc From cf97531102df74ec00655f61e6d87a5216086b1b Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 5 Mar 2024 11:56:20 -0800 Subject: [PATCH 22/51] Fix missing brackets --- src/registrar/utility/email.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 8960be001..72bbbabc7 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -59,7 +59,8 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr "Simple": { "Subject": {"Data": subject}, "Body": {"Text": {"Data": email_body}}, - }, + }, + } ) else: ses_client = boto3.client( From d61e17a4adcb70ce38b7b8145325027540cbc38c Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 5 Mar 2024 12:03:11 -0800 Subject: [PATCH 23/51] Fix conflicts and linting --- .../commands/generate_current_metadata_report.py | 2 +- src/registrar/tests/test_emails.py | 2 -- src/registrar/utility/email.py | 7 ++++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/generate_current_metadata_report.py index 2478f9e6b..a3c7f70e1 100644 --- a/src/registrar/management/commands/generate_current_metadata_report.py +++ b/src/registrar/management/commands/generate_current_metadata_report.py @@ -10,7 +10,7 @@ from django.core.management import BaseCommand from django.conf import settings from registrar.utility import csv_export from registrar.utility.s3_bucket import S3ClientHelper -from ...utility.email import send_templated_email, EmailSendingError +from ...utility.email import send_templated_email logger = logging.getLogger(__name__) diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index 292fe5b1c..2c480cec2 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -198,13 +198,11 @@ class TestEmails(TestCase): response = email.send_email_with_attachment( sender_email, recipient_email, subject, body, attachment_file, self.mock_client ) - print("response is", response) # Assert that the `send_raw_email` method of the mocked SES client was called with the expected params self.mock_client.send_raw_email.assert_called_once() # Get the args passed to the `send_raw_email` method call_args = self.mock_client.send_raw_email.call_args[1] - print("call_args is", call_args) # Assert that the attachment filename is correct self.assertEqual(call_args["RawMessage"]["Data"].count(f'filename="{current_filename}"'), 1) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 72bbbabc7..2fb08d10c 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -5,7 +5,6 @@ import logging from datetime import datetime from django.conf import settings from django.template.loader import get_template -from email.mime.base import MIMEBase from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -20,7 +19,9 @@ class EmailSendingError(RuntimeError): pass -def send_templated_email(template_name: str, subject_template_name: str, to_address: str, bcc_address="", context={}, file: str = None): +def send_templated_email( + template_name: str, subject_template_name: str, to_address: str, bcc_address="", context={}, file: str = None +): """Send an email built from a template to one email address. template_name and subject_template_name are relative to the same template @@ -60,7 +61,7 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr "Subject": {"Data": subject}, "Body": {"Text": {"Data": email_body}}, }, - } + }, ) else: ses_client = boto3.client( From c5f83b937d5e9d4f8c48edf848b0ea148a5e90d3 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 5 Mar 2024 12:21:41 -0800 Subject: [PATCH 24/51] Fix test --- src/registrar/tests/test_emails.py | 2 +- src/registrar/utility/email.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index 2c480cec2..99db0d644 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -195,7 +195,7 @@ class TestEmails(TestCase): current_date = datetime.now().strftime("%m%d%Y") current_filename = f"domain-metadata-{current_date}.zip" - response = email.send_email_with_attachment( + email.send_email_with_attachment( sender_email, recipient_email, subject, body, attachment_file, self.mock_client ) # Assert that the `send_raw_email` method of the mocked SES client was called with the expected params diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 2fb08d10c..35a4ecf03 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -51,11 +51,10 @@ def send_templated_email( destination["BccAddresses"] = [bcc_address] try: - if file is None: ses_client.send_email( FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - Destination={"ToAddresses": [to_address]}, + Destination=destination, Content={ "Simple": { "Subject": {"Data": subject}, From 2ba38848a67b78537f35da13277e430e520d34ef Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 5 Mar 2024 15:15:38 -0800 Subject: [PATCH 25/51] Update comment on why we're uploading to S3 instance --- .../commands/generate_current_metadata_report.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/generate_current_metadata_report.py index a3c7f70e1..209ed5cd5 100644 --- a/src/registrar/management/commands/generate_current_metadata_report.py +++ b/src/registrar/management/commands/generate_current_metadata_report.py @@ -48,8 +48,11 @@ class Command(BaseCommand): logger.info(f"Success! Created {file_name}") def generate_current_metadata_report(self, directory, file_name, check_path): - """Creates a current-full.csv file under the specified directory, - then uploads it to a AWS S3 bucket""" + """Creates a current-metadata.csv file under the specified directory, + then uploads it to a AWS S3 bucket. This is done for resiliency + reasons in the event our application goes down and/or the email + cannot send -- we'll still be able to grab info from the S3 + instance""" s3_client = S3ClientHelper() file_path = os.path.join(directory, file_name) @@ -60,7 +63,6 @@ class Command(BaseCommand): if check_path and not os.path.exists(file_path): raise FileNotFoundError(f"Could not find newly created file at '{file_path}'") - # Upload this generated file for our S3 instance s3_client.upload_file(file_path, file_name) # Set zip file name From ce98272b4841d8186cd53e089827e02bc69f041c Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 6 Mar 2024 11:45:50 -0800 Subject: [PATCH 26/51] Update pipfile --- src/Pipfile | 1 - src/Pipfile.lock | 12 ++++-------- src/requirements.txt | 1 - 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Pipfile b/src/Pipfile index b40a8c3ea..b9c5d72d4 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -29,7 +29,6 @@ django-login-required-middleware = "*" greenlet = "*" gevent = "*" fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"} -geventconnpool = {git = "https://github.com/rasky/geventconnpool.git", ref = "1bbb93a714a331a069adf27265fe582d9ba7ecd4"} pyzipper="*" tblib = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 789422c5b..4eb2c0fb3 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8094a1c9461f860e928b51542adf891c0f6f6c4c62bd1bd8ac3bba55a67f918d" + "sha256": "082a951f15bb26a28f2dca7e0840fdf61518b3d90c42d77a310f982344cbd1dc" }, "pipfile-spec": 6, "requires": {}, @@ -458,10 +458,6 @@ "markers": "python_version >= '3.8'", "version": "==24.2.1" }, - "geventconnpool": { - "git": "https://github.com/rasky/geventconnpool.git", - "ref": "1bbb93a714a331a069adf27265fe582d9ba7ecd4" - }, "greenlet": { "hashes": [ "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67", @@ -1217,12 +1213,12 @@ }, "boto3-stubs": { "hashes": [ - "sha256:5ee40bdfba94fcdba26f36869339c849e918827ed1fb2f8e470474e6b1e923ff", - "sha256:cbbae1b811b97e4e1f1d00eba237ff987678e652502226b87e6276f7963935b4" + "sha256:627f8eca69d832581ee1676d39df099a2a2e3a86d6b3ebd21c81c5f11ed6a6fa", + "sha256:a87e7ecbab6235ec371b4363027e57483bca349a9cd5c891f40db81dadfa273e" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.55" + "version": "==1.34.56" }, "botocore": { "hashes": [ diff --git a/src/requirements.txt b/src/requirements.txt index b24e3575a..1db089f5a 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -28,7 +28,6 @@ fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a furl==2.1.3 future==1.0.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' gevent==24.2.1; python_version >= '3.8' -geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4 greenlet==3.0.3; python_version >= '3.7' gunicorn==21.2.0; python_version >= '3.5' idna==3.6; python_version >= '3.5' From bac44a98149d1a446895374a4a1932223061d9a5 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 6 Mar 2024 15:54:05 -0800 Subject: [PATCH 27/51] Address variable and wording feedback --- .github/workflows/daily-csv-upload.yaml | 4 ++-- .../operations/runbooks/rotate_application_secrets.md | 6 ++++-- src/Pipfile | 3 +-- src/registrar/config/settings.py | 1 + .../commands/generate_current_metadata_report.py | 4 +++- src/registrar/tests/test_emails.py | 1 + src/registrar/utility/email.py | 11 ++++++++--- 7 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/workflows/daily-csv-upload.yaml b/.github/workflows/daily-csv-upload.yaml index 84881398e..2a57c2083 100644 --- a/.github/workflows/daily-csv-upload.yaml +++ b/.github/workflows/daily-csv-upload.yaml @@ -31,12 +31,12 @@ jobs: cf_space: ${{ secrets.CF_REPORT_ENV }} cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py generate_current_full_report' --name full" - - name: Generate current-metadata.csv + - name: Generate and email domain-metadata-.csv uses: cloud-gov/cg-cli-tools@main with: cf_username: ${{ secrets[env.CF_USERNAME] }} cf_password: ${{ secrets[env.CF_PASSWORD] }} cf_org: cisa-dotgov cf_space: ${{ secrets.CF_REPORT_ENV }} - cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py generate_current_metadata_report' --name metadata" + cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py email_current_metadata_report' --name metadata" diff --git a/docs/operations/runbooks/rotate_application_secrets.md b/docs/operations/runbooks/rotate_application_secrets.md index f7a5004ef..1094b4ff7 100644 --- a/docs/operations/runbooks/rotate_application_secrets.md +++ b/docs/operations/runbooks/rotate_application_secrets.md @@ -120,6 +120,8 @@ This is the hostname at which the registry can be found. ## SECRET_METADATA_KEY -This is in reference to the key for the metadata email that is sent daily. Reach out to product team members or leads with access to security passwords if the passcode is needed. +This is the passphrase for the zipped and encrypted metadata email that is sent out daily. Reach out to product team members or leads with access to security passwords if the passcode is needed. + +To change the password, use a password generator to generate a password, then update the user credentials per the above instructions. Be sure to update the [KBDX](https://docs.google.com/document/d/1_BbJmjYZNYLNh4jJPPnUEG9tFCzJrOc0nMrZrnSKKyw) file in Google Drive with this password change. + -To change the password, use a password generator to generate a password, then update the user credentials per the above instructions. Be sure to update the `KDBX` file in Google Drive with this password change. diff --git a/src/Pipfile b/src/Pipfile index b9c5d72d4..9208fada5 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -45,5 +45,4 @@ django-webtest = "*" types-cachetools = "*" boto3-mocking = "*" boto3-stubs = "*" -django-model2puml = "*" - +django-model2puml = "*" \ No newline at end of file diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 65b372fac..e5b97748a 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -74,6 +74,7 @@ secret_aws_s3_key_id = secret("access_key_id", None) or secret("AWS_S3_ACCESS_KE secret_aws_s3_key = secret("secret_access_key", None) or secret("AWS_S3_SECRET_ACCESS_KEY", None) secret_aws_s3_bucket_name = secret("bucket", None) or secret("AWS_S3_BUCKET_NAME", None) +# Passphrase for the encrypted metadata email secret_encrypt_metadata = secret("SECRET_ENCRYPT_METADATA", None) secret_registry_cl_id = secret("REGISTRY_CL_ID") diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/generate_current_metadata_report.py index 209ed5cd5..103ce0dab 100644 --- a/src/registrar/management/commands/generate_current_metadata_report.py +++ b/src/registrar/management/commands/generate_current_metadata_report.py @@ -45,7 +45,7 @@ class Command(BaseCommand): # TODO - #1317: Notify operations when auto report generation fails raise err else: - logger.info(f"Success! Created {file_name}") + logger.info(f"Success! Created {file_name} and successfully sent out an email!") def generate_current_metadata_report(self, directory, file_name, check_path): """Creates a current-metadata.csv file under the specified directory, @@ -68,6 +68,7 @@ class Command(BaseCommand): # Set zip file name current_date = datetime.now().strftime("%m%d%Y") current_filename = f"domain-metadata-{current_date}.zip" + # Pre-set zip file name encrypted_metadata_output = current_filename @@ -92,6 +93,7 @@ class Command(BaseCommand): ) def _encrypt_metadata(self, input_file, output_file, password): + """Helper function for encrypting the attachment file""" current_date = datetime.now().strftime("%m%d%Y") current_filename = f"domain-metadata-{current_date}.csv" # Using ZIP_DEFLATED bc it's a more common compression method supported by most zip utilities and faster diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index 99db0d644..b11f21da6 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -211,3 +211,4 @@ class TestEmails(TestCase): self.assertIn("Content-Type: application/octet-stream", call_args["RawMessage"]["Data"]) self.assertIn("Content-Transfer-Encoding: base64", call_args["RawMessage"]["Data"]) self.assertIn("Content-Disposition: attachment;", call_args["RawMessage"]["Data"]) + self.assertNotIn("Attachment file content", call_args["RawMessage"]["Data"]) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 35a4ecf03..91d55f361 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -20,7 +20,12 @@ class EmailSendingError(RuntimeError): def send_templated_email( - template_name: str, subject_template_name: str, to_address: str, bcc_address="", context={}, file: str = None + template_name: str, + subject_template_name: str, + to_address: str, + bcc_address="", + context={}, + attachment_file: str = None, ): """Send an email built from a template to one email address. @@ -51,7 +56,7 @@ def send_templated_email( destination["BccAddresses"] = [bcc_address] try: - if file is None: + if attachment_file is None: ses_client.send_email( FromEmailAddress=settings.DEFAULT_FROM_EMAIL, Destination=destination, @@ -71,7 +76,7 @@ def send_templated_email( config=settings.BOTO_CONFIG, ) response = send_email_with_attachment( - settings.DEFAULT_FROM_EMAIL, to_address, subject, email_body, file, ses_client + settings.DEFAULT_FROM_EMAIL, to_address, subject, email_body, attachment_file, ses_client ) # TODO: Remove this print statement when ready to merge, # leaving rn for getting error codes in case From 370b2ddda068bf77971f7eccbfa40ec4e0a0be7a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 6 Mar 2024 16:05:41 -0800 Subject: [PATCH 28/51] Fix variable name --- .../management/commands/generate_current_metadata_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/generate_current_metadata_report.py index 103ce0dab..da5aafb91 100644 --- a/src/registrar/management/commands/generate_current_metadata_report.py +++ b/src/registrar/management/commands/generate_current_metadata_report.py @@ -89,7 +89,7 @@ class Command(BaseCommand): # to_address=settings.DEFAULT_FROM_EMAIL, # TODO: Uncomment this when ready to merge to_address="rebecca.hsieh@truss.works ", context={"current_date_str": current_date_str}, - file=encrypted_metadata_in_bytes, + attachment_file=encrypted_metadata_in_bytes, ) def _encrypt_metadata(self, input_file, output_file, password): From 52f299d06e304cca79026037facee6794d5f1bb7 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 7 Mar 2024 12:35:10 -0500 Subject: [PATCH 29/51] initial form changes - wip --- src/registrar/admin.py | 58 +++++++--------------- src/registrar/models/domain_application.py | 4 +- src/registrar/models/domain_information.py | 2 +- 3 files changed, 21 insertions(+), 43 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 92e477667..0ea791181 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -777,18 +777,21 @@ class DomainInformationAdmin(ListHeaderAdmin): search_help_text = "Search by domain." fieldsets = [ - (None, {"fields": ["creator", "domain_application", "notes"]}), + (None, {"fields": ["creator", "submitter", "domain_application", "notes"]}), + (".gov domain", {"fields": ["domain"]}), + ("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}), + ("Background info", {"fields": ["anything_else"]}), ( "Type of organization", { "fields": [ "organization_type", + "is_election_board", + "federal_type", + "federal_agency", + "tribe_name", "federally_recognized_tribe", "state_recognized_tribe", - "tribe_name", - "federal_agency", - "federal_type", - "is_election_board", "about_your_organization", ] }, @@ -798,28 +801,15 @@ class DomainInformationAdmin(ListHeaderAdmin): { "fields": [ "organization_name", + "state_territory", "address_line1", "address_line2", "city", - "state_territory", "zipcode", "urbanization", ] }, ), - ("Authorizing official", {"fields": ["authorizing_official"]}), - (".gov domain", {"fields": ["domain"]}), - ("Your contact information", {"fields": ["submitter"]}), - ("Other employees from your organization?", {"fields": ["other_contacts"]}), - ( - "No other employees from your organization?", - {"fields": ["no_other_contacts_rationale"]}, - ), - ("Anything else?", {"fields": ["anything_else"]}), - ( - "Requirements for operating a .gov domain", - {"fields": ["is_policy_acknowledged"]}, - ), ] # Read only that we'll leverage for CISA Analysts @@ -979,18 +969,21 @@ class DomainApplicationAdmin(ListHeaderAdmin): search_help_text = "Search by domain or submitter." fieldsets = [ - (None, {"fields": ["status", "rejection_reason", "investigator", "creator", "approved_domain", "notes"]}), + (None, {"fields": ["status", "rejection_reason", "investigator", "creator", "submitter", "approved_domain", "notes"]}), + (".gov domain", {"fields": ["requested_domain", "alternative_domains"]}), + ("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}), + ("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}), ( "Type of organization", { "fields": [ "organization_type", + "is_election_board", + "federal_type", + "federal_agency", + "tribe_name", "federally_recognized_tribe", "state_recognized_tribe", - "tribe_name", - "federal_agency", - "federal_type", - "is_election_board", "about_your_organization", ] }, @@ -1000,30 +993,15 @@ class DomainApplicationAdmin(ListHeaderAdmin): { "fields": [ "organization_name", + "state_territory", "address_line1", "address_line2", "city", - "state_territory", "zipcode", "urbanization", ] }, ), - ("Authorizing official", {"fields": ["authorizing_official"]}), - ("Current websites", {"fields": ["current_websites"]}), - (".gov domain", {"fields": ["requested_domain", "alternative_domains"]}), - ("Purpose of your domain", {"fields": ["purpose"]}), - ("Your contact information", {"fields": ["submitter"]}), - ("Other employees from your organization?", {"fields": ["other_contacts"]}), - ( - "No other employees from your organization?", - {"fields": ["no_other_contacts_rationale"]}, - ), - ("Anything else?", {"fields": ["anything_else"]}), - ( - "Requirements for operating a .gov domain", - {"fields": ["is_policy_acknowledged"]}, - ), ] # Read only that we'll leverage for CISA Analysts diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 8c417b51a..90627a63b 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -504,7 +504,7 @@ class DomainApplication(TimeStampedModel): "registrar.Website", blank=True, related_name="current+", - verbose_name="websites", + verbose_name="Current websites", ) approved_domain = models.OneToOneField( @@ -550,7 +550,7 @@ class DomainApplication(TimeStampedModel): "registrar.Contact", blank=True, related_name="contact_applications", - verbose_name="contacts", + verbose_name="Other employees", ) no_other_contacts_rationale = models.TextField( diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 861171c5c..5e17cfd2c 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -183,7 +183,7 @@ class DomainInformation(TimeStampedModel): "registrar.Contact", blank=True, related_name="contact_applications_information", - verbose_name="contacts", + verbose_name="Other employees", ) no_other_contacts_rationale = models.TextField( From aa26f782d039920f4937c3b818962a2d1b5d2490 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 7 Mar 2024 12:43:40 -0500 Subject: [PATCH 30/51] lint and migrations --- src/registrar/admin.py | 15 +++++++- ...inapplication_current_websites_and_more.py | 37 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/registrar/migrations/0073_alter_domainapplication_current_websites_and_more.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 0ea791181..f03fe6713 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -969,7 +969,20 @@ class DomainApplicationAdmin(ListHeaderAdmin): search_help_text = "Search by domain or submitter." fieldsets = [ - (None, {"fields": ["status", "rejection_reason", "investigator", "creator", "submitter", "approved_domain", "notes"]}), + ( + None, + { + "fields": [ + "status", + "rejection_reason", + "investigator", + "creator", + "submitter", + "approved_domain", + "notes", + ] + }, + ), (".gov domain", {"fields": ["requested_domain", "alternative_domains"]}), ("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}), ("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}), diff --git a/src/registrar/migrations/0073_alter_domainapplication_current_websites_and_more.py b/src/registrar/migrations/0073_alter_domainapplication_current_websites_and_more.py new file mode 100644 index 000000000..cefaba27c --- /dev/null +++ b/src/registrar/migrations/0073_alter_domainapplication_current_websites_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.10 on 2024-03-07 17:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0072_alter_publiccontact_fax_alter_publiccontact_voice"), + ] + + operations = [ + migrations.AlterField( + model_name="domainapplication", + name="current_websites", + field=models.ManyToManyField( + blank=True, related_name="current+", to="registrar.website", verbose_name="Current websites" + ), + ), + migrations.AlterField( + model_name="domainapplication", + name="other_contacts", + field=models.ManyToManyField( + blank=True, related_name="contact_applications", to="registrar.contact", verbose_name="Other employees" + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="other_contacts", + field=models.ManyToManyField( + blank=True, + related_name="contact_applications_information", + to="registrar.contact", + verbose_name="Other employees", + ), + ), + ] From 1e90c3121ba0e493938345c6667a93688c4b9383 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 7 Mar 2024 14:17:41 -0500 Subject: [PATCH 31/51] changed font size of h2 --- src/registrar/assets/sass/_theme/_admin.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index b57c6a015..e53dc8b4b 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -141,6 +141,10 @@ h1, h2, h3, font-weight: font-weight('bold'); } +#content h2 { + font-size: 1.3rem; +} + .module h3 { padding: 0; color: var(--link-fg); From ed7d1d8c4fed4df2c89c7582e498cfba0742394c Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 7 Mar 2024 13:21:52 -0800 Subject: [PATCH 32/51] Fix script name --- .../management/commands/generate_current_metadata_report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/generate_current_metadata_report.py index da5aafb91..cdda32002 100644 --- a/src/registrar/management/commands/generate_current_metadata_report.py +++ b/src/registrar/management/commands/generate_current_metadata_report.py @@ -40,14 +40,14 @@ class Command(BaseCommand): logger.info("Generating report...") try: - self.generate_current_metadata_report(directory, file_name, check_path) + self.email_current_metadata_report(directory, file_name, check_path) except Exception as err: # TODO - #1317: Notify operations when auto report generation fails raise err else: logger.info(f"Success! Created {file_name} and successfully sent out an email!") - def generate_current_metadata_report(self, directory, file_name, check_path): + def email_current_metadata_report(self, directory, file_name, check_path): """Creates a current-metadata.csv file under the specified directory, then uploads it to a AWS S3 bucket. This is done for resiliency reasons in the event our application goes down and/or the email From 7e6cf54781aa89568d4374830b9f9055e187d4f2 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 7 Mar 2024 13:34:40 -0800 Subject: [PATCH 33/51] Fix renaming for yaml upload --- .github/workflows/daily-csv-upload.yaml | 2 +- ..._metadata_report.py => email_current_metadata_report.py} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/registrar/management/commands/{generate_current_metadata_report.py => email_current_metadata_report.py} (95%) diff --git a/.github/workflows/daily-csv-upload.yaml b/.github/workflows/daily-csv-upload.yaml index 2a57c2083..9cacfc3bf 100644 --- a/.github/workflows/daily-csv-upload.yaml +++ b/.github/workflows/daily-csv-upload.yaml @@ -31,7 +31,7 @@ jobs: cf_space: ${{ secrets.CF_REPORT_ENV }} cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py generate_current_full_report' --name full" - - name: Generate and email domain-metadata-.csv + - name: Generate and email domain-metadata.csv uses: cloud-gov/cg-cli-tools@main with: cf_username: ${{ secrets[env.CF_USERNAME] }} diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/email_current_metadata_report.py similarity index 95% rename from src/registrar/management/commands/generate_current_metadata_report.py rename to src/registrar/management/commands/email_current_metadata_report.py index cdda32002..4300bf227 100644 --- a/src/registrar/management/commands/generate_current_metadata_report.py +++ b/src/registrar/management/commands/email_current_metadata_report.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) class Command(BaseCommand): help = ( - "Generates and uploads a current-metadata.csv file to our S3 bucket " + "Generates and uploads a domain-metadata.csv file to our S3 bucket " "which is based off of all existing Domains." ) @@ -32,8 +32,8 @@ class Command(BaseCommand): ) def handle(self, **options): - """Grabs the directory then creates current-metadata.csv in that directory""" - file_name = "current-metadata.csv" + """Grabs the directory then creates domain-metadata.csv in that directory""" + file_name = "domain-metadata.csv" # Ensures a slash is added directory = os.path.join(options.get("directory"), "") check_path = options.get("checkpath") From 5f60134d1dbb1f14028537b2d1a5e936a8991efc Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 7 Mar 2024 19:20:08 -0500 Subject: [PATCH 34/51] updated style on h2 --- src/registrar/assets/sass/_theme/_admin.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index e53dc8b4b..fa3efb9a2 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -141,7 +141,7 @@ h1, h2, h3, font-weight: font-weight('bold'); } -#content h2 { +div#content > h2 { font-size: 1.3rem; } From 47147abf72ddf3c9b6ca915d9898eb3c95b330d6 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 8 Mar 2024 10:55:40 -0800 Subject: [PATCH 35/51] Update from created at to submission date --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 92e477667..4b85b3f3a 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -942,7 +942,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): "custom_election_board", "city", "state_territory", - "created_at", + "submission_date", #this is the right change "submitter", "investigator", ] From 139e7f11830f4a6b1fc8ee6be861cde205bb0b82 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 8 Mar 2024 14:45:02 -0500 Subject: [PATCH 36/51] remove .gov domain from domain form --- src/registrar/admin.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index f03fe6713..fb2ad50af 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1,5 +1,6 @@ from datetime import date import logging +import copy from django import forms from django.db.models.functions import Concat, Coalesce @@ -1185,7 +1186,13 @@ class DomainInformationInline(admin.StackedInline): model = models.DomainInformation - fieldsets = DomainInformationAdmin.fieldsets + fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets) + # remove .gov domain from fieldset + for index, (title, _) in enumerate(fieldsets): + if title == ".gov domain": + del fieldsets[index] + break + analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields # For each filter_horizontal, init in admin js extendFilterHorizontalWidgets # to activate the edit/delete/view buttons From feafc74d05aebc0969676cc4a6bc0a37703ec1bb Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 8 Mar 2024 11:49:52 -0800 Subject: [PATCH 37/51] Remove comment --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 4b85b3f3a..5c24aaf79 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -942,7 +942,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): "custom_election_board", "city", "state_territory", - "submission_date", #this is the right change + "submission_date", "submitter", "investigator", ] From 29e3275a87a18eac20449565672fbbae0c358f56 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 8 Mar 2024 14:52:01 -0500 Subject: [PATCH 38/51] satisfied linter --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index fb2ad50af..eb88473f8 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1188,7 +1188,7 @@ class DomainInformationInline(admin.StackedInline): fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets) # remove .gov domain from fieldset - for index, (title, _) in enumerate(fieldsets): + for index, (title, f) in enumerate(fieldsets): if title == ".gov domain": del fieldsets[index] break From 454cac951ab6cf9f5cb51103092c3bb849f54408 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:41:40 -0700 Subject: [PATCH 39/51] Bug fix for PR --- src/registrar/assets/js/get-gov-admin.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 4ed1a0d28..8170e4bd0 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -61,18 +61,19 @@ function openInNewTab(el, removeAttribute = false){ * This intentionally does not interact with createPhantomModalFormButtons() */ (function (){ - function displayModalOnDropdownClick(linkClickedDisplaysModal, statusDropdown, cancelButton, valueToCheck){ + function displayModalOnDropdownClick(linkClickedDisplaysModal, statusDropdown, actionButton, valueToCheck){ // If these exist all at the same time, we're on the right page if (linkClickedDisplaysModal && statusDropdown && statusDropdown.value){ + + // Set the previous value in the event the user cancels. + let previousValue = statusDropdown.value; + if (actionButton){ - if (cancelButton){ - // Store the previous value in the event the user cancels. - // We only need to do this if cancel button is specified. - let previousValue = statusDropdown.value; - cancelButton.addEventListener('click', function() { + // Otherwise, if the confirmation buttion is pressed, set it to that + actionButton.addEventListener('click', function() { // Revert the dropdown to its previous value - statusDropdown.value = previousValue; + statusDropdown.value = valueToCheck; }); }else { console.log("displayModalOnDropdownClick() -> Cancel button was null") @@ -82,6 +83,10 @@ function openInNewTab(el, removeAttribute = false){ statusDropdown.addEventListener('change', function() { // Check if "Ineligible" is selected if (this.value && this.value.toLowerCase() === valueToCheck) { + // Set the old value in the event the user cancels, + // or otherwise exists the dropdown. + statusDropdown.value = previousValue + // Display the modal. linkClickedDisplaysModal.click() } @@ -98,9 +103,9 @@ function openInNewTab(el, removeAttribute = false){ // Because the modal button does not have the class "dja-form-placeholder", // it will not be affected by the createPhantomModalFormButtons() function. - let cancelButton = document.querySelector('button[name="_cancel_application_ineligible"]'); + let actionButton = document.querySelector('button[name="_set_application_ineligible"]'); let valueToCheck = "ineligible" - displayModalOnDropdownClick(modalButton, statusDropdown, cancelButton, valueToCheck); + displayModalOnDropdownClick(modalButton, statusDropdown, actionButton, valueToCheck); } hookModalToIneligibleStatus() From d75cbf50e3268d4a69ec57ddc95b1c8b19bafd54 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Sat, 9 Mar 2024 06:41:10 -0500 Subject: [PATCH 40/51] merge changes --- src/registrar/admin.py | 2 +- src/registrar/assets/sass/_theme/_admin.scss | 4 ++ ...maininformation_other_contacts_and_more.py | 40 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/registrar/migrations/0075_alter_domaininformation_other_contacts_and_more.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index e877d8ef8..6e4e7f50f 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -777,7 +777,7 @@ class DomainInformationAdmin(ListHeaderAdmin): search_help_text = "Search by domain." fieldsets = [ - (None, {"fields": ["creator", "submitter", "domain_application", "notes"]}), + (None, {"fields": ["creator", "submitter", "domain_request", "notes"]}), (".gov domain", {"fields": ["domain"]}), ("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}), ("Background info", {"fields": ["anything_else"]}), diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 9c0d7517c..aba2ad9a3 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -143,6 +143,10 @@ h1, h2, h3, font-weight: font-weight('bold'); } +div#content > h2 { + font-size: 1.3rem; +} + .module h3 { padding: 0; color: var(--link-fg); diff --git a/src/registrar/migrations/0075_alter_domaininformation_other_contacts_and_more.py b/src/registrar/migrations/0075_alter_domaininformation_other_contacts_and_more.py new file mode 100644 index 000000000..53ad96ca4 --- /dev/null +++ b/src/registrar/migrations/0075_alter_domaininformation_other_contacts_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.10 on 2024-03-09 11:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0074_create_groups_v08"), + ] + + operations = [ + migrations.AlterField( + model_name="domaininformation", + name="other_contacts", + field=models.ManyToManyField( + blank=True, + related_name="contact_domain_requests_information", + to="registrar.contact", + verbose_name="Other employees", + ), + ), + migrations.AlterField( + model_name="domainrequest", + name="current_websites", + field=models.ManyToManyField( + blank=True, related_name="current+", to="registrar.website", verbose_name="Current websites" + ), + ), + migrations.AlterField( + model_name="domainrequest", + name="other_contacts", + field=models.ManyToManyField( + blank=True, + related_name="contact_domain_requests", + to="registrar.contact", + verbose_name="Other employees", + ), + ), + ] From b27318e9755990f48ccef75273200467e48753e6 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Sat, 9 Mar 2024 06:45:30 -0500 Subject: [PATCH 41/51] merge migrations --- ...inapplication_current_websites_and_more.py | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 src/registrar/migrations/0073_alter_domainapplication_current_websites_and_more.py diff --git a/src/registrar/migrations/0073_alter_domainapplication_current_websites_and_more.py b/src/registrar/migrations/0073_alter_domainapplication_current_websites_and_more.py deleted file mode 100644 index cefaba27c..000000000 --- a/src/registrar/migrations/0073_alter_domainapplication_current_websites_and_more.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 4.2.10 on 2024-03-07 17:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("registrar", "0072_alter_publiccontact_fax_alter_publiccontact_voice"), - ] - - operations = [ - migrations.AlterField( - model_name="domainapplication", - name="current_websites", - field=models.ManyToManyField( - blank=True, related_name="current+", to="registrar.website", verbose_name="Current websites" - ), - ), - migrations.AlterField( - model_name="domainapplication", - name="other_contacts", - field=models.ManyToManyField( - blank=True, related_name="contact_applications", to="registrar.contact", verbose_name="Other employees" - ), - ), - migrations.AlterField( - model_name="domaininformation", - name="other_contacts", - field=models.ManyToManyField( - blank=True, - related_name="contact_applications_information", - to="registrar.contact", - verbose_name="Other employees", - ), - ), - ] From f31d6ee0735e1014d3dd7c7e553c213ff48fac7d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:44:56 -0600 Subject: [PATCH 42/51] Update test_admin.py --- src/registrar/tests/test_admin.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index fb7e7af5e..8d983dcb7 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1224,7 +1224,7 @@ class TestDomainRequestAdmin(MockEppLib): EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - # Create a sample application + # Create a sample domain request domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) p = "userpass" @@ -1261,7 +1261,7 @@ class TestDomainRequestAdmin(MockEppLib): # Test that approved domain exists and equals requested domain self.assertEqual(domain_request.creator.status, "restricted") - # 'Get' to the application again + # 'Get' to the domain request again response = self.client.get( "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), follow=True, @@ -1282,7 +1282,7 @@ class TestDomainRequestAdmin(MockEppLib): EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - # Create a sample application + # Create a sample domain request domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) p = "userpass" @@ -1310,7 +1310,7 @@ class TestDomainRequestAdmin(MockEppLib): "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), follow=True ) with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # Modify the application's property + # Modify the domain request's property domain_request.status = DomainRequest.DomainRequestStatus.INELIGIBLE # Use the model admin's save_model method @@ -1319,7 +1319,7 @@ class TestDomainRequestAdmin(MockEppLib): # Test that approved domain exists and equals requested domain self.assertEqual(domain_request.creator.status, "restricted") - # 'Get' to the application again + # 'Get' to the domain request again response = self.client.get( "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), follow=True, From 240c5c8e538984ae60d89d3e6c109035495f1686 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 12 Mar 2024 16:21:17 -0700 Subject: [PATCH 43/51] added bob and meoward --- .github/workflows/migrate.yaml | 2 + .github/workflows/reset-db.yaml | 2 + ops/manifests/manifest-bob.yaml | 32 +++++++++++++ ops/manifests/manifest-meoward.yaml | 32 +++++++++++++ ops/scripts/create_dev_sandbox.sh | 72 ++++++++++++++--------------- src/registrar/config/settings.py | 2 + 6 files changed, 106 insertions(+), 36 deletions(-) create mode 100644 ops/manifests/manifest-bob.yaml create mode 100644 ops/manifests/manifest-meoward.yaml diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index 2033ee51c..825ab04d7 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -16,6 +16,8 @@ on: - stable - staging - development + - bob + - meoward - backup - ky - es diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index f8730c865..05eb963c3 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -16,6 +16,8 @@ on: options: - staging - development + - bob + - meoward - backup - ky - es diff --git a/ops/manifests/manifest-bob.yaml b/ops/manifests/manifest-bob.yaml new file mode 100644 index 000000000..f39d9e145 --- /dev/null +++ b/ops/manifests/manifest-bob.yaml @@ -0,0 +1,32 @@ +--- +applications: +- name: getgov-bob + buildpacks: + - python_buildpack + path: ../../src + instances: 1 + memory: 512M + stack: cflinuxfs4 + timeout: 180 + command: ./run.sh + health-check-type: http + health-check-http-endpoint: /health + health-check-invocation-timeout: 40 + env: + # Send stdout and stderr straight to the terminal without buffering + PYTHONUNBUFFERED: yup + # Tell Django where to find its configuration + DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: https://getgov-bob.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO + # default public site location + GETGOV_PUBLIC_SITE_URL: https://get.gov + # Flag to disable/enable features in prod environments + IS_PRODUCTION: False + routes: + - route: getgov-bob.app.cloud.gov + services: + - getgov-credentials + - getgov-bob-database diff --git a/ops/manifests/manifest-meoward.yaml b/ops/manifests/manifest-meoward.yaml new file mode 100644 index 000000000..c47d9529d --- /dev/null +++ b/ops/manifests/manifest-meoward.yaml @@ -0,0 +1,32 @@ +--- +applications: +- name: getgov-meoward + buildpacks: + - python_buildpack + path: ../../src + instances: 1 + memory: 512M + stack: cflinuxfs4 + timeout: 180 + command: ./run.sh + health-check-type: http + health-check-http-endpoint: /health + health-check-invocation-timeout: 40 + env: + # Send stdout and stderr straight to the terminal without buffering + PYTHONUNBUFFERED: yup + # Tell Django where to find its configuration + DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: https://getgov-meoward.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO + # default public site location + GETGOV_PUBLIC_SITE_URL: https://get.gov + # Flag to disable/enable features in prod environments + IS_PRODUCTION: False + routes: + - route: getgov-meoward.app.cloud.gov + services: + - getgov-credentials + - getgov-meoward-database diff --git a/ops/scripts/create_dev_sandbox.sh b/ops/scripts/create_dev_sandbox.sh index 676fcf7ae..975c7d997 100755 --- a/ops/scripts/create_dev_sandbox.sh +++ b/ops/scripts/create_dev_sandbox.sh @@ -7,51 +7,51 @@ if [ -z "$1" ]; then exit 1 fi -if [ ! $(command -v gh) ] || [ ! $(command -v jq) ] || [ ! $(command -v cf) ]; then - echo "jq, cf, and gh packages must be installed. Please install via your preferred manager." - exit 1 -fi +# if [ ! $(command -v gh) ] || [ ! $(command -v jq) ] || [ ! $(command -v cf) ]; then +# echo "jq, cf, and gh packages must be installed. Please install via your preferred manager." +# exit 1 +# fi upcase_name=$(printf "%s" "$1" | tr '[:lower:]' '[:upper:]') -read -p "Are you on a new branch? We will have to commit this work. (y/n) " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]] -then - git checkout -b new-dev-sandbox-$1 -fi +# read -p "Are you on a new branch? We will have to commit this work. (y/n) " -n 1 -r +# echo +# if [[ ! $REPLY =~ ^[Yy]$ ]] +# then +# git checkout -b new-dev-sandbox-$1 +# fi -cf target -o cisa-dotgov +# cf target -o cisa-dotgov -read -p "Are you logged in to the cisa-dotgov CF org above? (y/n) " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]] -then - cf login -a https://api.fr.cloud.gov --sso -fi +# read -p "Are you logged in to the cisa-dotgov CF org above? (y/n) " -n 1 -r +# echo +# if [[ ! $REPLY =~ ^[Yy]$ ]] +# then +# cf login -a https://api.fr.cloud.gov --sso +# fi -gh auth status -read -p "Are you logged into a Github account with access to cisagov/getgov? (y/n) " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]] -then - gh auth login -fi +# gh auth status +# read -p "Are you logged into a Github account with access to cisagov/getgov? (y/n) " -n 1 -r +# echo +# if [[ ! $REPLY =~ ^[Yy]$ ]] +# then +# gh auth login +# fi -echo "Creating manifest for $1..." -cp ops/scripts/manifest-sandbox-template.yaml ops/manifests/manifest-$1.yaml -sed -i '' "s/ENVIRONMENT/$1/" "ops/manifests/manifest-$1.yaml" +# echo "Creating manifest for $1..." +# cp ops/scripts/manifest-sandbox-template.yaml ops/manifests/manifest-$1.yaml +# sed -i '' "s/ENVIRONMENT/$1/" "ops/manifests/manifest-$1.yaml" -echo "Adding new environment to settings.py..." -sed -i '' '/getgov-development.app.cloud.gov/ {a\ - '\"getgov-$1.app.cloud.gov\"', -}' src/registrar/config/settings.py +# echo "Adding new environment to settings.py..." +# sed -i '' '/getgov-development.app.cloud.gov/ {a\ +# '\"getgov-$1.app.cloud.gov\"', +# }' src/registrar/config/settings.py -echo "Creating new cloud.gov space for $1..." -cf create-space $1 -cf target -o "cisa-dotgov" -s $1 -cf bind-security-group public_networks_egress cisa-dotgov --space $1 -cf bind-security-group trusted_local_networks_egress cisa-dotgov --space $1 +# echo "Creating new cloud.gov space for $1..." +# cf create-space $1 +# cf target -o "cisa-dotgov" -s $1 +# cf bind-security-group public_networks_egress cisa-dotgov --space $1 +# cf bind-security-group trusted_local_networks_egress cisa-dotgov --space $1 echo "Creating new cloud.gov DB for $1. This usually takes about 5 minutes..." cf create-service aws-rds micro-psql getgov-$1-database diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 15799f91b..1ce01ac1f 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -635,6 +635,8 @@ ALLOWED_HOSTS = [ "getgov-stable.app.cloud.gov", "getgov-staging.app.cloud.gov", "getgov-development.app.cloud.gov", + "getgov-bob.app.cloud.gov", + "getgov-meoward.app.cloud.gov", "getgov-backup.app.cloud.gov", "getgov-ky.app.cloud.gov", "getgov-es.app.cloud.gov", From bc2843bfe7d7032f931550520f985ab93772827c Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 12 Mar 2024 16:24:10 -0700 Subject: [PATCH 44/51] undo code comments --- ops/scripts/create_dev_sandbox.sh | 72 +++++++++++++++---------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/ops/scripts/create_dev_sandbox.sh b/ops/scripts/create_dev_sandbox.sh index 975c7d997..676fcf7ae 100755 --- a/ops/scripts/create_dev_sandbox.sh +++ b/ops/scripts/create_dev_sandbox.sh @@ -7,51 +7,51 @@ if [ -z "$1" ]; then exit 1 fi -# if [ ! $(command -v gh) ] || [ ! $(command -v jq) ] || [ ! $(command -v cf) ]; then -# echo "jq, cf, and gh packages must be installed. Please install via your preferred manager." -# exit 1 -# fi +if [ ! $(command -v gh) ] || [ ! $(command -v jq) ] || [ ! $(command -v cf) ]; then + echo "jq, cf, and gh packages must be installed. Please install via your preferred manager." + exit 1 +fi upcase_name=$(printf "%s" "$1" | tr '[:lower:]' '[:upper:]') -# read -p "Are you on a new branch? We will have to commit this work. (y/n) " -n 1 -r -# echo -# if [[ ! $REPLY =~ ^[Yy]$ ]] -# then -# git checkout -b new-dev-sandbox-$1 -# fi +read -p "Are you on a new branch? We will have to commit this work. (y/n) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]] +then + git checkout -b new-dev-sandbox-$1 +fi -# cf target -o cisa-dotgov +cf target -o cisa-dotgov -# read -p "Are you logged in to the cisa-dotgov CF org above? (y/n) " -n 1 -r -# echo -# if [[ ! $REPLY =~ ^[Yy]$ ]] -# then -# cf login -a https://api.fr.cloud.gov --sso -# fi +read -p "Are you logged in to the cisa-dotgov CF org above? (y/n) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]] +then + cf login -a https://api.fr.cloud.gov --sso +fi -# gh auth status -# read -p "Are you logged into a Github account with access to cisagov/getgov? (y/n) " -n 1 -r -# echo -# if [[ ! $REPLY =~ ^[Yy]$ ]] -# then -# gh auth login -# fi +gh auth status +read -p "Are you logged into a Github account with access to cisagov/getgov? (y/n) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]] +then + gh auth login +fi -# echo "Creating manifest for $1..." -# cp ops/scripts/manifest-sandbox-template.yaml ops/manifests/manifest-$1.yaml -# sed -i '' "s/ENVIRONMENT/$1/" "ops/manifests/manifest-$1.yaml" +echo "Creating manifest for $1..." +cp ops/scripts/manifest-sandbox-template.yaml ops/manifests/manifest-$1.yaml +sed -i '' "s/ENVIRONMENT/$1/" "ops/manifests/manifest-$1.yaml" -# echo "Adding new environment to settings.py..." -# sed -i '' '/getgov-development.app.cloud.gov/ {a\ -# '\"getgov-$1.app.cloud.gov\"', -# }' src/registrar/config/settings.py +echo "Adding new environment to settings.py..." +sed -i '' '/getgov-development.app.cloud.gov/ {a\ + '\"getgov-$1.app.cloud.gov\"', +}' src/registrar/config/settings.py -# echo "Creating new cloud.gov space for $1..." -# cf create-space $1 -# cf target -o "cisa-dotgov" -s $1 -# cf bind-security-group public_networks_egress cisa-dotgov --space $1 -# cf bind-security-group trusted_local_networks_egress cisa-dotgov --space $1 +echo "Creating new cloud.gov space for $1..." +cf create-space $1 +cf target -o "cisa-dotgov" -s $1 +cf bind-security-group public_networks_egress cisa-dotgov --space $1 +cf bind-security-group trusted_local_networks_egress cisa-dotgov --space $1 echo "Creating new cloud.gov DB for $1. This usually takes about 5 minutes..." cf create-service aws-rds micro-psql getgov-$1-database From f0b510f09d68c6d714b4cb26304660211abf4bc6 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 13 Mar 2024 08:48:25 -0700 Subject: [PATCH 45/51] Remove extraneous TODOs except for 1 --- .../management/commands/email_current_metadata_report.py | 4 +--- src/registrar/utility/email.py | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/registrar/management/commands/email_current_metadata_report.py b/src/registrar/management/commands/email_current_metadata_report.py index 4300bf227..7e3016842 100644 --- a/src/registrar/management/commands/email_current_metadata_report.py +++ b/src/registrar/management/commands/email_current_metadata_report.py @@ -75,8 +75,6 @@ class Command(BaseCommand): # Set context for the subject current_date_str = datetime.now().strftime("%Y-%m-%d") - # TODO: Update secret in getgov-credentials via cloud.gov and my own .env when merging - # Encrypt the metadata encrypted_metadata_in_bytes = self._encrypt_metadata( s3_client.get_file(file_name), encrypted_metadata_output, str.encode(settings.SECRET_ENCRYPT_METADATA) @@ -86,7 +84,7 @@ class Command(BaseCommand): send_templated_email( template_name="emails/metadata_body.txt", subject_template_name="emails/metadata_subject.txt", - # to_address=settings.DEFAULT_FROM_EMAIL, # TODO: Uncomment this when ready to merge + # to_address=settings.DEFAULT_FROM_EMAIL, to_address="rebecca.hsieh@truss.works ", context={"current_date_str": current_date_str}, attachment_file=encrypted_metadata_in_bytes, diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 91d55f361..5f61181c7 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -75,12 +75,9 @@ def send_templated_email( aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, config=settings.BOTO_CONFIG, ) - response = send_email_with_attachment( + send_email_with_attachment( settings.DEFAULT_FROM_EMAIL, to_address, subject, email_body, attachment_file, ses_client ) - # TODO: Remove this print statement when ready to merge, - # leaving rn for getting error codes in case - print("Response from send_email_with_attachment_is:", response) except Exception as exc: raise EmailSendingError("Could not send SES email.") from exc From 6dacc18956b114cb08a5d26af2f4906a0f62cd29 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 13 Mar 2024 08:55:54 -0700 Subject: [PATCH 46/51] Update final to email --- .../management/commands/email_current_metadata_report.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/management/commands/email_current_metadata_report.py b/src/registrar/management/commands/email_current_metadata_report.py index 7e3016842..dcaf47b06 100644 --- a/src/registrar/management/commands/email_current_metadata_report.py +++ b/src/registrar/management/commands/email_current_metadata_report.py @@ -84,8 +84,7 @@ class Command(BaseCommand): send_templated_email( template_name="emails/metadata_body.txt", subject_template_name="emails/metadata_subject.txt", - # to_address=settings.DEFAULT_FROM_EMAIL, - to_address="rebecca.hsieh@truss.works ", + to_address=settings.DEFAULT_FROM_EMAIL, context={"current_date_str": current_date_str}, attachment_file=encrypted_metadata_in_bytes, ) From 763a1c51fc52367e11d371a16793c535c9c45198 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:53:30 -0600 Subject: [PATCH 47/51] Merge conflict fixes --- src/registrar/assets/js/get-gov-admin.js | 4 +- .../admin/domain_application_change_form.html | 8 +-- src/registrar/tests/test_admin.py | 58 ------------------- 3 files changed, 6 insertions(+), 64 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 8170e4bd0..4ed00c33f 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -57,7 +57,7 @@ function openInNewTab(el, removeAttribute = false){ createPhantomModalFormButtons(); })(); -/** An IIFE for DomainApplication to hook a modal to a dropdown option. +/** An IIFE for DomainRequest to hook a modal to a dropdown option. * This intentionally does not interact with createPhantomModalFormButtons() */ (function (){ @@ -103,7 +103,7 @@ function openInNewTab(el, removeAttribute = false){ // Because the modal button does not have the class "dja-form-placeholder", // it will not be affected by the createPhantomModalFormButtons() function. - let actionButton = document.querySelector('button[name="_set_application_ineligible"]'); + let actionButton = document.querySelector('button[name="_set_domain_request_ineligible"]'); let valueToCheck = "ineligible" displayModalOnDropdownClick(modalButton, statusDropdown, actionButton, valueToCheck); } diff --git a/src/registrar/templates/django/admin/domain_application_change_form.html b/src/registrar/templates/django/admin/domain_application_change_form.html index f0e4cfe4f..95392da1e 100644 --- a/src/registrar/templates/django/admin/domain_application_change_form.html +++ b/src/registrar/templates/django/admin/domain_application_change_form.html @@ -27,7 +27,7 @@ class="usa-modal" id="toggle-set-ineligible" aria-labelledby="Are you sure you want to select ineligible status?" - aria-describedby="This application will be marked as ineligible." + aria-describedby="This request will be marked as ineligible." >
            @@ -51,7 +51,7 @@ Domain: {{ original.requested_domain.name }} {# Acts as a
            #}
            - New status: {{ original.ApplicationStatus.INELIGIBLE|capfirst }} + New status: {{ original.DomainRequestStatus.INELIGIBLE|capfirst }}

            @@ -61,7 +61,7 @@