From adc2013c7ca1905084dad4fc85d2f933179abcae Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 17 Jun 2024 14:07:12 -0600 Subject: [PATCH 01/45] Simplify report and handle many items --- .../commands/email_current_metadata_report.py | 109 ++++++++++-------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/src/registrar/management/commands/email_current_metadata_report.py b/src/registrar/management/commands/email_current_metadata_report.py index dcaf47b06..1e8faac82 100644 --- a/src/registrar/management/commands/email_current_metadata_report.py +++ b/src/registrar/management/commands/email_current_metadata_report.py @@ -9,7 +9,7 @@ from datetime import datetime 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 io import StringIO from ...utility.email import send_templated_email @@ -17,89 +17,104 @@ logger = logging.getLogger(__name__) class Command(BaseCommand): + """Emails a encrypted zip file containing a csv of our domains and domain requests""" help = ( "Generates and uploads a domain-metadata.csv file to our S3 bucket " "which is based off of all existing Domains." ) + current_date = datetime.now().strftime("%m%d%Y") + email_to: str 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", + "--emailTo", + default=settings.DEFAULT_FROM_EMAIL, + help="Defines where we should email this report", ) def handle(self, **options): """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") + self.email_to = options.get("emailTo") + + # Don't email to DEFAULT_FROM_EMAIL when not prod. + if not settings.IS_PRODUCTION and self.email_to == settings.DEFAULT_FROM_EMAIL: + raise ValueError( + "The --emailTo arg must be specified in non-prod environments, " + "and the arg must not equal the DEFAULT_FROM_EMAIL value (aka: help@get.gov)." + ) logger.info("Generating report...") + zip_filename = f"domain-metadata-{self.current_date}.zip" try: - self.email_current_metadata_report(directory, file_name, check_path) + self.email_current_metadata_report(zip_filename) 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!") + logger.info(f"Success! Created {zip_filename} and successfully sent out an email!") - def email_current_metadata_report(self, directory, file_name, check_path): + def email_current_metadata_report(self, zip_filename): """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) + reports = { + "Domain report": { + "report_filename": f"domain-metadata-{self.current_date}.csv", + "report_function": csv_export.export_data_type_to_csv, + }, + "Domain request report": { + "report_filename": f"domain-request-metadata-{self.current_date}.csv", + "report_function": csv_export.DomainRequestExport.export_full_domain_request_report, + }, + } + # Set the password equal to our content in SECRET_ENCRYPT_METADATA. + # For local development, this will be "devpwd" unless otherwise set. + override = settings.SECRET_ENCRYPT_METADATA is None and not settings.IS_PRODUCTION + password = "devpwd" if override else settings.SECRET_ENCRYPT_METADATA - # 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}'") - - s3_client.upload_file(file_path, file_name) - - # 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 - - # Set context for the subject - current_date_str = datetime.now().strftime("%Y-%m-%d") - - # 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) - ) + encrypted_zip_in_bytes = self.get_encrypted_zip(zip_filename, reports, password) # Send the metadata file that is zipped send_templated_email( template_name="emails/metadata_body.txt", subject_template_name="emails/metadata_subject.txt", - to_address=settings.DEFAULT_FROM_EMAIL, - context={"current_date_str": current_date_str}, - attachment_file=encrypted_metadata_in_bytes, + to_address=self.email_to, + context={"current_date_str": datetime.now().strftime("%Y-%m-%d")}, + attachment_file=encrypted_zip_in_bytes, ) - def _encrypt_metadata(self, input_file, output_file, password): + + def get_encrypted_zip(self, zip_filename, reports, 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 # 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 + zip_filename, "w", compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES ) as f_out: - f_out.setpassword(password) - f_out.writestr(current_filename, input_file) - with open(output_file, "rb") as file_data: + f_out.setpassword(str.encode(password)) + for report_name, report_value in reports.items(): + report_filename = report_value["report_filename"] + report_function = report_value["report_function"] + + report = self.write_and_return_report(report_function) + f_out.writestr(report_filename, report) + logger.info(f"Generated {report_name}") + + # Get the final report for emailing purposes + with open(zip_filename, "rb") as file_data: attachment_in_bytes = file_data.read() + return attachment_in_bytes + + def write_and_return_report(self, report_function): + """Writes a report to a StringIO object given a report_function and returns the string.""" + report_bytes = StringIO() + report_function(report_bytes) + + # Rewind the buffer to the beginning after writing + report_bytes.seek(0) + return report_bytes.read() From de6ff1d7d1f1115d5bf1a93212e3818aa8da2dae Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 17 Jun 2024 14:18:33 -0600 Subject: [PATCH 02/45] Streamline --- .../commands/email_current_metadata_report.py | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/registrar/management/commands/email_current_metadata_report.py b/src/registrar/management/commands/email_current_metadata_report.py index 1e8faac82..3f38bec31 100644 --- a/src/registrar/management/commands/email_current_metadata_report.py +++ b/src/registrar/management/commands/email_current_metadata_report.py @@ -23,7 +23,6 @@ class Command(BaseCommand): "which is based off of all existing Domains." ) current_date = datetime.now().strftime("%m%d%Y") - email_to: str def add_arguments(self, parser): """Add our two filename arguments.""" @@ -35,31 +34,27 @@ class Command(BaseCommand): def handle(self, **options): """Grabs the directory then creates domain-metadata.csv in that directory""" - self.email_to = options.get("emailTo") + zip_filename = f"domain-metadata-{self.current_date}.zip" + email_to = options.get("emailTo") # Don't email to DEFAULT_FROM_EMAIL when not prod. - if not settings.IS_PRODUCTION and self.email_to == settings.DEFAULT_FROM_EMAIL: + if not settings.IS_PRODUCTION and email_to == settings.DEFAULT_FROM_EMAIL: raise ValueError( "The --emailTo arg must be specified in non-prod environments, " "and the arg must not equal the DEFAULT_FROM_EMAIL value (aka: help@get.gov)." ) logger.info("Generating report...") - zip_filename = f"domain-metadata-{self.current_date}.zip" try: - self.email_current_metadata_report(zip_filename) + self.email_current_metadata_report(zip_filename, email_to) except Exception as err: # TODO - #1317: Notify operations when auto report generation fails raise err else: logger.info(f"Success! Created {zip_filename} and successfully sent out an email!") - def email_current_metadata_report(self, zip_filename): - """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""" + def email_current_metadata_report(self, zip_filename, email_to): + """Emails a password protected zip containing domain-metadata and domain-request-metadata""" reports = { "Domain report": { "report_filename": f"domain-metadata-{self.current_date}.csv", @@ -81,7 +76,7 @@ class Command(BaseCommand): send_templated_email( template_name="emails/metadata_body.txt", subject_template_name="emails/metadata_subject.txt", - to_address=self.email_to, + to_address=email_to, context={"current_date_str": datetime.now().strftime("%Y-%m-%d")}, attachment_file=encrypted_zip_in_bytes, ) @@ -96,13 +91,10 @@ class Command(BaseCommand): zip_filename, "w", compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES ) as f_out: f_out.setpassword(str.encode(password)) - for report_name, report_value in reports.items(): - report_filename = report_value["report_filename"] - report_function = report_value["report_function"] - - report = self.write_and_return_report(report_function) - f_out.writestr(report_filename, report) - logger.info(f"Generated {report_name}") + for report_name, report in reports.items(): + logger.info(f"Generating {report_name}") + report = self.write_and_return_report(report["report_function"]) + f_out.writestr(report["report_filename"], report) # Get the final report for emailing purposes with open(zip_filename, "rb") as file_data: From a64ecbd0d20f82d5136bd30f905ef9caedcdecdf Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 17 Jun 2024 14:22:10 -0600 Subject: [PATCH 03/45] Lint --- .../management/commands/email_current_metadata_report.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/management/commands/email_current_metadata_report.py b/src/registrar/management/commands/email_current_metadata_report.py index 3f38bec31..b76e77608 100644 --- a/src/registrar/management/commands/email_current_metadata_report.py +++ b/src/registrar/management/commands/email_current_metadata_report.py @@ -18,6 +18,7 @@ logger = logging.getLogger(__name__) class Command(BaseCommand): """Emails a encrypted zip file containing a csv of our domains and domain requests""" + help = ( "Generates and uploads a domain-metadata.csv file to our S3 bucket " "which is based off of all existing Domains." @@ -65,6 +66,7 @@ class Command(BaseCommand): "report_function": csv_export.DomainRequestExport.export_full_domain_request_report, }, } + # Set the password equal to our content in SECRET_ENCRYPT_METADATA. # For local development, this will be "devpwd" unless otherwise set. override = settings.SECRET_ENCRYPT_METADATA is None and not settings.IS_PRODUCTION @@ -81,7 +83,6 @@ class Command(BaseCommand): attachment_file=encrypted_zip_in_bytes, ) - def get_encrypted_zip(self, zip_filename, reports, password): """Helper function for encrypting the attachment file""" From dde2b6830ce57c71a4c455cad1a9f5b4b5a3d29e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 18 Jun 2024 19:14:32 -0400 Subject: [PATCH 04/45] infra --- src/registrar/config/settings.py | 1 + src/registrar/config/urls.py | 6 + src/registrar/registrar_middleware.py | 35 +++ src/registrar/templates/home.html | 215 +++--------------- .../templates/home_organizations.html | 55 +++++ .../includes/domain_requests_table.html | 69 ++++++ .../templates/includes/domains_table.html | 81 +++++++ .../templates/organization_sidebar.html | 18 ++ src/registrar/views/index.py | 3 + src/registrar/views/index_organizations.py | 19 ++ 10 files changed, 324 insertions(+), 178 deletions(-) create mode 100644 src/registrar/templates/home_organizations.html create mode 100644 src/registrar/templates/includes/domain_requests_table.html create mode 100644 src/registrar/templates/includes/domains_table.html create mode 100644 src/registrar/templates/organization_sidebar.html create mode 100644 src/registrar/views/index_organizations.py diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 8438812c4..aa7d73c2f 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -189,6 +189,7 @@ MIDDLEWARE = [ # Used for waffle feature flags "waffle.middleware.WaffleMiddleware", "registrar.registrar_middleware.CheckUserProfileMiddleware", + "registrar.registrar_middleware.CheckOrganizationMiddleware", ] # application object used by Django’s built-in servers (e.g. `runserver`) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index dc6c8acb5..72115f66a 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -25,6 +25,7 @@ from registrar.views.domain_request import Step from registrar.views.domain_requests_json import get_domain_requests_json from registrar.views.domains_json import get_domains_json from registrar.views.utility import always_404 +from registrar.views.index_organizations import index_organizations from api.views import available, get_current_federal, get_current_full @@ -58,6 +59,11 @@ for step, view in [ urlpatterns = [ path("", views.index, name="home"), + path( + "organization", + index_organizations, + name="home-organization", + ), path( "admin/logout/", RedirectView.as_view(pattern_name="logout", permanent=False), diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 79e3b7a11..04ec9fa13 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -2,6 +2,7 @@ Contains middleware used in settings.py """ +import logging from urllib.parse import parse_qs from django.urls import reverse from django.http import HttpResponseRedirect @@ -10,6 +11,7 @@ from waffle.decorators import flag_is_active from registrar.models.utility.generic_helper import replace_url_queryparams +logger = logging.getLogger(__name__) class NoCacheMiddleware: """ @@ -119,3 +121,36 @@ class CheckUserProfileMiddleware: else: # Process the view as normal return None + +class CheckOrganizationMiddleware: + """ + """ + + def __init__(self, get_response): + self.get_response = get_response + self.home_organization = reverse("home-organization") + self.home = reverse("home") + self.json1 = reverse("get_domains_json") + self.json2 = reverse("get_domain_requests_json") + + def __call__(self, request): + response = self.get_response(request) + return response + + def process_view(self, request, view_func, view_args, view_kwargs): + current_path = request.path + logger.debug(f"Current path: {current_path}") + + # Avoid infinite loop by skipping the redirect check on the home-organization URL + if current_path == self.home_organization or current_path == self.json1 or current_path == self.json2: + logger.debug("Skipping middleware check for home-organization URL") + return None + + has_organization_feature_flag = flag_is_active(request, "organization_feature") + logger.debug(f"Flag is active: {has_organization_feature_flag}") + + if has_organization_feature_flag: + logger.debug(f"Redirecting to {self.home_organization}") + return HttpResponseRedirect(self.home_organization) + + return None diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index f93159f01..a5ed4c86c 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -9,189 +9,48 @@ {% if user.is_authenticated %} {# the entire logged in page goes here #} -
- {% block messages %} - {% include "includes/form_messages.html" %} - {% endblock %} -

Manage your domains

+{% block homepage_content %} - {% comment %} - IMPORTANT: - If this button is added on any other page, make sure to update the - relevant view to reset request.session["new_request"] = True - {% endcomment %} -

- - Start a new domain request - -

+
+ {% block messages %} + {% include "includes/form_messages.html" %} + {% endblock %} +

Manage your domains

-
-
-
-

Domains

-
-
-
- -
-
-
- - - -
- + {% comment %} + IMPORTANT: + If this button is added on any other page, make sure to update the + relevant view to reset request.session["new_request"] = True + {% endcomment %} +

+ + Start a new domain request + +

-
-
-
-

Domain requests

-
-
-
- -
-
-
- - - -
- + {% include "includes/domains_table.html" %} + {% include "includes/domain_requests_table.html" %} - {# Note: Reimplement this after MVP #} - + {# Note: Reimplement this after MVP #} + - - + + + +{% endblock %}
{% else %} {# not user.is_authenticated #} diff --git a/src/registrar/templates/home_organizations.html b/src/registrar/templates/home_organizations.html new file mode 100644 index 000000000..5edd3860a --- /dev/null +++ b/src/registrar/templates/home_organizations.html @@ -0,0 +1,55 @@ +{% extends 'home.html' %} + +{% load static %} + +{% block homepage_content %} + +
+
+
+ {% include "organization_sidebar.html" %} +
+
+ {% block messages %} + {% include "includes/form_messages.html" %} + {% endblock %} +

Manage your domains

+ + {% comment %} + IMPORTANT: + If this button is added on any other page, make sure to update the + relevant view to reset request.session["new_request"] = True + {% endcomment %} +

+ + Start a new domain request + +

+ + {% include "includes/domains_table.html" %} + {% include "includes/domain_requests_table.html" %} + + {# Note: Reimplement this after MVP #} + + + + + + +
+
+ +{% endblock %} diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html new file mode 100644 index 000000000..377f49d02 --- /dev/null +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -0,0 +1,69 @@ +{% load static %} + +
+
+
+

Domain requests

+
+
+
+ +
+
+
+ + + +
+ diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html new file mode 100644 index 000000000..334dba3da --- /dev/null +++ b/src/registrar/templates/includes/domains_table.html @@ -0,0 +1,81 @@ +{% load static %} + +
+
+
+

Domains

+
+
+
+ +
+
+
+ + + +
+ diff --git a/src/registrar/templates/organization_sidebar.html b/src/registrar/templates/organization_sidebar.html new file mode 100644 index 000000000..ed664affd --- /dev/null +++ b/src/registrar/templates/organization_sidebar.html @@ -0,0 +1,18 @@ +{% load static url_helpers %} + +
+ +
diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py index c05bde21d..5e546e8e7 100644 --- a/src/registrar/views/index.py +++ b/src/registrar/views/index.py @@ -9,8 +9,11 @@ def index(request): if request.user.is_authenticated: # This is a django waffle flag which toggles features based off of the "flag" table context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") + context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature") # This controls the creation of a new domain request in the wizard request.session["new_request"] = True + print('homepage view') + return render(request, "home.html", context) diff --git a/src/registrar/views/index_organizations.py b/src/registrar/views/index_organizations.py new file mode 100644 index 000000000..4ec15ad59 --- /dev/null +++ b/src/registrar/views/index_organizations.py @@ -0,0 +1,19 @@ +from django.shortcuts import render +from waffle.decorators import flag_is_active + + +def index_organizations(request): + """This page is available to anyone without logging in.""" + context = {} + + if request.user.is_authenticated: + # This is a django waffle flag which toggles features based off of the "flag" table + context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") + context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature") + + # This controls the creation of a new domain request in the wizard + request.session["new_request"] = True + + print('homepage organizations view') + + return render(request, "home_organizations.html", context) From c020007d515cfdd87f9eb788234f8dd14bec5f9a Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 19 Jun 2024 07:30:09 -0400 Subject: [PATCH 05/45] wip --- src/registrar/registrar_middleware.py | 49 +++++++++++++++++---------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 79e3b7a11..7ba304aba 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -52,6 +52,30 @@ class CheckUserProfileMiddleware: "/admin", ] + def __init__(self): + self.setup_page = reverse("finish-user-profile-setup") + self.profile_page = reverse("user-profile") + self.logout_page = reverse("logout") + + self.regular_excluded_pages = [ + self.setup_page, + self.logout_page, + "/admin", + ] + self.other_excluded_pages = [ + self.profile_page, + self.logout_page, + "/admin", + ] + + self.excluded_pages = { + self.setup_page: self.regular_excluded_pages, + self.profile_page: self.other_excluded_pages + } + + def _get_excluded_pages(self, page): + return self.excluded_pages.get(page, []) + def __call__(self, request): response = self.get_response(request) return response @@ -68,16 +92,16 @@ class CheckUserProfileMiddleware: return None if request.user.is_authenticated: + profile_page = self.profile_page + if request.user.verification_type == User.VerificationTypeChoices.REGULAR: + profile_page = self.setup_page if hasattr(request.user, "finished_setup") and not request.user.finished_setup: - if request.user.verification_type == User.VerificationTypeChoices.REGULAR: - return self._handle_regular_user_setup_not_finished(request) - else: - return self._handle_other_user_setup_not_finished(request) + return self._handle_user_setup_not_finished(request, profile_page) # Continue processing the view return None - def _handle_regular_user_setup_not_finished(self, request): + def _handle_user_setup_not_finished(self, request, profile_page): """Redirects the given user to the finish setup page. We set the "redirect" query param equal to where the user wants to go. @@ -93,7 +117,8 @@ class CheckUserProfileMiddleware: custom_redirect = "domain-request:" if request.path == "/request/" else None # Don't redirect on excluded pages (such as the setup page itself) - if not any(request.path.startswith(page) for page in self.regular_excluded_pages): + #if not any(request.path.startswith(page) for page in excluded_pages): + if not any(request.path.startswith(page) for page in self._get_excluded_pages(profile_page)): # Preserve the original query parameters, and coerce them into a dict query_params = parse_qs(request.META["QUERY_STRING"]) @@ -103,19 +128,9 @@ class CheckUserProfileMiddleware: query_params["redirect"] = custom_redirect # Add our new query param, while preserving old ones - new_setup_page = replace_url_queryparams(self.setup_page, query_params) if query_params else self.setup_page + new_setup_page = replace_url_queryparams(profile_page, query_params) if query_params else profile_page return HttpResponseRedirect(new_setup_page) else: # Process the view as normal return None - - def _handle_other_user_setup_not_finished(self, request): - """Redirects the given user to the profile page to finish setup.""" - - # Don't redirect on excluded pages (such as the setup page itself) - if not any(request.path.startswith(page) for page in self.other_excluded_pages): - return HttpResponseRedirect(self.profile_page) - else: - # Process the view as normal - return None From a6c4710777962502b15a5f1744695df17fb056da Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 19 Jun 2024 14:26:36 -0400 Subject: [PATCH 06/45] minimally works, requires cleanup --- src/registrar/forms/user_profile.py | 2 + src/registrar/registrar_middleware.py | 15 -- .../templates/domain_request_intro.html | 2 +- .../includes/finish_profile_form.html | 2 + .../templates/includes/profile_form.html | 3 + src/registrar/templates/profile.html | 8 +- src/registrar/views/user_profile.py | 195 +++++++++++------- 7 files changed, 130 insertions(+), 97 deletions(-) diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py index 3dd8cbdce..682e1a5df 100644 --- a/src/registrar/forms/user_profile.py +++ b/src/registrar/forms/user_profile.py @@ -10,6 +10,8 @@ from registrar.models.utility.domain_helper import DomainHelper class UserProfileForm(forms.ModelForm): """Form for updating user profile.""" + redirect = forms.CharField(widget=forms.HiddenInput(), required=False) + class Meta: model = Contact fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"] diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 7ba304aba..88eb9af2d 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -38,21 +38,6 @@ class CheckUserProfileMiddleware: def __init__(self, get_response): self.get_response = get_response - self.setup_page = reverse("finish-user-profile-setup") - self.profile_page = reverse("user-profile") - self.logout_page = reverse("logout") - self.regular_excluded_pages = [ - self.setup_page, - self.logout_page, - "/admin", - ] - self.other_excluded_pages = [ - self.profile_page, - self.logout_page, - "/admin", - ] - - def __init__(self): self.setup_page = reverse("finish-user-profile-setup") self.profile_page = reverse("user-profile") self.logout_page = reverse("logout") diff --git a/src/registrar/templates/domain_request_intro.html b/src/registrar/templates/domain_request_intro.html index 2bfeeeef1..370ea2b2b 100644 --- a/src/registrar/templates/domain_request_intro.html +++ b/src/registrar/templates/domain_request_intro.html @@ -18,7 +18,7 @@ completing your domain request might take around 15 minutes.

{% if has_profile_feature_flag %}

How we’ll reach you

-

While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review If the contact information below is not correct, visit your profile to make updates.

+

While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review If the contact information below is not correct, visit your profile to make updates.

{% include "includes/profile_information.html" with user=user%} {% endif %} diff --git a/src/registrar/templates/includes/finish_profile_form.html b/src/registrar/templates/includes/finish_profile_form.html index a40534b48..1e48d5578 100644 --- a/src/registrar/templates/includes/finish_profile_form.html +++ b/src/registrar/templates/includes/finish_profile_form.html @@ -33,6 +33,8 @@ Your contact information + + {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable usa-form-editable--no-border padding-top-2" %} {% input_with_errors form.full_name %} {% endwith %} diff --git a/src/registrar/templates/includes/profile_form.html b/src/registrar/templates/includes/profile_form.html index cb3e734bf..966b92b01 100644 --- a/src/registrar/templates/includes/profile_form.html +++ b/src/registrar/templates/includes/profile_form.html @@ -19,6 +19,9 @@
{% csrf_token %} + {# Include the hidden 'redirect' field #} + + {% input_with_errors form.first_name %} {% input_with_errors form.middle_name %} diff --git a/src/registrar/templates/profile.html b/src/registrar/templates/profile.html index c62d0a7c1..41471fe88 100644 --- a/src/registrar/templates/profile.html +++ b/src/registrar/templates/profile.html @@ -25,19 +25,13 @@ Edit your User Profile | {% include "includes/form_errors.html" with form=form %} {% if show_back_button %} - + - {% if not return_to_request %}

{{ profile_back_button_text }}

- {% else %} -

- Go back to your domain request -

- {% endif %}
{% endif %} diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index f148f5652..ddb6136f5 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -9,6 +9,7 @@ from urllib.parse import parse_qs, unquote from urllib.parse import quote from django.contrib import messages +from django.http import QueryDict from django.views.generic.edit import FormMixin from registrar.forms.user_profile import UserProfileForm, FinishSetupProfileForm from django.urls import NoReverseMatch, reverse @@ -31,16 +32,48 @@ class UserProfileView(UserProfilePermissionView, FormMixin): Base View for the User Profile. Handles getting and setting the User Profile """ + class RedirectType(Enum): + """ + Enums for each type of redirection. Enforces behaviour on `get_redirect_url()`. + + - HOME: We want to redirect to reverse("home") + - BACK_TO_SELF: We want to redirect back to this page + - TO_SPECIFIC_PAGE: We want to redirect to the page specified in the queryparam "redirect" + - COMPLETE_SETUP: Indicates that we want to navigate BACK_TO_SELF, but the subsequent + redirect after the next POST should be either HOME or TO_SPECIFIC_PAGE + """ + + HOME = "home" + TO_SPECIFIC_PAGE = "domain_request" + BACK_TO_SELF = "back_to_self" + COMPLETE_SETUP = "complete_setup" + + @classmethod + def get_all_redirect_types(cls) -> list[str]: + """Returns the value of every redirect type defined in this enum.""" + return [r.value for r in cls] + model = Contact template_name = "profile.html" form_class = UserProfileForm + base_view_name = "user-profile" + + all_redirect_types = RedirectType.get_all_redirect_types() + redirect_type: RedirectType def get(self, request, *args, **kwargs): """Handle get requests by getting user's contact object and setting object and form to context before rendering.""" - self._refresh_session_and_object(request) - form = self.form_class(instance=self.object) - context = self.get_context_data(object=self.object, form=form) + #self._refresh_session_and_object(request) + self.object = self.get_object() + + # Get the redirect parameter from the query string + redirect = request.GET.get('redirect', 'home') + + logger.info(f"redirect value is {redirect}") + + form = self.form_class(instance=self.object, initial={'redirect': redirect}) + context = self.get_context_data(object=self.object, form=form, redirect=redirect) if ( hasattr(self.user, "finished_setup") @@ -63,8 +96,8 @@ class UserProfileView(UserProfilePermissionView, FormMixin): @waffle_flag("profile_feature") # type: ignore def dispatch(self, request, *args, **kwargs): # type: ignore # Store the original queryparams to persist them - query_params = request.META["QUERY_STRING"] - request.session["query_params"] = query_params + # query_params = request.META["QUERY_STRING"] + # request.session["query_params"] = query_params return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): @@ -73,10 +106,14 @@ class UserProfileView(UserProfilePermissionView, FormMixin): # This is a django waffle flag which toggles features based off of the "flag" table context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") - # The text for the back button on this page - context["profile_back_button_text"] = "Go to manage your domains" - context["show_back_button"] = False + # Set the profile_back_button_text based on the redirect parameter + if kwargs.get('redirect') == 'domain-request:': + context["profile_back_button_text"] = "Go back to your request" + else: + context["profile_back_button_text"] = "Go to manage your domains" + # Show back button conditional on user having finished setup + context["show_back_button"] = False if hasattr(self.user, "finished_setup") and self.user.finished_setup: context["user_finished_setup"] = True context["show_back_button"] = True @@ -84,21 +121,30 @@ class UserProfileView(UserProfilePermissionView, FormMixin): return context def get_success_url(self): - """Redirect to the user's profile page.""" + """Redirect to the user's profile page with updated query parameters.""" - query_params = {} - if "query_params" in self.session: - params = unquote(self.session["query_params"]) - query_params = parse_qs(params) + # Get the redirect parameter from the form submission + redirect_param = self.request.POST.get('redirect', None) - # Preserve queryparams and add them back to the url - base_url = reverse("user-profile") - new_redirect = replace_url_queryparams(base_url, query_params, convert_list_to_csv=True) - return new_redirect + # Initialize QueryDict with existing query parameters from current request + query_params = QueryDict(mutable=True) + query_params.update(self.request.GET) + + # Update query parameters with the 'redirect' value from form submission + if redirect_param and redirect_param != 'home': + query_params['redirect'] = redirect_param + + # Generate the URL with updated query parameters + base_url = reverse(self.base_view_name) + + # Generate the full url from the given query params + full_url = replace_url_queryparams(base_url, query_params) + return full_url def post(self, request, *args, **kwargs): """Handle post requests (form submissions)""" - self._refresh_session_and_object(request) + #self._refresh_session_and_object(request) + self.object = self.get_object() form = self.form_class(request.POST, instance=self.object) if form.is_valid(): @@ -133,80 +179,75 @@ class FinishProfileSetupView(UserProfileView): """This view forces the user into providing additional details that we may have missed from Login.gov""" - class RedirectType(Enum): - """ - Enums for each type of redirection. Enforces behaviour on `get_redirect_url()`. - - - HOME: We want to redirect to reverse("home") - - BACK_TO_SELF: We want to redirect back to this page - - TO_SPECIFIC_PAGE: We want to redirect to the page specified in the queryparam "redirect" - - COMPLETE_SETUP: Indicates that we want to navigate BACK_TO_SELF, but the subsequent - redirect after the next POST should be either HOME or TO_SPECIFIC_PAGE - """ - - HOME = "home" - TO_SPECIFIC_PAGE = "domain_request" - BACK_TO_SELF = "back_to_self" - COMPLETE_SETUP = "complete_setup" - - @classmethod - def get_all_redirect_types(cls) -> list[str]: - """Returns the value of every redirect type defined in this enum.""" - return [r.value for r in cls] - template_name = "finish_profile_setup.html" form_class = FinishSetupProfileForm model = Contact - all_redirect_types = RedirectType.get_all_redirect_types() - redirect_type: RedirectType + base_view_name = "finish-user-profile-setup" def get_context_data(self, **kwargs): - + """Extend get_context_data to include has_profile_feature_flag""" context = super().get_context_data(**kwargs) - # Hide the back button by default + # Show back button conditional on user having finished setup context["show_back_button"] = False - - if self.redirect_type == self.RedirectType.COMPLETE_SETUP: + if hasattr(self.user, "finished_setup") and self.user.finished_setup: context["confirm_changes"] = True - - if "redirect_viewname" not in self.session: + if kwargs.get('redirect') == 'home': + context["profile_back_button_text"] = "Go to manage your domains" context["show_back_button"] = True else: context["going_to_specific_page"] = True context["redirect_button_text"] = "Continue to your request" - return context + + # def get_context_data(self, **kwargs): - @method_decorator(csrf_protect) - def dispatch(self, request, *args, **kwargs): - """ - Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag. + # context = super().get_context_data(**kwargs) - This method sets the redirect type based on the 'redirect' query parameter, - defaulting to BACK_TO_SELF if not provided. - It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE. + # # Hide the back button by default + # context["show_back_button"] = False - Returns: - HttpResponse: The response generated by the parent class's dispatch method. - """ + # logger.info(f"self.redirect_type = {self.redirect_type}") + # if self.redirect_type == self.RedirectType.COMPLETE_SETUP: + # context["confirm_changes"] = True - # Update redirect type based on the query parameter if present - default_redirect_value = self.RedirectType.BACK_TO_SELF.value - redirect_value = request.GET.get("redirect", default_redirect_value) + # if "redirect_viewname" not in self.session: + # context["show_back_button"] = True + # else: + # context["going_to_specific_page"] = True + # context["redirect_button_text"] = "Continue to your request" - if redirect_value in self.all_redirect_types: - # If the redirect value is a preexisting value in our enum, set it to that. - self.redirect_type = self.RedirectType(redirect_value) - else: - # If the redirect type is undefined, then we assume that we are specifying a particular page to redirect to. - self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE + # return context - # Store the page that we want to redirect to for later use - request.session["redirect_viewname"] = str(redirect_value) + # @method_decorator(csrf_protect) + # def dispatch(self, request, *args, **kwargs): + # """ + # Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag. - return super().dispatch(request, *args, **kwargs) + # This method sets the redirect type based on the 'redirect' query parameter, + # defaulting to BACK_TO_SELF if not provided. + # It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE. + + # Returns: + # HttpResponse: The response generated by the parent class's dispatch method. + # """ + + # # Update redirect type based on the query parameter if present + # default_redirect_value = self.RedirectType.BACK_TO_SELF.value + # redirect_value = request.GET.get("redirect", default_redirect_value) + + # if redirect_value in self.all_redirect_types: + # # If the redirect value is a preexisting value in our enum, set it to that. + # self.redirect_type = self.RedirectType(redirect_value) + # else: + # # If the redirect type is undefined, then we assume that we are specifying a particular page to redirect to. + # self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE + + # # Store the page that we want to redirect to for later use + # request.session["redirect_viewname"] = str(redirect_value) + + # return super().dispatch(request, *args, **kwargs) def post(self, request, *args, **kwargs): """Form submission posts to this view.""" @@ -217,17 +258,23 @@ class FinishProfileSetupView(UserProfileView): if form.is_valid(): if "contact_setup_save_button" in request.POST: # Logic for when the 'Save' button is clicked - self.redirect_type = self.RedirectType.COMPLETE_SETUP + self.redirect_page = False + #self.redirect_type = self.RedirectType.COMPLETE_SETUP elif "contact_setup_submit_button" in request.POST: - specific_redirect = "redirect_viewname" in self.session - self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE if specific_redirect else self.RedirectType.HOME + self.redirect_page = True + # specific_redirect = "redirect_viewname" in self.session + # self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE if specific_redirect else self.RedirectType.HOME return self.form_valid(form) else: return self.form_invalid(form) def get_success_url(self): """Redirect to the nameservers page for the domain.""" - return self.get_redirect_url() + # Get the redirect parameter from the form submission + redirect_param = self.request.POST.get('redirect', None) + if self.redirect_page and redirect_param: + return reverse(redirect_param) + return super().get_success_url() def get_redirect_url(self): """ From f2e0875091ac37753f6b80fd0ec89322a3180416 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 19 Jun 2024 14:47:52 -0400 Subject: [PATCH 07/45] cleaned up code and formatted for readability --- src/registrar/registrar_middleware.py | 10 +- .../templates/finish_profile_setup.html | 4 +- .../includes/finish_profile_form.html | 4 +- src/registrar/views/user_profile.py | 171 ++---------------- 4 files changed, 27 insertions(+), 162 deletions(-) diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 88eb9af2d..be787aca9 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -41,7 +41,7 @@ class CheckUserProfileMiddleware: self.setup_page = reverse("finish-user-profile-setup") self.profile_page = reverse("user-profile") self.logout_page = reverse("logout") - + self.regular_excluded_pages = [ self.setup_page, self.logout_page, @@ -52,12 +52,12 @@ class CheckUserProfileMiddleware: self.logout_page, "/admin", ] - + self.excluded_pages = { self.setup_page: self.regular_excluded_pages, - self.profile_page: self.other_excluded_pages + self.profile_page: self.other_excluded_pages, } - + def _get_excluded_pages(self, page): return self.excluded_pages.get(page, []) @@ -102,7 +102,7 @@ class CheckUserProfileMiddleware: custom_redirect = "domain-request:" if request.path == "/request/" else None # Don't redirect on excluded pages (such as the setup page itself) - #if not any(request.path.startswith(page) for page in excluded_pages): + # if not any(request.path.startswith(page) for page in excluded_pages): if not any(request.path.startswith(page) for page in self._get_excluded_pages(profile_page)): # Preserve the original query parameters, and coerce them into a dict diff --git a/src/registrar/templates/finish_profile_setup.html b/src/registrar/templates/finish_profile_setup.html index f8070551b..6e35ad5da 100644 --- a/src/registrar/templates/finish_profile_setup.html +++ b/src/registrar/templates/finish_profile_setup.html @@ -5,7 +5,7 @@ {# Disable the redirect #} {% block logo %} - {% include "includes/gov_extended_logo.html" with logo_clickable=confirm_changes %} + {% include "includes/gov_extended_logo.html" with logo_clickable=user_finished_setup %} {% endblock %} {# Add the new form #} @@ -16,5 +16,5 @@ {% endblock content_bottom %} {% block footer %} - {% include "includes/footer.html" with show_manage_your_domains=confirm_changes %} + {% include "includes/footer.html" with show_manage_your_domains=user_finished_setup %} {% endblock footer %} diff --git a/src/registrar/templates/includes/finish_profile_form.html b/src/registrar/templates/includes/finish_profile_form.html index 1e48d5578..d43ff812c 100644 --- a/src/registrar/templates/includes/finish_profile_form.html +++ b/src/registrar/templates/includes/finish_profile_form.html @@ -34,7 +34,7 @@ - + {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable usa-form-editable--no-border padding-top-2" %} {% input_with_errors form.full_name %} {% endwith %} @@ -80,7 +80,7 @@ - {% if confirm_changes and going_to_specific_page %} + {% if user_finished_setup and going_to_specific_page %} diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index ddb6136f5..939f11933 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -2,11 +2,7 @@ """ -from enum import Enum import logging -from urllib.parse import parse_qs, unquote - -from urllib.parse import quote from django.contrib import messages from django.http import QueryDict @@ -21,9 +17,6 @@ from registrar.models.utility.generic_helper import replace_url_queryparams from registrar.views.utility.permission_views import UserProfilePermissionView from waffle.decorators import flag_is_active, waffle_flag -from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_protect - logger = logging.getLogger(__name__) @@ -32,47 +25,20 @@ class UserProfileView(UserProfilePermissionView, FormMixin): Base View for the User Profile. Handles getting and setting the User Profile """ - class RedirectType(Enum): - """ - Enums for each type of redirection. Enforces behaviour on `get_redirect_url()`. - - - HOME: We want to redirect to reverse("home") - - BACK_TO_SELF: We want to redirect back to this page - - TO_SPECIFIC_PAGE: We want to redirect to the page specified in the queryparam "redirect" - - COMPLETE_SETUP: Indicates that we want to navigate BACK_TO_SELF, but the subsequent - redirect after the next POST should be either HOME or TO_SPECIFIC_PAGE - """ - - HOME = "home" - TO_SPECIFIC_PAGE = "domain_request" - BACK_TO_SELF = "back_to_self" - COMPLETE_SETUP = "complete_setup" - - @classmethod - def get_all_redirect_types(cls) -> list[str]: - """Returns the value of every redirect type defined in this enum.""" - return [r.value for r in cls] - model = Contact template_name = "profile.html" form_class = UserProfileForm base_view_name = "user-profile" - all_redirect_types = RedirectType.get_all_redirect_types() - redirect_type: RedirectType - def get(self, request, *args, **kwargs): """Handle get requests by getting user's contact object and setting object and form to context before rendering.""" - #self._refresh_session_and_object(request) self.object = self.get_object() # Get the redirect parameter from the query string - redirect = request.GET.get('redirect', 'home') + redirect = request.GET.get("redirect", "home") - logger.info(f"redirect value is {redirect}") - - form = self.form_class(instance=self.object, initial={'redirect': redirect}) + form = self.form_class(instance=self.object, initial={"redirect": redirect}) context = self.get_context_data(object=self.object, form=form, redirect=redirect) if ( @@ -82,22 +48,10 @@ class UserProfileView(UserProfilePermissionView, FormMixin): ): context["show_confirmation_modal"] = True - return_to_request = request.GET.get("return_to_request") - if return_to_request: - context["return_to_request"] = True - return self.render_to_response(context) - def _refresh_session_and_object(self, request): - """Sets the current session to self.session and the current object to self.object""" - self.session = request.session - self.object = self.get_object() - @waffle_flag("profile_feature") # type: ignore def dispatch(self, request, *args, **kwargs): # type: ignore - # Store the original queryparams to persist them - # query_params = request.META["QUERY_STRING"] - # request.session["query_params"] = query_params return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): @@ -107,7 +61,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin): context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") # Set the profile_back_button_text based on the redirect parameter - if kwargs.get('redirect') == 'domain-request:': + if kwargs.get("redirect") == "domain-request:": context["profile_back_button_text"] = "Go back to your request" else: context["profile_back_button_text"] = "Go to manage your domains" @@ -124,15 +78,15 @@ class UserProfileView(UserProfilePermissionView, FormMixin): """Redirect to the user's profile page with updated query parameters.""" # Get the redirect parameter from the form submission - redirect_param = self.request.POST.get('redirect', None) + redirect_param = self.request.POST.get("redirect", None) # Initialize QueryDict with existing query parameters from current request query_params = QueryDict(mutable=True) query_params.update(self.request.GET) # Update query parameters with the 'redirect' value from form submission - if redirect_param and redirect_param != 'home': - query_params['redirect'] = redirect_param + if redirect_param and redirect_param != "home": + query_params["redirect"] = redirect_param # Generate the URL with updated query parameters base_url = reverse(self.base_view_name) @@ -143,7 +97,6 @@ class UserProfileView(UserProfilePermissionView, FormMixin): def post(self, request, *args, **kwargs): """Handle post requests (form submissions)""" - #self._refresh_session_and_object(request) self.object = self.get_object() form = self.form_class(request.POST, instance=self.object) @@ -192,127 +145,39 @@ class FinishProfileSetupView(UserProfileView): # Show back button conditional on user having finished setup context["show_back_button"] = False if hasattr(self.user, "finished_setup") and self.user.finished_setup: - context["confirm_changes"] = True - if kwargs.get('redirect') == 'home': - context["profile_back_button_text"] = "Go to manage your domains" + if kwargs.get("redirect") == "home": context["show_back_button"] = True else: context["going_to_specific_page"] = True context["redirect_button_text"] = "Continue to your request" return context - - # def get_context_data(self, **kwargs): - - # context = super().get_context_data(**kwargs) - - # # Hide the back button by default - # context["show_back_button"] = False - - # logger.info(f"self.redirect_type = {self.redirect_type}") - # if self.redirect_type == self.RedirectType.COMPLETE_SETUP: - # context["confirm_changes"] = True - - # if "redirect_viewname" not in self.session: - # context["show_back_button"] = True - # else: - # context["going_to_specific_page"] = True - # context["redirect_button_text"] = "Continue to your request" - - # return context - - # @method_decorator(csrf_protect) - # def dispatch(self, request, *args, **kwargs): - # """ - # Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag. - - # This method sets the redirect type based on the 'redirect' query parameter, - # defaulting to BACK_TO_SELF if not provided. - # It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE. - - # Returns: - # HttpResponse: The response generated by the parent class's dispatch method. - # """ - - # # Update redirect type based on the query parameter if present - # default_redirect_value = self.RedirectType.BACK_TO_SELF.value - # redirect_value = request.GET.get("redirect", default_redirect_value) - - # if redirect_value in self.all_redirect_types: - # # If the redirect value is a preexisting value in our enum, set it to that. - # self.redirect_type = self.RedirectType(redirect_value) - # else: - # # If the redirect type is undefined, then we assume that we are specifying a particular page to redirect to. - # self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE - - # # Store the page that we want to redirect to for later use - # request.session["redirect_viewname"] = str(redirect_value) - - # return super().dispatch(request, *args, **kwargs) def post(self, request, *args, **kwargs): """Form submission posts to this view.""" - self._refresh_session_and_object(request) + self.object = self.get_object() form = self.form_class(request.POST, instance=self.object) # Get the current form and validate it if form.is_valid(): if "contact_setup_save_button" in request.POST: - # Logic for when the 'Save' button is clicked + # Logic for when the 'Save' button is clicked, which indicates + # user should stay on this page self.redirect_page = False - #self.redirect_type = self.RedirectType.COMPLETE_SETUP elif "contact_setup_submit_button" in request.POST: + # Logic for when the other button is clicked, which indicates + # the user should be taken to the redirect page self.redirect_page = True - # specific_redirect = "redirect_viewname" in self.session - # self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE if specific_redirect else self.RedirectType.HOME return self.form_valid(form) else: return self.form_invalid(form) def get_success_url(self): - """Redirect to the nameservers page for the domain.""" - # Get the redirect parameter from the form submission - redirect_param = self.request.POST.get('redirect', None) - if self.redirect_page and redirect_param: - return reverse(redirect_param) - return super().get_success_url() - - def get_redirect_url(self): - """ - Returns a URL string based on the current value of self.redirect_type. - - Depending on self.redirect_type, constructs a base URL and appends a - 'redirect' query parameter. Handles different redirection types such as - HOME, BACK_TO_SELF, COMPLETE_SETUP, and TO_SPECIFIC_PAGE. - - Returns: - str: The full URL with the appropriate query parameters. - """ - - # These redirect types redirect to the same page - self_redirect = [self.RedirectType.BACK_TO_SELF, self.RedirectType.COMPLETE_SETUP] - - # Maps the redirect type to a URL - base_url = "" + """Redirect to the redirect page, or redirect to the current page""" try: - if self.redirect_type in self_redirect: - base_url = reverse("finish-user-profile-setup") - elif self.redirect_type == self.RedirectType.TO_SPECIFIC_PAGE: - # We only allow this session value to use viewnames, - # because this restricts what can be redirected to. - desired_view = self.session["redirect_viewname"] - self.session.pop("redirect_viewname") - base_url = reverse(desired_view) - else: - base_url = reverse("home") + # Get the redirect parameter from the form submission + redirect_param = self.request.POST.get("redirect", None) + if self.redirect_page and redirect_param: + return reverse(redirect_param) except NoReverseMatch as err: logger.error(f"get_redirect_url -> Could not find the specified page. Err: {err}") - - query_params = {} - - # Quote cleans up the value so that it can be used in a url - if self.redirect_type and self.redirect_type.value: - query_params["redirect"] = quote(self.redirect_type.value) - - # Generate the full url from the given query params - full_url = replace_url_queryparams(base_url, query_params) - return full_url + return super().get_success_url() From 2ced462f94bb88b3caa48e004ff4e62fdcdf28fc Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 19 Jun 2024 15:24:31 -0400 Subject: [PATCH 08/45] wip --- src/registrar/views/user_profile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index 939f11933..865ed2b78 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -159,6 +159,7 @@ class FinishProfileSetupView(UserProfileView): # Get the current form and validate it if form.is_valid(): + self.redirect_page = True if "contact_setup_save_button" in request.POST: # Logic for when the 'Save' button is clicked, which indicates # user should stay on this page From e0df3ed4537e68bbe620a1d97c8d98f7ff56c50a Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 19 Jun 2024 16:02:57 -0400 Subject: [PATCH 09/45] updated tests --- src/registrar/tests/test_views.py | 16 +++++++++++++--- src/registrar/views/user_profile.py | 4 ++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index fa63da17a..c2ea2e6f4 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -530,8 +530,11 @@ class FinishUserProfileTests(TestWithUser, WebTest): session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - def _submit_form_webtest(self, form, follow=False): - page = form.submit() + def _submit_form_webtest(self, form, follow=False, name=None): + if name: + page = form.submit(name=name) + else: + page = form.submit() self._set_session_cookie() return page.follow() if follow else page @@ -605,6 +608,13 @@ class FinishUserProfileTests(TestWithUser, WebTest): self.assertEqual(completed_setup_page.status_code, 200) + finish_setup_form = completed_setup_page.form + + # Submit the form using the specific submit button to execute the redirect + completed_setup_page = self._submit_form_webtest(finish_setup_form, follow=True, name="contact_setup_submit_button") + self.assertEqual(completed_setup_page.status_code, 200) + + # Assert that we are still on the # Assert that we're on the domain request page self.assertNotContains(completed_setup_page, "Finish setting up your profile") self.assertNotContains(completed_setup_page, "What contact information should we use to reach you?") @@ -822,7 +832,7 @@ class UserProfileTests(TestWithUser, WebTest): """tests user profile when profile_feature is on, and when they are redirected from the domain request page""" with override_flag("profile_feature", active=True): - response = self.client.get("/user-profile?return_to_request=True") + response = self.client.get("/user-profile?redirect=domain-request:") self.assertContains(response, "Your profile") self.assertContains(response, "Go back to your domain request") self.assertNotContains(response, "Back to manage your domains") diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index 865ed2b78..3f9aeb79f 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -62,7 +62,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin): # Set the profile_back_button_text based on the redirect parameter if kwargs.get("redirect") == "domain-request:": - context["profile_back_button_text"] = "Go back to your request" + context["profile_back_button_text"] = "Go back to your domain request" else: context["profile_back_button_text"] = "Go to manage your domains" @@ -159,7 +159,7 @@ class FinishProfileSetupView(UserProfileView): # Get the current form and validate it if form.is_valid(): - self.redirect_page = True + self.redirect_page = False if "contact_setup_save_button" in request.POST: # Logic for when the 'Save' button is clicked, which indicates # user should stay on this page From 60f10cb3cd88947cb75fa0967fd5abd021af9f97 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 19 Jun 2024 16:05:14 -0400 Subject: [PATCH 10/45] updated code for readability --- src/registrar/tests/test_views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index c2ea2e6f4..f719ba39f 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -611,10 +611,12 @@ class FinishUserProfileTests(TestWithUser, WebTest): finish_setup_form = completed_setup_page.form # Submit the form using the specific submit button to execute the redirect - completed_setup_page = self._submit_form_webtest(finish_setup_form, follow=True, name="contact_setup_submit_button") + completed_setup_page = self._submit_form_webtest( + finish_setup_form, follow=True, name="contact_setup_submit_button" + ) self.assertEqual(completed_setup_page.status_code, 200) - # Assert that we are still on the + # Assert that we are still on the # Assert that we're on the domain request page self.assertNotContains(completed_setup_page, "Finish setting up your profile") self.assertNotContains(completed_setup_page, "What contact information should we use to reach you?") From f2c3c320e9b751fa9f89c8b542a36d4dbe5aa8bb Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 19 Jun 2024 21:38:20 -0400 Subject: [PATCH 11/45] added portfolio(s) to the middleware --- src/registrar/config/urls.py | 2 +- src/registrar/registrar_middleware.py | 20 +++++++++++++------- src/registrar/views/index_organizations.py | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 72115f66a..ad53a8b36 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -60,7 +60,7 @@ for step, view in [ urlpatterns = [ path("", views.index, name="home"), path( - "organization", + "organization//", index_organizations, name="home-organization", ), diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 04ec9fa13..bb0d678c2 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -6,6 +6,7 @@ import logging from urllib.parse import parse_qs from django.urls import reverse from django.http import HttpResponseRedirect +from registrar.models.portfolio import Portfolio from registrar.models.user import User from waffle.decorators import flag_is_active @@ -128,7 +129,6 @@ class CheckOrganizationMiddleware: def __init__(self, get_response): self.get_response = get_response - self.home_organization = reverse("home-organization") self.home = reverse("home") self.json1 = reverse("get_domains_json") self.json2 = reverse("get_domain_requests_json") @@ -141,16 +141,22 @@ class CheckOrganizationMiddleware: current_path = request.path logger.debug(f"Current path: {current_path}") - # Avoid infinite loop by skipping the redirect check on the home-organization URL - if current_path == self.home_organization or current_path == self.json1 or current_path == self.json2: - logger.debug("Skipping middleware check for home-organization URL") + # Avoid infinite loop by skipping the redirect check on the home-organization URL and other JSON URLs + if current_path in [self.json1, self.json2] or current_path.startswith('/admin'): + logger.debug("Skipping middleware check for home-organization and JSON URLs") return None has_organization_feature_flag = flag_is_active(request, "organization_feature") logger.debug(f"Flag is active: {has_organization_feature_flag}") if has_organization_feature_flag: - logger.debug(f"Redirecting to {self.home_organization}") - return HttpResponseRedirect(self.home_organization) - + if request.user.is_authenticated: + user_portfolios = Portfolio.objects.filter(creator=request.user) + if user_portfolios.exists(): + first_portfolio = user_portfolios.first() + home_organization_with_portfolio = reverse("home-organization", kwargs={'portfolio_id': first_portfolio.id}) + + if current_path != home_organization_with_portfolio: + logger.debug(f"User has portfolios, redirecting to {home_organization_with_portfolio}") + return HttpResponseRedirect(home_organization_with_portfolio) return None diff --git a/src/registrar/views/index_organizations.py b/src/registrar/views/index_organizations.py index 4ec15ad59..3f881c3f3 100644 --- a/src/registrar/views/index_organizations.py +++ b/src/registrar/views/index_organizations.py @@ -2,7 +2,7 @@ from django.shortcuts import render from waffle.decorators import flag_is_active -def index_organizations(request): +def index_organizations(request, portfolio_id): """This page is available to anyone without logging in.""" context = {} From bd2ad797cfea0c5ad37c3f4c7cd419afe04ca572 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 20 Jun 2024 08:24:57 -0400 Subject: [PATCH 12/45] additional views added --- src/registrar/config/urls.py | 12 +++- src/registrar/registrar_middleware.py | 6 +- .../templates/home_organizations.html | 10 +-- .../templates/organization_sidebar.html | 11 ++-- src/registrar/views/index_organizations.py | 19 ------ src/registrar/views/organizations.py | 64 +++++++++++++++++++ 6 files changed, 91 insertions(+), 31 deletions(-) delete mode 100644 src/registrar/views/index_organizations.py create mode 100644 src/registrar/views/organizations.py diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index ad53a8b36..b693a9b65 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -25,7 +25,7 @@ from registrar.views.domain_request import Step from registrar.views.domain_requests_json import get_domain_requests_json from registrar.views.domains_json import get_domains_json from registrar.views.utility import always_404 -from registrar.views.index_organizations import index_organizations +from registrar.views.organizations import index_organizations, organization_domains, organization_domain_requests from api.views import available, get_current_federal, get_current_full @@ -64,6 +64,16 @@ urlpatterns = [ index_organizations, name="home-organization", ), + path( + "organization//domains/", + organization_domains, + name="organization-domains", + ), + path( + "organization//domain_requests/", + organization_domain_requests, + name="organization-domain-requests", + ), path( "admin/logout/", RedirectView.as_view(pattern_name="logout", permanent=False), diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index bb0d678c2..86817f63d 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -142,8 +142,8 @@ class CheckOrganizationMiddleware: logger.debug(f"Current path: {current_path}") # Avoid infinite loop by skipping the redirect check on the home-organization URL and other JSON URLs - if current_path in [self.json1, self.json2] or current_path.startswith('/admin'): - logger.debug("Skipping middleware check for home-organization and JSON URLs") + if current_path in [self.json1, self.json2] or current_path.startswith('/admin') or current_path.startswith('/organization'): + logger.debug("Skipping middleware check for admin, organization and JSON URLs") return None has_organization_feature_flag = flag_is_active(request, "organization_feature") @@ -154,7 +154,7 @@ class CheckOrganizationMiddleware: user_portfolios = Portfolio.objects.filter(creator=request.user) if user_portfolios.exists(): first_portfolio = user_portfolios.first() - home_organization_with_portfolio = reverse("home-organization", kwargs={'portfolio_id': first_portfolio.id}) + home_organization_with_portfolio = reverse("organization-domains", kwargs={'portfolio_id': first_portfolio.id}) if current_path != home_organization_with_portfolio: logger.debug(f"User has portfolios, redirecting to {home_organization_with_portfolio}") diff --git a/src/registrar/templates/home_organizations.html b/src/registrar/templates/home_organizations.html index 5edd3860a..8ce67d146 100644 --- a/src/registrar/templates/home_organizations.html +++ b/src/registrar/templates/home_organizations.html @@ -7,7 +7,7 @@
- {% include "organization_sidebar.html" %} + {% include "organization_sidebar.html" with portfolio=portfolio current_path=content %}
{% block messages %} @@ -26,9 +26,11 @@ Start a new domain request

- - {% include "includes/domains_table.html" %} - {% include "includes/domain_requests_table.html" %} + {% if content == 'domains' %} + {% include "includes/domains_table.html" with portfolio=portfolio %} + {% elif content == 'domain-requests' %} + {% include "includes/domain_requests_table.html" with portfolio=portfolio %} + {% endif %} {# Note: Reimplement this after MVP #} {% comment %} IMPORTANT: If this button is added on any other page, make sure to update the relevant view to reset request.session["new_request"] = True {% endcomment %} + {% if content == 'domains' %} {% include "includes/domains_table.html" with portfolio=portfolio %} {% elif content == 'domain-requests' %} diff --git a/src/registrar/templates/organization_sidebar.html b/src/registrar/templates/organization_sidebar.html index 7d1b606f2..35d9d5e95 100644 --- a/src/registrar/templates/organization_sidebar.html +++ b/src/registrar/templates/organization_sidebar.html @@ -16,6 +16,21 @@ Domain requests +
  • + + Members + +
  • +
  • + + Organization + +
  • +
  • + + Senior official + +
  • From 300e305c005152ead50c38f04072852d2fce11cf Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 20 Jun 2024 11:27:20 -0400 Subject: [PATCH 14/45] cleanup --- src/registrar/config/urls.py | 7 +---- src/registrar/registrar_middleware.py | 28 ++++++++----------- .../templates/organization_sidebar.html | 7 +++-- ..._organizations.html => organizations.html} | 2 +- src/registrar/views/organizations.py | 24 ++-------------- 5 files changed, 20 insertions(+), 48 deletions(-) rename src/registrar/templates/{home_organizations.html => organizations.html} (99%) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index b693a9b65..3e0d44c4c 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -25,7 +25,7 @@ from registrar.views.domain_request import Step from registrar.views.domain_requests_json import get_domain_requests_json from registrar.views.domains_json import get_domains_json from registrar.views.utility import always_404 -from registrar.views.organizations import index_organizations, organization_domains, organization_domain_requests +from registrar.views.organizations import organization_domains, organization_domain_requests from api.views import available, get_current_federal, get_current_full @@ -59,11 +59,6 @@ for step, view in [ urlpatterns = [ path("", views.index, name="home"), - path( - "organization//", - index_organizations, - name="home-organization", - ), path( "organization//domains/", organization_domains, diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 86817f63d..30137ee63 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -130,8 +130,6 @@ class CheckOrganizationMiddleware: def __init__(self, get_response): self.get_response = get_response self.home = reverse("home") - self.json1 = reverse("get_domains_json") - self.json2 = reverse("get_domain_requests_json") def __call__(self, request): response = self.get_response(request) @@ -141,22 +139,18 @@ class CheckOrganizationMiddleware: current_path = request.path logger.debug(f"Current path: {current_path}") - # Avoid infinite loop by skipping the redirect check on the home-organization URL and other JSON URLs - if current_path in [self.json1, self.json2] or current_path.startswith('/admin') or current_path.startswith('/organization'): - logger.debug("Skipping middleware check for admin, organization and JSON URLs") - return None - has_organization_feature_flag = flag_is_active(request, "organization_feature") logger.debug(f"Flag is active: {has_organization_feature_flag}") - if has_organization_feature_flag: - if request.user.is_authenticated: - user_portfolios = Portfolio.objects.filter(creator=request.user) - if user_portfolios.exists(): - first_portfolio = user_portfolios.first() - home_organization_with_portfolio = reverse("organization-domains", kwargs={'portfolio_id': first_portfolio.id}) - - if current_path != home_organization_with_portfolio: - logger.debug(f"User has portfolios, redirecting to {home_organization_with_portfolio}") - return HttpResponseRedirect(home_organization_with_portfolio) + if current_path == self.home: + if has_organization_feature_flag: + if request.user.is_authenticated: + user_portfolios = Portfolio.objects.filter(creator=request.user) + if user_portfolios.exists(): + first_portfolio = user_portfolios.first() + home_organization_with_portfolio = reverse("organization-domains", kwargs={'portfolio_id': first_portfolio.id}) + + if current_path != home_organization_with_portfolio: + logger.debug(f"User has portfolios, redirecting to {home_organization_with_portfolio}") + return HttpResponseRedirect(home_organization_with_portfolio) return None diff --git a/src/registrar/templates/organization_sidebar.html b/src/registrar/templates/organization_sidebar.html index 35d9d5e95..4f2779816 100644 --- a/src/registrar/templates/organization_sidebar.html +++ b/src/registrar/templates/organization_sidebar.html @@ -7,12 +7,15 @@ {{ portfolio.organization_name }}
  • - + {% url 'organization-domains' portfolio.id as url %} + Domains +
  • - + {% url 'organization-domain-requests' portfolio.id as url %} + Domain requests
  • diff --git a/src/registrar/templates/home_organizations.html b/src/registrar/templates/organizations.html similarity index 99% rename from src/registrar/templates/home_organizations.html rename to src/registrar/templates/organizations.html index ba97ca292..b354fb1bf 100644 --- a/src/registrar/templates/home_organizations.html +++ b/src/registrar/templates/organizations.html @@ -30,7 +30,7 @@ Start a new domain request

    - --> + --> {% if content == 'domains' %} {% include "includes/domains_table.html" with portfolio=portfolio %} {% elif content == 'domain-requests' %} diff --git a/src/registrar/views/organizations.py b/src/registrar/views/organizations.py index 4a736e1f1..8f0578859 100644 --- a/src/registrar/views/organizations.py +++ b/src/registrar/views/organizations.py @@ -3,26 +3,6 @@ from registrar.models.portfolio import Portfolio from waffle.decorators import flag_is_active -def index_organizations(request, portfolio_id): - """This page is available to anyone without logging in.""" - context = {} - - if request.user.is_authenticated: - # This is a django waffle flag which toggles features based off of the "flag" table - context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") - context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature") - - # Retrieve the portfolio object based on the provided portfolio_id - portfolio = get_object_or_404(Portfolio, id=portfolio_id) - context["portfolio"] = portfolio - - # This controls the creation of a new domain request in the wizard - request.session["new_request"] = True - - print('homepage organizations view') - - return render(request, "home_organizations.html", context) - def organization_domains(request, portfolio_id): context = {} @@ -41,7 +21,7 @@ def organization_domains(request, portfolio_id): print('organization domains view') - return render(request, "home_organizations.html", context) + return render(request, "organizations.html", context) def organization_domain_requests(request, portfolio_id): context = {} @@ -61,4 +41,4 @@ def organization_domain_requests(request, portfolio_id): print('organization domain requests view') - return render(request, "home_organizations.html", context) \ No newline at end of file + return render(request, "organizations.html", context) \ No newline at end of file From c7544dabe2f1edddfc7651ca3e96bf82fd96b430 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 20 Jun 2024 12:55:42 -0400 Subject: [PATCH 15/45] Unit tests and cleanup --- src/registrar/registrar_middleware.py | 15 ++-- .../includes/domain_requests_table.html | 8 +- .../templates/includes/domains_table.html | 8 +- .../{organizations.html => organization.html} | 27 +------ .../templates/organization_domains.html | 8 ++ .../templates/organization_requests.html | 21 +++++ .../templates/organization_sidebar.html | 4 +- src/registrar/tests/test_views.py | 76 ++++++++++++++++++- src/registrar/views/index.py | 2 +- src/registrar/views/organizations.py | 12 +-- 10 files changed, 131 insertions(+), 50 deletions(-) rename src/registrar/templates/{organizations.html => organization.html} (60%) create mode 100644 src/registrar/templates/organization_domains.html create mode 100644 src/registrar/templates/organization_requests.html diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 30137ee63..79a728310 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -14,6 +14,7 @@ from registrar.models.utility.generic_helper import replace_url_queryparams logger = logging.getLogger(__name__) + class NoCacheMiddleware: """ Middleware to add Cache-control: no-cache to every response. @@ -122,9 +123,12 @@ class CheckUserProfileMiddleware: else: # Process the view as normal return None - + + class CheckOrganizationMiddleware: """ + Checks if the current user has a portfolio + If they do, redirect them to the org homepage when they navigate to home. """ def __init__(self, get_response): @@ -137,10 +141,8 @@ class CheckOrganizationMiddleware: def process_view(self, request, view_func, view_args, view_kwargs): current_path = request.path - logger.debug(f"Current path: {current_path}") has_organization_feature_flag = flag_is_active(request, "organization_feature") - logger.debug(f"Flag is active: {has_organization_feature_flag}") if current_path == self.home: if has_organization_feature_flag: @@ -148,9 +150,10 @@ class CheckOrganizationMiddleware: user_portfolios = Portfolio.objects.filter(creator=request.user) if user_portfolios.exists(): first_portfolio = user_portfolios.first() - home_organization_with_portfolio = reverse("organization-domains", kwargs={'portfolio_id': first_portfolio.id}) - + home_organization_with_portfolio = reverse( + "organization-domains", kwargs={"portfolio_id": first_portfolio.id} + ) + if current_path != home_organization_with_portfolio: - logger.debug(f"User has portfolios, redirecting to {home_organization_with_portfolio}") return HttpResponseRedirect(home_organization_with_portfolio) return None diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html index 377f49d02..e760687b6 100644 --- a/src/registrar/templates/includes/domain_requests_table.html +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -2,9 +2,11 @@
    -
    -

    Domain requests

    -
    + {% if portfolio is None %} +
    +

    Domain requests

    +
    + {% endif %}
    diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index 334dba3da..4347520cc 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -2,9 +2,11 @@
    -
    -

    Domains

    -
    + {% if portfolio is None %} +
    +

    Domains

    +
    + {% endif %}
    diff --git a/src/registrar/templates/organizations.html b/src/registrar/templates/organization.html similarity index 60% rename from src/registrar/templates/organizations.html rename to src/registrar/templates/organization.html index b354fb1bf..33ab7ff6c 100644 --- a/src/registrar/templates/organizations.html +++ b/src/registrar/templates/organization.html @@ -7,35 +7,16 @@
    - {% include "organization_sidebar.html" with portfolio=portfolio current_path=content %} + {% include "organization_sidebar.html" with portfolio=portfolio %}
    {% block messages %} {% include "includes/form_messages.html" %} {% endblock %} {# Note: Reimplement commented out functionality #} - - - {% comment %} - IMPORTANT: - If this button is added on any other page, make sure to update the - relevant view to reset request.session["new_request"] = True - {% endcomment %} - - {% if content == 'domains' %} - {% include "includes/domains_table.html" with portfolio=portfolio %} - {% elif content == 'domain-requests' %} - {% include "includes/domain_requests_table.html" with portfolio=portfolio %} - {% endif %} + + {% block organization_content %} + {% endblock %} {# Note: Reimplement this after MVP #} - - - - - -
    -
    - -{% endblock %} diff --git a/src/registrar/templates/portfolio.html b/src/registrar/templates/portfolio.html new file mode 100644 index 000000000..4f37c0175 --- /dev/null +++ b/src/registrar/templates/portfolio.html @@ -0,0 +1,24 @@ +{% extends 'home.html' %} + +{% load static %} + +{% block homepage_content %} + +
    +
    +
    + {% include "portfolio_sidebar.html" with portfolio=portfolio %} +
    +
    + {% block messages %} + {% include "includes/form_messages.html" %} + {% endblock %} + {# Note: Reimplement commented out functionality #} + + {% block portfolio_content %} + {% endblock %} + +
    +
    + +{% endblock %} diff --git a/src/registrar/templates/organization_domains.html b/src/registrar/templates/portfolio_domains.html similarity index 65% rename from src/registrar/templates/organization_domains.html rename to src/registrar/templates/portfolio_domains.html index 9dff1fb0b..3009b4b14 100644 --- a/src/registrar/templates/organization_domains.html +++ b/src/registrar/templates/portfolio_domains.html @@ -1,8 +1,8 @@ -{% extends 'organization.html' %} +{% extends 'portfolio.html' %} {% load static %} -{% block organization_content %} +{% block portfolio_content %}

    Domains

    {% include "includes/domains_table.html" with portfolio=portfolio %} {% endblock %} diff --git a/src/registrar/templates/organization_requests.html b/src/registrar/templates/portfolio_requests.html similarity index 88% rename from src/registrar/templates/organization_requests.html rename to src/registrar/templates/portfolio_requests.html index 29d8e589d..b95f03914 100644 --- a/src/registrar/templates/organization_requests.html +++ b/src/registrar/templates/portfolio_requests.html @@ -1,8 +1,8 @@ -{% extends 'organization.html' %} +{% extends 'portfolio.html' %} {% load static %} -{% block organization_content %} +{% block portfolio_content %}

    Domain requests

    {% comment %} diff --git a/src/registrar/templates/organization_sidebar.html b/src/registrar/templates/portfolio_sidebar.html similarity index 87% rename from src/registrar/templates/organization_sidebar.html rename to src/registrar/templates/portfolio_sidebar.html index d2d7cd306..94c4a0b16 100644 --- a/src/registrar/templates/organization_sidebar.html +++ b/src/registrar/templates/portfolio_sidebar.html @@ -5,14 +5,14 @@

    {{ portfolio.organization_name }}

    • - {% url 'organization-domains' portfolio.id as url %} + {% url 'portfolio-domains' portfolio.id as url %} Domains
    • - {% url 'organization-domain-requests' portfolio.id as url %} + {% url 'portfolio-domain-requests' portfolio.id as url %} Domain requests diff --git a/src/registrar/views/organizations.py b/src/registrar/views/portfolios.py similarity index 85% rename from src/registrar/views/organizations.py rename to src/registrar/views/portfolios.py index 61b764d7e..5ecd5d1d0 100644 --- a/src/registrar/views/organizations.py +++ b/src/registrar/views/portfolios.py @@ -5,7 +5,7 @@ from django.contrib.auth.decorators import login_required @login_required -def organization_domains(request, portfolio_id): +def portfolio_domains(request, portfolio_id): context = {} if request.user.is_authenticated: @@ -17,11 +17,11 @@ def organization_domains(request, portfolio_id): portfolio = get_object_or_404(Portfolio, id=portfolio_id) context["portfolio"] = portfolio - return render(request, "organization_domains.html", context) + return render(request, "portfolio_domains.html", context) @login_required -def organization_domain_requests(request, portfolio_id): +def portfolio_domain_requests(request, portfolio_id): context = {} if request.user.is_authenticated: @@ -36,4 +36,4 @@ def organization_domain_requests(request, portfolio_id): # This controls the creation of a new domain request in the wizard request.session["new_request"] = True - return render(request, "organization_requests.html", context) + return render(request, "portfolio_requests.html", context) From aa652fac1bb2332ce5ac715e95c00a32c456d7e6 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 20 Jun 2024 16:43:57 -0400 Subject: [PATCH 20/45] updating tests from org to portfolio --- src/registrar/tests/test_views.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 2554bed01..5c8232d85 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -908,7 +908,7 @@ class UserProfileTests(TestWithUser, WebTest): self.assertContains(profile_page, "Your profile has been updated") -class OrganizationsTests(TestWithUser, WebTest): +class PortfoliosTests(TestWithUser, WebTest): """A series of tests that target the organizations""" # csrf checks do not work well with WebTest. @@ -939,33 +939,33 @@ class OrganizationsTests(TestWithUser, WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @less_console_noise_decorator - def test_middleware_redirects_to_organization_homepage(self): - """Tests that a user is redirected to the org homepage when organization_feature is on and + def test_middleware_redirects_to_portfolio_homepage(self): + """Tests that a user is redirected to the portfolio homepage when organization_feature is on and a portfolio belongs to the user, test for the special h1s which only exist in that version of the homepage""" self.app.set_user(self.user.username) with override_flag("organization_feature", active=True): - # This will redirect the user to the org page. + # This will redirect the user to the portfolio page. # Follow implicity checks if our redirect is working. - org_page = self.app.get(reverse("home")).follow() + portfolio_page = self.app.get(reverse("home")).follow() self._set_session_cookie() # Assert that we're on the right page - self.assertContains(org_page, self.portfolio.organization_name) + self.assertContains(portfolio_page, self.portfolio.organization_name) - self.assertContains(org_page, "

      Domains

      ") + self.assertContains(portfolio_page, "

      Domains

      ") @less_console_noise_decorator def test_no_redirect_when_org_flag_false(self): """No redirect so no follow, implicitely test for the presense of the h2 by looking up its id""" self.app.set_user(self.user.username) - org_page = self.app.get(reverse("home")) + home_page = self.app.get(reverse("home")) self._set_session_cookie() - self.assertNotContains(org_page, self.portfolio.organization_name) + self.assertNotContains(home_page, self.portfolio.organization_name) - self.assertContains(org_page, 'id="domain-requests-header"') + self.assertContains(home_page, 'id="domain-requests-header"') @less_console_noise_decorator def test_no_redirect_when_user_has_no_portfolios(self): @@ -974,9 +974,9 @@ class OrganizationsTests(TestWithUser, WebTest): self.portfolio.delete() self.app.set_user(self.user.username) with override_flag("organization_feature", active=True): - org_page = self.app.get(reverse("home")) + home_page = self.app.get(reverse("home")) self._set_session_cookie() - self.assertNotContains(org_page, self.portfolio.organization_name) + self.assertNotContains(home_page, self.portfolio.organization_name) - self.assertContains(org_page, 'id="domain-requests-header"') + self.assertContains(home_page, 'id="domain-requests-header"') From 8b6f555fc48e02b5c700916c2722a610e5e76586 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 20 Jun 2024 16:48:31 -0400 Subject: [PATCH 21/45] formatted for code readability --- src/registrar/registrar_middleware.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 880f9ee40..f5274c99f 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -150,8 +150,6 @@ class CheckPortfolioMiddleware: user_portfolios = Portfolio.objects.filter(creator=request.user) if user_portfolios.exists(): first_portfolio = user_portfolios.first() - home_with_portfolio = reverse( - "portfolio-domains", kwargs={"portfolio_id": first_portfolio.id} - ) + home_with_portfolio = reverse("portfolio-domains", kwargs={"portfolio_id": first_portfolio.id}) return HttpResponseRedirect(home_with_portfolio) return None From 2995792375830a4b6247059d5473ca48df31b627 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:19:59 -0700 Subject: [PATCH 22/45] Add DomainGroup and Suborg models --- src/registrar/admin.py | 23 +++++++++++ .../0105_suborganization_domaingroup.py | 41 +++++++++++++++++++ src/registrar/models/__init__.py | 6 +++ src/registrar/models/domain_group.py | 33 +++++++++++++++ src/registrar/models/portfolio.py | 3 ++ src/registrar/models/suborganization.py | 23 +++++++++++ 6 files changed, 129 insertions(+) create mode 100644 src/registrar/migrations/0105_suborganization_domaingroup.py create mode 100644 src/registrar/models/domain_group.py create mode 100644 src/registrar/models/suborganization.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 215239d66..c0f52c696 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2655,6 +2655,27 @@ class WaffleFlagAdmin(FlagAdmin): model = models.WaffleFlag fields = "__all__" +class DomainGroupResource(resources.ModelResource): + """defines how each field in the referenced model should be mapped to the corresponding fields in the + import/export file""" + + class Meta: + model = models.DomainGroup + +class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin): + resource_classes = [DomainGroupResource] + list_display = ["name", "portfolio"] + +class SuborganizationResource(resources.ModelResource): + """defines how each field in the referenced model should be mapped to the corresponding fields in the + import/export file""" + + class Meta: + model = models.Suborganization + +class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): + resource_classes = [SuborganizationResource] + list_display = ["name", "portfolio"] admin.site.unregister(LogEntry) # Unregister the default registration @@ -2679,6 +2700,8 @@ admin.site.register(models.DomainRequest, DomainRequestAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin) admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) admin.site.register(models.Portfolio, PortfolioAdmin) +admin.site.register(models.DomainGroup, DomainGroupAdmin) +admin.site.register(models.Suborganization, SuborganizationAdmin) # Register our custom waffle implementations admin.site.register(models.WaffleFlag, WaffleFlagAdmin) diff --git a/src/registrar/migrations/0105_suborganization_domaingroup.py b/src/registrar/migrations/0105_suborganization_domaingroup.py new file mode 100644 index 000000000..3d99a4001 --- /dev/null +++ b/src/registrar/migrations/0105_suborganization_domaingroup.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.10 on 2024-06-20 21:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0104_create_groups_v13"), + ] + + operations = [ + migrations.CreateModel( + name="Suborganization", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(blank=True, help_text="Domain group", null=True, unique=True)), + ("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="DomainGroup", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(blank=True, help_text="Domain group", null=True, unique=True)), + ("domains", models.ManyToManyField(blank=True, to="registrar.domaininformation")), + ("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")), + ], + options={ + "unique_together": {("name", "portfolio")}, + }, + ), + ] diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index b2cffaf32..2af89ce00 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -17,6 +17,8 @@ from .transition_domain import TransitionDomain from .verified_by_staff import VerifiedByStaff from .waffle_flag import WaffleFlag from .portfolio import Portfolio +from .domain_group import DomainGroup +from .suborganization import Suborganization __all__ = [ @@ -38,6 +40,8 @@ __all__ = [ "VerifiedByStaff", "WaffleFlag", "Portfolio", + "DomainGroup", + "Suborganization" ] auditlog.register(Contact) @@ -58,3 +62,5 @@ auditlog.register(TransitionDomain) auditlog.register(VerifiedByStaff) auditlog.register(WaffleFlag) auditlog.register(Portfolio) +auditlog.register(DomainGroup) +auditlog.register(Suborganization) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py new file mode 100644 index 000000000..aa6d64e1f --- /dev/null +++ b/src/registrar/models/domain_group.py @@ -0,0 +1,33 @@ +from django.db import models +from .utility.time_stamped_model import TimeStampedModel +from registrar.models.portfolio import Portfolio +from registrar.models.domain_information import DomainInformation + + +class DomainGroup(TimeStampedModel): + + class Meta: + unique_together = [("name", "portfolio")] + + """ + TODO: Write DomainGroup description + """ + name = models.CharField( + null=True, + blank=True, + unique=True, + help_text="Domain group", + ) + + portfolio = models.ForeignKey( + "registrar.Portfolio", + on_delete=models.PROTECT + ) + + domains = models.ManyToManyField( + "registrar.DomainInformation", + blank=True + ) + + def __str__(self) -> str: + return f"{self.name}" \ No newline at end of file diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index a05422960..0ea036bb7 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -97,3 +97,6 @@ class Portfolio(TimeStampedModel): verbose_name="security contact e-mail", max_length=320, ) + + def __str__(self) -> str: + return f"{self.organization_name}" diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py new file mode 100644 index 000000000..6ebae62e2 --- /dev/null +++ b/src/registrar/models/suborganization.py @@ -0,0 +1,23 @@ +from django.db import models +from .utility.time_stamped_model import TimeStampedModel +from registrar.models.portfolio import Portfolio + + +class Suborganization(TimeStampedModel): + """ + TODO: Write DomainGroup description + """ + name = models.CharField( + null=True, + blank=True, + unique=True, + help_text="Domain group", + ) + + portfolio = models.ForeignKey( + "registrar.Portfolio", + on_delete=models.PROTECT, + ) + + def __str__(self) -> str: + return f"{self.name}" From b48802e0d7cfe73b2b7f44bb015c28145e4996b6 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:21:57 -0700 Subject: [PATCH 23/45] Change suborganization name help text --- src/registrar/migrations/0105_suborganization_domaingroup.py | 4 ++-- src/registrar/models/suborganization.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/migrations/0105_suborganization_domaingroup.py b/src/registrar/migrations/0105_suborganization_domaingroup.py index 3d99a4001..95d16e3b4 100644 --- a/src/registrar/migrations/0105_suborganization_domaingroup.py +++ b/src/registrar/migrations/0105_suborganization_domaingroup.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-06-20 21:18 +# Generated by Django 4.2.10 on 2024-06-20 21:21 from django.db import migrations, models import django.db.models.deletion @@ -17,7 +17,7 @@ class Migration(migrations.Migration): ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), - ("name", models.CharField(blank=True, help_text="Domain group", null=True, unique=True)), + ("name", models.CharField(blank=True, help_text="Suborganization", null=True, unique=True)), ("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")), ], options={ diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index 6ebae62e2..15612e7d2 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -11,7 +11,7 @@ class Suborganization(TimeStampedModel): null=True, blank=True, unique=True, - help_text="Domain group", + help_text="Suborganization", ) portfolio = models.ForeignKey( From 0a80cb1c62b68c0dbccfbc237dd98ffee4caf0ad Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:42:27 -0700 Subject: [PATCH 24/45] Update descriptions of DomainGroup and Suborg --- src/registrar/models/domain_group.py | 2 +- src/registrar/models/suborganization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py index aa6d64e1f..f43627251 100644 --- a/src/registrar/models/domain_group.py +++ b/src/registrar/models/domain_group.py @@ -10,7 +10,7 @@ class DomainGroup(TimeStampedModel): unique_together = [("name", "portfolio")] """ - TODO: Write DomainGroup description + Organized group of domains. """ name = models.CharField( null=True, diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index 15612e7d2..c90a76f27 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -5,7 +5,7 @@ from registrar.models.portfolio import Portfolio class Suborganization(TimeStampedModel): """ - TODO: Write DomainGroup description + Suborganization under an organization (portfolio) """ name = models.CharField( null=True, From a2a43b66b4a394bd8683178584e2241419125483 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:43:22 -0700 Subject: [PATCH 25/45] Fix lint errors --- src/registrar/admin.py | 5 +++++ src/registrar/models/__init__.py | 2 +- src/registrar/models/domain_group.py | 12 +++--------- src/registrar/models/suborganization.py | 1 + 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index c0f52c696..b1dcb1f6e 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2655,6 +2655,7 @@ class WaffleFlagAdmin(FlagAdmin): model = models.WaffleFlag fields = "__all__" + class DomainGroupResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file""" @@ -2662,10 +2663,12 @@ class DomainGroupResource(resources.ModelResource): class Meta: model = models.DomainGroup + class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin): resource_classes = [DomainGroupResource] list_display = ["name", "portfolio"] + class SuborganizationResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the import/export file""" @@ -2673,10 +2676,12 @@ class SuborganizationResource(resources.ModelResource): class Meta: model = models.Suborganization + class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): resource_classes = [SuborganizationResource] list_display = ["name", "portfolio"] + admin.site.unregister(LogEntry) # Unregister the default registration admin.site.register(LogEntry, CustomLogEntryAdmin) diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 2af89ce00..376739826 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -41,7 +41,7 @@ __all__ = [ "WaffleFlag", "Portfolio", "DomainGroup", - "Suborganization" + "Suborganization", ] auditlog.register(Contact) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py index f43627251..7f77cdc16 100644 --- a/src/registrar/models/domain_group.py +++ b/src/registrar/models/domain_group.py @@ -19,15 +19,9 @@ class DomainGroup(TimeStampedModel): help_text="Domain group", ) - portfolio = models.ForeignKey( - "registrar.Portfolio", - on_delete=models.PROTECT - ) + portfolio = models.ForeignKey("registrar.Portfolio", on_delete=models.PROTECT) - domains = models.ManyToManyField( - "registrar.DomainInformation", - blank=True - ) + domains = models.ManyToManyField("registrar.DomainInformation", blank=True) def __str__(self) -> str: - return f"{self.name}" \ No newline at end of file + return f"{self.name}" diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index c90a76f27..c731c64b3 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -7,6 +7,7 @@ class Suborganization(TimeStampedModel): """ Suborganization under an organization (portfolio) """ + name = models.CharField( null=True, blank=True, From b8588b326628e2c3bc1e6b506f04f462aa12cec4 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:46:24 -0700 Subject: [PATCH 26/45] Remove unused imports --- src/registrar/models/domain_group.py | 2 -- src/registrar/models/suborganization.py | 1 - 2 files changed, 3 deletions(-) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py index 7f77cdc16..61bc90661 100644 --- a/src/registrar/models/domain_group.py +++ b/src/registrar/models/domain_group.py @@ -1,7 +1,5 @@ from django.db import models from .utility.time_stamped_model import TimeStampedModel -from registrar.models.portfolio import Portfolio -from registrar.models.domain_information import DomainInformation class DomainGroup(TimeStampedModel): diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index c731c64b3..c96ea19f3 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -1,6 +1,5 @@ from django.db import models from .utility.time_stamped_model import TimeStampedModel -from registrar.models.portfolio import Portfolio class Suborganization(TimeStampedModel): From baee980b47e1b578ad7a1a554296ffca168b1faf Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 20 Jun 2024 17:48:00 -0400 Subject: [PATCH 27/45] cleaned up a comment --- src/registrar/registrar_middleware.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index be787aca9..e0ebb4dc5 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -102,7 +102,6 @@ class CheckUserProfileMiddleware: custom_redirect = "domain-request:" if request.path == "/request/" else None # Don't redirect on excluded pages (such as the setup page itself) - # if not any(request.path.startswith(page) for page in excluded_pages): if not any(request.path.startswith(page) for page in self._get_excluded_pages(profile_page)): # Preserve the original query parameters, and coerce them into a dict From f0756683add2878bd75eaeb467f4b6ac29ab3f3d Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:06:11 -0700 Subject: [PATCH 28/45] Create new user groups migration for DomainGroup and Suborg --- .../migrations/0106_create_groups_v14.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/registrar/migrations/0106_create_groups_v14.py diff --git a/src/registrar/migrations/0106_create_groups_v14.py b/src/registrar/migrations/0106_create_groups_v14.py new file mode 100644 index 000000000..0ce3bafa5 --- /dev/null +++ b/src/registrar/migrations/0106_create_groups_v14.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0079 (which populates federal agencies) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0103_portfolio_domaininformation_portfolio_and_more"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] From 4ac9e9d4f3293a45628e3f906345c523f6d92f6b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 21 Jun 2024 10:06:31 -0600 Subject: [PATCH 29/45] Remove os --- .../management/commands/email_current_metadata_report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/management/commands/email_current_metadata_report.py b/src/registrar/management/commands/email_current_metadata_report.py index b76e77608..0255cc178 100644 --- a/src/registrar/management/commands/email_current_metadata_report.py +++ b/src/registrar/management/commands/email_current_metadata_report.py @@ -1,7 +1,6 @@ """Generates current-metadata.csv then uploads to S3 + sends email""" import logging -import os import pyzipper from datetime import datetime From c220a8a9ac74679d6f73803cba834207ae845243 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:10:19 -0700 Subject: [PATCH 30/45] Correct usergroup migration dependency --- src/registrar/migrations/0106_create_groups_v14.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/migrations/0106_create_groups_v14.py b/src/registrar/migrations/0106_create_groups_v14.py index 0ce3bafa5..816a49ac8 100644 --- a/src/registrar/migrations/0106_create_groups_v14.py +++ b/src/registrar/migrations/0106_create_groups_v14.py @@ -25,7 +25,7 @@ def create_groups(apps, schema_editor) -> Any: class Migration(migrations.Migration): dependencies = [ - ("registrar", "0103_portfolio_domaininformation_portfolio_and_more"), + ("registrar", "0105_suborganization_domaingroup"), ] operations = [ From 5acaac7c6c7fe562731c733f50bc57d7b97753a9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 21 Jun 2024 10:19:00 -0600 Subject: [PATCH 31/45] Add some docs --- docs/operations/data_migration.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/operations/data_migration.md b/docs/operations/data_migration.md index 472362a79..17aa9c606 100644 --- a/docs/operations/data_migration.md +++ b/docs/operations/data_migration.md @@ -697,3 +697,31 @@ Example: `cf ssh getgov-za` | | Parameter | Description | |:-:|:-------------------------- |:----------------------------------------------------------------------------| | 1 | **debug** | Increases logging detail. Defaults to False. | + +## Email current metadata report + +### Running on sandboxes + +#### Step 1: Login to CloudFoundry +```cf login -a api.fr.cloud.gov --sso``` + +#### Step 2: SSH into your environment +```cf ssh getgov-{space}``` + +Example: `cf ssh getgov-za` + +#### Step 3: Create a shell instance +```/tmp/lifecycle/shell``` + +#### Step 4: Running the script +```./manage.py email_current_metadata_report --emailTo {desired email address}``` + +### Running locally + +#### Step 1: Running the script +```docker-compose exec app ./manage.py email_current_metadata_report --emailTo {desired email address}``` + +##### Parameters +| | Parameter | Description | +|:-:|:-------------------------- |:-----------------------------------------------------------------------------------| +| 1 | **emailTo** | Specifies where the email will be emailed. Defaults to help@get.gov on production. | \ No newline at end of file From 53991c127c2892dc2565bbb03ab69f20194055b3 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:39:12 -0700 Subject: [PATCH 32/45] Remove blank/null option and add max length to Suborg --- src/registrar/models/suborganization.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index c96ea19f3..b1e010953 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -8,9 +8,8 @@ class Suborganization(TimeStampedModel): """ name = models.CharField( - null=True, - blank=True, unique=True, + max_length=1000, help_text="Suborganization", ) From 5712596d19e6a115d1955f4c6a261c9628931ac8 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:06:25 -0700 Subject: [PATCH 33/45] Remove blank and null options from Domain Group --- src/registrar/models/domain_group.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py index 61bc90661..8699c0337 100644 --- a/src/registrar/models/domain_group.py +++ b/src/registrar/models/domain_group.py @@ -11,8 +11,6 @@ class DomainGroup(TimeStampedModel): Organized group of domains. """ name = models.CharField( - null=True, - blank=True, unique=True, help_text="Domain group", ) From f60b79e13baac166241d6e26a4b856fd15a2d0a5 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:15:55 -0700 Subject: [PATCH 34/45] Readd migrations --- .../migrations/0105_suborganization_domaingroup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/migrations/0105_suborganization_domaingroup.py b/src/registrar/migrations/0105_suborganization_domaingroup.py index 95d16e3b4..471f9379c 100644 --- a/src/registrar/migrations/0105_suborganization_domaingroup.py +++ b/src/registrar/migrations/0105_suborganization_domaingroup.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-06-20 21:21 +# Generated by Django 4.2.10 on 2024-06-21 18:15 from django.db import migrations, models import django.db.models.deletion @@ -17,7 +17,7 @@ class Migration(migrations.Migration): ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), - ("name", models.CharField(blank=True, help_text="Suborganization", null=True, unique=True)), + ("name", models.CharField(help_text="Suborganization", max_length=1000, unique=True)), ("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")), ], options={ @@ -30,7 +30,7 @@ class Migration(migrations.Migration): ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), - ("name", models.CharField(blank=True, help_text="Domain group", null=True, unique=True)), + ("name", models.CharField(help_text="Domain group", unique=True)), ("domains", models.ManyToManyField(blank=True, to="registrar.domaininformation")), ("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")), ], From 60bbf5f0fc0d2885df75154d8d899f11ff9a5e87 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:44:09 -0700 Subject: [PATCH 35/45] Reorder description comment for domain group --- src/registrar/models/domain_group.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/domain_group.py b/src/registrar/models/domain_group.py index 8699c0337..16b1fd203 100644 --- a/src/registrar/models/domain_group.py +++ b/src/registrar/models/domain_group.py @@ -3,13 +3,13 @@ from .utility.time_stamped_model import TimeStampedModel class DomainGroup(TimeStampedModel): + """ + Organized group of domains. + """ class Meta: unique_together = [("name", "portfolio")] - """ - Organized group of domains. - """ name = models.CharField( unique=True, help_text="Domain group", From 69b81ba6d6803583024b282b872891f16ca3edde Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Mon, 24 Jun 2024 10:28:28 -0700 Subject: [PATCH 36/45] Remove import/export resource for domain group and suborg admin classes --- src/registrar/admin.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index b1dcb1f6e..686545f77 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2656,29 +2656,11 @@ class WaffleFlagAdmin(FlagAdmin): fields = "__all__" -class DomainGroupResource(resources.ModelResource): - """defines how each field in the referenced model should be mapped to the corresponding fields in the - import/export file""" - - class Meta: - model = models.DomainGroup - - class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin): - resource_classes = [DomainGroupResource] list_display = ["name", "portfolio"] -class SuborganizationResource(resources.ModelResource): - """defines how each field in the referenced model should be mapped to the corresponding fields in the - import/export file""" - - class Meta: - model = models.Suborganization - - class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin): - resource_classes = [SuborganizationResource] list_display = ["name", "portfolio"] From 3769113da3343865e7c1ae9693749c53180495bc Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:44:36 -0600 Subject: [PATCH 37/45] Fix bug --- .../management/commands/email_current_metadata_report.py | 4 ++-- 1 file changed, 2 insertions(+), 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 0255cc178..595d39215 100644 --- a/src/registrar/management/commands/email_current_metadata_report.py +++ b/src/registrar/management/commands/email_current_metadata_report.py @@ -93,8 +93,8 @@ class Command(BaseCommand): f_out.setpassword(str.encode(password)) for report_name, report in reports.items(): logger.info(f"Generating {report_name}") - report = self.write_and_return_report(report["report_function"]) - f_out.writestr(report["report_filename"], report) + report_content = self.write_and_return_report(report["report_function"]) + f_out.writestr(report["report_filename"], report_content) # Get the final report for emailing purposes with open(zip_filename, "rb") as file_data: From aa1958c1432e4659443458294b805efeb9b66be2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:11:08 -0600 Subject: [PATCH 38/45] Comment out password override for local testing --- .../management/commands/email_current_metadata_report.py | 6 ++++-- 1 file changed, 4 insertions(+), 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 595d39215..82b83ea9b 100644 --- a/src/registrar/management/commands/email_current_metadata_report.py +++ b/src/registrar/management/commands/email_current_metadata_report.py @@ -68,8 +68,10 @@ class Command(BaseCommand): # Set the password equal to our content in SECRET_ENCRYPT_METADATA. # For local development, this will be "devpwd" unless otherwise set. - override = settings.SECRET_ENCRYPT_METADATA is None and not settings.IS_PRODUCTION - password = "devpwd" if override else settings.SECRET_ENCRYPT_METADATA + # Uncomment these lines if you want to use this: + # override = settings.SECRET_ENCRYPT_METADATA is None and not settings.IS_PRODUCTION + # password = "devpwd" if override else settings.SECRET_ENCRYPT_METADATA + password = settings.SECRET_ENCRYPT_METADATA encrypted_zip_in_bytes = self.get_encrypted_zip(zip_filename, reports, password) From 5fe084cdabd1b98221192175b29d53f31fef6f30 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:18:37 -0600 Subject: [PATCH 39/45] Add err handling --- .../management/commands/email_current_metadata_report.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/registrar/management/commands/email_current_metadata_report.py b/src/registrar/management/commands/email_current_metadata_report.py index 82b83ea9b..773199198 100644 --- a/src/registrar/management/commands/email_current_metadata_report.py +++ b/src/registrar/management/commands/email_current_metadata_report.py @@ -72,6 +72,10 @@ class Command(BaseCommand): # override = settings.SECRET_ENCRYPT_METADATA is None and not settings.IS_PRODUCTION # password = "devpwd" if override else settings.SECRET_ENCRYPT_METADATA password = settings.SECRET_ENCRYPT_METADATA + if not password: + raise ValueError( + "No password was specified for this zip file." + ) encrypted_zip_in_bytes = self.get_encrypted_zip(zip_filename, reports, password) From d5cd639b854552f8fba3d5201764c4795aa03a25 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:21:42 -0600 Subject: [PATCH 40/45] Lint --- .../management/commands/email_current_metadata_report.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/management/commands/email_current_metadata_report.py b/src/registrar/management/commands/email_current_metadata_report.py index 773199198..905e4a57a 100644 --- a/src/registrar/management/commands/email_current_metadata_report.py +++ b/src/registrar/management/commands/email_current_metadata_report.py @@ -73,9 +73,7 @@ class Command(BaseCommand): # password = "devpwd" if override else settings.SECRET_ENCRYPT_METADATA password = settings.SECRET_ENCRYPT_METADATA if not password: - raise ValueError( - "No password was specified for this zip file." - ) + raise ValueError("No password was specified for this zip file.") encrypted_zip_in_bytes = self.get_encrypted_zip(zip_filename, reports, password) From 51e6a925daaa513230e71e7f16e5aa20d8166714 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 24 Jun 2024 15:39:05 -0400 Subject: [PATCH 41/45] formatted for code readability --- src/registrar/registrar_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 1c2f63054..82ee9d0fc 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -121,7 +121,7 @@ class CheckUserProfileMiddleware: return HttpResponseRedirect(new_setup_page) else: # Process the view as normal - return None + return None class CheckPortfolioMiddleware: From 1c015b7aa1024622d0ad033bc0819346867d8944 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 24 Jun 2024 12:46:26 -0700 Subject: [PATCH 42/45] Update to have flag logic --- src/registrar/models/domain_request.py | 13 +++++++------ src/registrar/tests/test_models.py | 6 +++--- src/registrar/views/domain_request.py | 4 ++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 1c4725be1..49b84d472 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -17,6 +17,8 @@ from .utility.time_stamped_model import TimeStampedModel from ..utility.email import send_templated_email, EmailSendingError from itertools import chain +from waffle.decorators import flag_is_active + logger = logging.getLogger(__name__) @@ -1165,19 +1167,20 @@ class DomainRequest(TimeStampedModel): def _is_policy_acknowledgement_complete(self): return self.is_policy_acknowledged is not None - def _is_general_form_complete(self): + def _is_general_form_complete(self, request): + has_profile_feature_flag = flag_is_active(request, "profile_feature") return ( self._is_organization_name_and_address_complete() and self._is_authorizing_official_complete() and self._is_requested_domain_complete() and self._is_purpose_complete() - and self._is_submitter_complete() + and (self._is_submitter_complete() if not has_profile_feature_flag else True) and self._is_other_contacts_complete() and self._is_additional_details_complete() and self._is_policy_acknowledgement_complete() ) - def _form_complete(self): + def _form_complete(self, request): match self.generic_org_type: case DomainRequest.OrganizationChoices.FEDERAL: is_complete = self._is_federal_complete() @@ -1198,8 +1201,6 @@ class DomainRequest(TimeStampedModel): case _: # NOTE: Shouldn't happen, this is only if somehow they didn't choose an org type is_complete = False - - if not is_complete or not self._is_general_form_complete(): + if not is_complete or not self._is_general_form_complete(request): return False - return True diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 53af1674e..c3bb1fac9 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -2012,8 +2012,8 @@ class TestDomainRequestIncomplete(TestCase): self.domain_request.is_policy_acknowledged = None self.assertFalse(self.domain_request._is_policy_acknowledgement_complete()) - def test_form_complete(self): - self.assertTrue(self.domain_request._form_complete()) + def test_form_complete(self, request): + self.assertTrue(self.domain_request._form_complete(request)) self.domain_request.generic_org_type = None self.domain_request.save() - self.assertFalse(self.domain_request._form_complete()) + self.assertFalse(self.domain_request._form_complete(request)) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 77699e17a..73524dbe3 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -383,7 +383,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): has_profile_flag = flag_is_active(self.request, "profile_feature") context_stuff = {} - if DomainRequest._form_complete(self.domain_request): + if DomainRequest._form_complete(self.domain_request, self.request): modal_button = '" context_stuff = { "not_form": False, @@ -695,7 +695,7 @@ class Review(DomainRequestWizard): forms = [] # type: ignore def get_context_data(self): - if DomainRequest._form_complete(self.domain_request) is False: + if DomainRequest._form_complete(self.domain_request, self.request) is False: logger.warning("User arrived at review page with an incomplete form.") context = super().get_context_data() context["Step"] = Step.__members__ From 668611f13e1bc71f951ee97bbded0985f3ebad3f Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 24 Jun 2024 14:09:56 -0700 Subject: [PATCH 43/45] Update test --- src/registrar/tests/test_models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index c3bb1fac9..272996147 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -3,6 +3,8 @@ from django.db.utils import IntegrityError from unittest.mock import patch from django.contrib.auth import get_user_model +from django.test import RequestFactory + from registrar.models import ( Contact, DomainRequest, @@ -1610,6 +1612,7 @@ class TestDomainInformationCustomSave(TestCase): class TestDomainRequestIncomplete(TestCase): def setUp(self): super().setUp() + self.factory = RequestFactory() username = "test_user" first_name = "First" last_name = "Last" @@ -2012,7 +2015,10 @@ class TestDomainRequestIncomplete(TestCase): self.domain_request.is_policy_acknowledged = None self.assertFalse(self.domain_request._is_policy_acknowledgement_complete()) - def test_form_complete(self, request): + def test_form_complete(self): + request = self.factory.get("/") + request.user = self.user + self.assertTrue(self.domain_request._form_complete(request)) self.domain_request.generic_org_type = None self.domain_request.save() From b8bc2900cce9c04dfe48de399411d440f1331e25 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 24 Jun 2024 14:24:33 -0700 Subject: [PATCH 44/45] Add comment --- src/registrar/models/domain_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 49b84d472..63f11c8d0 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1174,6 +1174,7 @@ class DomainRequest(TimeStampedModel): and self._is_authorizing_official_complete() and self._is_requested_domain_complete() and self._is_purpose_complete() + # NOTE: This flag leaves submitter as empty (request wont submt) hence preset to True and (self._is_submitter_complete() if not has_profile_feature_flag else True) and self._is_other_contacts_complete() and self._is_additional_details_complete() From 4e3b7a49bc02c6ca0d1b763038a629f4fe2cbd1a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 24 Jun 2024 14:25:21 -0700 Subject: [PATCH 45/45] Spelling error fix --- src/registrar/models/domain_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 63f11c8d0..25b696c2a 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1174,7 +1174,7 @@ class DomainRequest(TimeStampedModel): and self._is_authorizing_official_complete() and self._is_requested_domain_complete() and self._is_purpose_complete() - # NOTE: This flag leaves submitter as empty (request wont submt) hence preset to True + # NOTE: This flag leaves submitter as empty (request wont submit) hence preset to True and (self._is_submitter_complete() if not has_profile_feature_flag else True) and self._is_other_contacts_complete() and self._is_additional_details_complete()