From d94420d0ba0fdd96c27b2824ef2263941ddf2563 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jul 2023 22:55:52 +0000 Subject: [PATCH 001/164] Bump django from 4.2.1 to 4.2.3 in /src Bumps [django](https://github.com/django/django) from 4.2.1 to 4.2.3. - [Commits](https://github.com/django/django/compare/4.2.1...4.2.3) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- src/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requirements.txt b/src/requirements.txt index a7a1cdddc..644e75a74 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -11,7 +11,7 @@ cryptography==41.0.1 ; python_version >= '3.7' defusedxml==0.7.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' dj-database-url==2.0.0 dj-email-url==1.0.6 -django==4.2.1 +django==4.2.3 django-allow-cidr==0.6.0 django-auditlog==2.3.0 django-cache-url==3.4.4 From 52dc04c1acebfda230ba321a43b41e3b91caf41d Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Fri, 7 Jul 2023 11:01:15 -0400 Subject: [PATCH 002/164] Expose users in django admin for CISA Analysts while hiding uuids --- docs/django-admin/roles.md | 3 +- src/registrar/admin.py | 21 +++++++- src/registrar/fixtures.py | 1 + src/registrar/tests/common.py | 44 ++++++++++++----- src/registrar/tests/test_admin.py | 80 +++++++++++++++++++++++-------- 5 files changed, 112 insertions(+), 37 deletions(-) diff --git a/docs/django-admin/roles.md b/docs/django-admin/roles.md index 431380300..ab4867184 100644 --- a/docs/django-admin/roles.md +++ b/docs/django-admin/roles.md @@ -17,4 +17,5 @@ Staff auditlog | log entry | can view log entry registrar | contact | can view contact registrar | domain application | can change domain application -registrar | domain | can view domain \ No newline at end of file +registrar | domain | can view domain +registrar | user | can view user \ No newline at end of file diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 7a3647582..784da0564 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1,6 +1,6 @@ import logging from django.contrib import admin, messages -from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect from django.urls import reverse @@ -93,12 +93,29 @@ class UserContactInline(admin.StackedInline): model = models.Contact -class MyUserAdmin(UserAdmin): +class MyUserAdmin(BaseUserAdmin): """Custom user admin class to use our inlines.""" inlines = [UserContactInline] + def get_list_display(self, request): + if not request.user.is_superuser: + # Customize the list display for staff users + return ("email", "first_name", "last_name", "is_staff", "is_superuser") + else: + # Use the default list display for non-staff users + return super().get_list_display(request) + + def get_fieldsets(self, request, obj=None): + if not request.user.is_superuser: + # If the user doesn't have permission to change the model, + # show a read-only fieldset + return ((None, {"fields": []}),) + + # If the user has permission to change the model, show all fields + return super().get_fieldsets(request, obj) + class HostIPInline(admin.StackedInline): diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index b47ed4aef..35a72ab15 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -85,6 +85,7 @@ class UserFixture: "permissions": ["change_domainapplication"], }, {"app_label": "registrar", "model": "domain", "permissions": ["view_domain"]}, + {"app_label": "registrar", "model": "user", "permissions": ["view_user"]}, ] @classmethod diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index c89f36563..a613ba648 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -88,6 +88,37 @@ class MockSESClient(Mock): self.EMAILS_SENT.append({"args": args, "kwargs": kwargs}) +def mock_user(): + """A simple user.""" + user_kwargs = dict( + id=4, + first_name="Rachid", + last_name="Mrad", + ) + mock_user, _ = User.objects.get_or_create(**user_kwargs) + return mock_user + + +def create_superuser(self): + User = get_user_model() + p = "adminpass" + return User.objects.create_superuser( + username="superuser", + email="admin@example.com", + password=p, + ) + + +def create_user(self): + User = get_user_model() + p = "userpass" + return User.objects.create_user( + username="staffuser", + email="user@example.com", + password=p, + ) + + def completed_application( has_other_contacts=True, has_current_website=True, @@ -157,16 +188,3 @@ def completed_application( application.alternative_domains.add(alt) return application - - -def mock_user(): - """A simple user.""" - user_kwargs = dict( - id=4, - first_name="Rachid", - last_name="Mrad", - ) - - user, _ = User.objects.get_or_create(**user_kwargs) - - return user diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index d5396a829..0b3b6bc1e 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1,8 +1,8 @@ from django.test import TestCase, RequestFactory, Client from django.contrib.admin.sites import AdminSite -from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin +from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin, MyUserAdmin from registrar.models import DomainApplication, DomainInformation, User -from .common import completed_application, mock_user +from .common import completed_application, mock_user, create_superuser, create_user from django.contrib.auth import get_user_model from django.conf import settings @@ -14,21 +14,9 @@ class TestDomainApplicationAdmin(TestCase): def setUp(self): self.site = AdminSite() self.factory = RequestFactory() - self.admin = ListHeaderAdmin(model=DomainApplication, admin_site=None) - self.client = Client(HTTP_HOST="localhost:8080") - username = "admin" - first_name = "First" - last_name = "Last" - email = "info@example.com" - p = "adminpassword" - User = get_user_model() - self.superuser = User.objects.create_superuser( - username=username, - first_name=first_name, - last_name=last_name, - email=email, - password=p, - ) + # self.admin = ListHeaderAdmin(model=DomainApplication, admin_site=None) + # self.client = Client(HTTP_HOST="localhost:8080") + # self.superuser = create_superuser(self) @boto3_mocking.patching def test_save_model_sends_submitted_email(self): @@ -179,10 +167,23 @@ class TestDomainApplicationAdmin(TestCase): DomainInformation.objects.get(id=application.pk).delete() application.delete() + def tearDown(self): + DomainApplication.objects.all().delete() + User.objects.all().delete() + + +class ListHeaderAdminTest(TestCase): + def setUp(self): + self.site = AdminSite() + self.factory = RequestFactory() + self.admin = ListHeaderAdmin(model=DomainApplication, admin_site=None) + self.client = Client(HTTP_HOST="localhost:8080") + self.superuser = create_superuser(self) + def test_changelist_view(self): # Have to get creative to get past linter - p = "adminpassword" - self.client.login(username="admin", password=p) + p = "adminpass" + self.client.login(username="superuser", password=p) # Mock a user user = mock_user() @@ -240,7 +241,44 @@ class TestDomainApplicationAdmin(TestCase): ) def tearDown(self): - # delete any applications too - DomainApplication.objects.all().delete() User.objects.all().delete() self.superuser.delete() + + +class MyUserAdminTest(TestCase): + def setUp(self): + admin_site = AdminSite() + self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site) + + def test_list_display_without_username(self): + request = self.client.request().wsgi_request + request.user = create_user(self) + + list_display = self.admin.get_list_display(request) + expected_list_display = ( + "email", + "first_name", + "last_name", + "is_staff", + "is_superuser", + ) + + self.assertEqual(list_display, expected_list_display) + self.assertNotIn("username", list_display) + + def test_get_fieldsets_superuser(self): + request = self.client.request().wsgi_request + request.user = create_superuser(self) + fieldsets = self.admin.get_fieldsets(request) + expected_fieldsets = super(MyUserAdmin, self.admin).get_fieldsets(request) + self.assertEqual(fieldsets, expected_fieldsets) + + def test_get_fieldsets_non_superuser(self): + request = self.client.request().wsgi_request + request.user = create_user(self) + fieldsets = self.admin.get_fieldsets(request) + expected_fieldsets = ((None, {"fields": []}),) + self.assertEqual(fieldsets, expected_fieldsets) + + def tearDown(self): + User.objects.all().delete() From a92ca84ccb7520a86897b40022231e2578942257 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Fri, 7 Jul 2023 11:09:15 -0400 Subject: [PATCH 003/164] cleanup --- src/registrar/tests/test_admin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 0b3b6bc1e..cd499e306 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -14,9 +14,6 @@ class TestDomainApplicationAdmin(TestCase): def setUp(self): self.site = AdminSite() self.factory = RequestFactory() - # self.admin = ListHeaderAdmin(model=DomainApplication, admin_site=None) - # self.client = Client(HTTP_HOST="localhost:8080") - # self.superuser = create_superuser(self) @boto3_mocking.patching def test_save_model_sends_submitted_email(self): From 5763bb4a6a20ffc6c2b023773dbc6bd88904aa80 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 11 Jul 2023 11:45:55 -0400 Subject: [PATCH 004/164] Action needed status and email --- src/registrar/admin.py | 2 + src/registrar/models/domain_application.py | 18 +++++++- .../templates/application_status.html | 1 + .../emails/status_change_action_needed.txt | 43 ++++++++++++++++++ .../status_change_action_needed_subject.txt | 1 + src/registrar/templates/home.html | 2 +- src/registrar/tests/test_models.py | 45 +++++++++++++++++++ 7 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 src/registrar/templates/emails/status_change_action_needed.txt create mode 100644 src/registrar/templates/emails/status_change_action_needed_subject.txt diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 7a3647582..64839da31 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -265,6 +265,8 @@ class DomainApplicationAdmin(ListHeaderAdmin): original_obj.submit(updated_domain_application=obj) elif obj.status == models.DomainApplication.INVESTIGATING: original_obj.in_review(updated_domain_application=obj) + elif obj.status == models.DomainApplication.ACTION_NEEDED: + original_obj.action_needed(updated_domain_application=obj) elif obj.status == models.DomainApplication.APPROVED: original_obj.approve(updated_domain_application=obj) elif obj.status == models.DomainApplication.WITHDRAWN: diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index a50cb34b8..76e227447 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -18,16 +18,18 @@ class DomainApplication(TimeStampedModel): """A registrant's application for a new domain.""" - # #### Contants for choice fields #### + # #### Constants for choice fields #### STARTED = "started" SUBMITTED = "submitted" INVESTIGATING = "investigating" + ACTION_NEEDED = "action needed" APPROVED = "approved" WITHDRAWN = "withdrawn" STATUS_CHOICES = [ (STARTED, STARTED), (SUBMITTED, SUBMITTED), (INVESTIGATING, INVESTIGATING), + (ACTION_NEEDED, ACTION_NEEDED), (APPROVED, APPROVED), (WITHDRAWN, WITHDRAWN), ] @@ -497,7 +499,7 @@ class DomainApplication(TimeStampedModel): except EmailSendingError: logger.warning("Failed to send confirmation email", exc_info=True) - @transition(field="status", source=[STARTED, WITHDRAWN], target=SUBMITTED) + @transition(field="status", source=[STARTED, ACTION_NEEDED, WITHDRAWN], target=SUBMITTED) def submit(self, updated_domain_application=None): """Submit an application that is started. @@ -554,6 +556,18 @@ class DomainApplication(TimeStampedModel): "emails/status_change_in_review.txt", "emails/status_change_in_review_subject.txt", ) + + @transition(field="status", source=[INVESTIGATING], target=ACTION_NEEDED) + def action_needed(self, updated_domain_application): + """Send back an application that is under investigation or rejected. + + As a side effect, an email notification is sent, similar to in_review""" + + updated_domain_application._send_status_update_email( + "action needed", + "emails/status_change_action_needed.txt", + "emails/status_change_action_needed_subject.txt", + ) @transition(field="status", source=[SUBMITTED, INVESTIGATING], target=APPROVED) def approve(self, updated_domain_application=None): diff --git a/src/registrar/templates/application_status.html b/src/registrar/templates/application_status.html index 3d0f44a9f..c0904373f 100644 --- a/src/registrar/templates/application_status.html +++ b/src/registrar/templates/application_status.html @@ -21,6 +21,7 @@ {% if domainapplication.status == 'approved' %} Approved {% elif domainapplication.status == 'investigating' %} In Review + {% elif domainapplication.status == 'action needed' %} Action Needed {% elif domainapplication.status == 'submitted' %} Received {% else %}ERROR Please contact technical support/dev {% endif %} diff --git a/src/registrar/templates/emails/status_change_action_needed.txt b/src/registrar/templates/emails/status_change_action_needed.txt new file mode 100644 index 000000000..10bbc0087 --- /dev/null +++ b/src/registrar/templates/emails/status_change_action_needed.txt @@ -0,0 +1,43 @@ +{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} +Hi {{ application.submitter.first_name }}. + +ACTION NEEDED: Your .gov domain request requires your attention. A CISA analyst will get in touch with you shortly with more details. + +DOMAIN REQUESTED: {{ application.requested_domain.name }} +REQUEST RECEIVED ON: {{ application.updated_at|date }} +REQUEST #: {{ application.id }} +STATUS: Action needed + + +NEED TO MAKE CHANGES? + +If you need to change your request you have to first withdraw it. Once you +withdraw the request you can edit it and submit it again. Changing your request +might add to the wait time. Learn more about withdrawing your request. +. + + +NEXT STEPS + +- We’re reviewing your request. This usually takes 20 business days. + +- You can check the status of your request at any time. + + +- We’ll email you with questions or when we complete our review. + + +THANK YOU + +.Gov helps the public identify official, trusted information. Thank you for +requesting a .gov domain. + +---------------------------------------------------------------- + +{% include 'emails/includes/application_summary.txt' %} +---------------------------------------------------------------- + +The .gov team +Contact us: +Visit +{% endautoescape %} diff --git a/src/registrar/templates/emails/status_change_action_needed_subject.txt b/src/registrar/templates/emails/status_change_action_needed_subject.txt new file mode 100644 index 000000000..f505dd45e --- /dev/null +++ b/src/registrar/templates/emails/status_change_action_needed_subject.txt @@ -0,0 +1 @@ +ACTION NEEDED: Your .gov domain request requires your attention \ No newline at end of file diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index aa2034a27..6163a1ce0 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -88,7 +88,7 @@ {{ application.created_at|date }} {{ application.status|title }} - {% if application.status == "started" or application.status == "withdrawn" %} + {% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}
+ + + + {# .gov override #} + + + + + {% if show_changelinks %} + + + {% endif %} + + + {# end .gov override #} + + {% for model in app.models %} + + {% if model.admin_url %} + + {% else %} + + {% endif %} + + {% if model.add_url %} + + {% else %} + + {% endif %} + + {% if model.admin_url and show_changelinks %} + {% if model.view_only %} + + {% else %} + + {% endif %} + {% elif show_changelinks %} + + {% endif %} + + {% endfor %} +
+ {{ app.name }} +
ModelAdd + + {% translate 'View/Change' %}
{{ model.name }}{{ model.name }}{% translate 'Add' %}{% translate 'View' %}{% translate 'Change' %}
+
+ {% endfor %} +{% else %} +

{% translate 'You don’t have permission to view or edit anything.' %}

+{% endif %} \ No newline at end of file diff --git a/src/registrar/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html new file mode 100644 index 000000000..1887ea332 --- /dev/null +++ b/src/registrar/templates/admin/base_site.html @@ -0,0 +1,15 @@ +{% extends "admin/base.html" %} +{% load static %} + +{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} + +{% block extrastyle %}{{ block.super }}{% endblock %} + +{% block branding %} +

.gov admin

+{% if user.is_anonymous %} + {% include "admin/color_theme_toggle.html" %} +{% endif %} +{% endblock %} + +{% block nav-global %}{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/admin/change_list_results.html b/src/registrar/templates/admin/change_list_results.html new file mode 100644 index 000000000..1b8f40b1e --- /dev/null +++ b/src/registrar/templates/admin/change_list_results.html @@ -0,0 +1,90 @@ +{% load i18n static %} + +{% comment %} +.gov override +Load our custom filters to extract info from the django generated markup. +{% endcomment %} +{% load custom_filters %} + +{% if result_hidden_fields %} +
{# DIV for HTML validation #} +{% for item in result_hidden_fields %}{{ item }}{% endfor %} +
+{% endif %} +{% if results %} +
+ + + + +{# .gov - hardcode the select all checkbox #} + +{# .gov - don't let django generate the select all checkbox #} +{% for header in result_headers|slice:"1:" %} + +{% endfor %} + + + + + +{% comment %} +{% for result in results %} +{% if result.form.non_field_errors %} + +{% endif %} +{% for item in result %}{{ item }}{% endfor %} +{% endfor %} +{% endcomment %} + +{% comment %} +.gov - hardcode the row checkboxes using the custom filters to extract +the value attribute's value, and a label based on the anchor elements's +text. Then edit the for loop to keep django from generating the row select +checkboxes. +{% endcomment %} +{% for result in results %} + {% if result.form.non_field_errors %} + + {% endif %} + + + {% with result_value=result.0|extract_value %} + {% with result_label=result.1|extract_a_text %} + + {% endwith %} + {% endwith %} + + {% for item in result|slice:"1:" %} + {{ item }} + {% endfor %} + +{% endfor %} + + +
+
+ + + + +
+
+
+ {% if header.sortable %} + {% if header.sort_priority > 0 %} +
+ + {% if num_sorted_fields > 1 %}{{ header.sort_priority }}{% endif %} + +
+ {% endif %} + {% endif %} +
{% if header.sortable %}{{ header.text|capfirst }}{% else %}{{ header.text|capfirst }}{% endif %}
+
+
{{ result.form.non_field_errors }}
{{ result.form.non_field_errors }}
+ + +
+
+{% endif %} \ No newline at end of file diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py new file mode 100644 index 000000000..a22b15a28 --- /dev/null +++ b/src/registrar/templatetags/custom_filters.py @@ -0,0 +1,25 @@ +from django import template +import re + +register = template.Library() + + +@register.filter(name="extract_value") +def extract_value(html_input): + match = re.search(r'value="([^"]*)"', html_input) + if match: + return match.group(1) + return "" + + +@register.filter +def extract_a_text(value): + # Use regex to extract the text within the tag + pattern = r"]*>(.*?)" + match = re.search(pattern, value) + if match: + extracted_text = match.group(1) + else: + extracted_text = "" + + return extracted_text diff --git a/src/registrar/tests/test_templatetags.py b/src/registrar/tests/test_templatetags.py index 681d823b7..0d1eed4fc 100644 --- a/src/registrar/tests/test_templatetags.py +++ b/src/registrar/tests/test_templatetags.py @@ -29,3 +29,33 @@ class TestTemplateTags(TestCase): self.assertTrue(result.startswith(settings.GETGOV_PUBLIC_SITE_URL)) # slash-slash host slash directory slash page self.assertEqual(result.count("/"), 4) + + +class CustomFiltersTestCase(TestCase): + def test_extract_value_filter(self): + from registrar.templatetags.custom_filters import extract_value + + html_input = ( + '' + ) + result = extract_value(html_input) + self.assertEqual(result, "123") + + html_input = ( + '' + ) + result = extract_value(html_input) + self.assertEqual(result, "abc") + + def test_extract_a_text_filter(self): + from registrar.templatetags.custom_filters import extract_a_text + + input_text = 'Link Text' + result = extract_a_text(input_text) + self.assertEqual(result, "Link Text") + + input_text = 'Another Link' + result = extract_a_text(input_text) + self.assertEqual(result, "Another Link") From 04f4498f53fab32049b3b68db254b078e419cb6e Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Wed, 26 Jul 2023 16:09:43 -0400 Subject: [PATCH 027/164] Hide accessible checkbox labels --- src/registrar/templates/admin/change_list_results.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/admin/change_list_results.html b/src/registrar/templates/admin/change_list_results.html index 1b8f40b1e..d5e1587d0 100644 --- a/src/registrar/templates/admin/change_list_results.html +++ b/src/registrar/templates/admin/change_list_results.html @@ -22,7 +22,7 @@ Load our custom filters to extract info from the django generated markup.
- +
@@ -73,7 +73,7 @@ checkboxes. {% with result_label=result.1|extract_a_text %} - + {% endwith %} {% endwith %} From 09a46c586daabd0d956a0837d270472dd327bd68 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Thu, 27 Jul 2023 11:42:40 -0400 Subject: [PATCH 028/164] endline --- src/registrar/assets/sass/_theme/styles.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/sass/_theme/styles.scss index fc98416e8..27d844760 100644 --- a/src/registrar/assets/sass/_theme/styles.scss +++ b/src/registrar/assets/sass/_theme/styles.scss @@ -12,4 +12,4 @@ /*-------------------------------------------------- --- Admin ---------------------------------*/ -@forward "admin"; \ No newline at end of file +@forward "admin"; From a16d9a06d1da0c58e5c0114234c76abe9200c7a0 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Thu, 27 Jul 2023 12:28:55 -0400 Subject: [PATCH 029/164] add a note to ANDI documentation --- docs/developer/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/developer/README.md b/docs/developer/README.md index b5c993ad3..1f5960d75 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -172,8 +172,10 @@ We use the [ANDI](https://www.ssa.gov/accessibility/andi/help/install.html) brow from ssa.gov for accessibility testing outside the pipeline. ANDI will get blocked by our CSP settings, so you will need to install the -[Disable Content-Security-Policy extension](https://chrome.google.com/webstore/detail/disable-content-security/ieelmcmcagommplceebfedjlakkhpden) and activate it for the page you'd like -to test. +[Disable Content-Security-Policy extension](https://chrome.google.com/webstore/detail/disable-content-security/ieelmcmcagommplceebfedjlakkhpden) +and activate it for the page you'd like to test. + +Note - you will have to hot refresh on a given page after activating the extension. ### Accessibility Scanning From 58332edddd0b548fe710443266bcb4d3970c859b Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Thu, 27 Jul 2023 12:47:05 -0400 Subject: [PATCH 030/164] add a note to ANDI documentation --- docs/developer/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/developer/README.md b/docs/developer/README.md index 1f5960d75..5ac11a031 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -177,7 +177,6 @@ and activate it for the page you'd like to test. Note - you will have to hot refresh on a given page after activating the extension. - ### Accessibility Scanning The tool `pa11y-ci` is used to scan pages for compliance with a set of From 3df342ce1dbb68d955190da327c1adcc8cf116fd Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Thu, 27 Jul 2023 12:58:57 -0400 Subject: [PATCH 031/164] add a note to ANDI documentation --- docs/developer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/README.md b/docs/developer/README.md index 5ac11a031..d824ea820 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -175,7 +175,7 @@ ANDI will get blocked by our CSP settings, so you will need to install the [Disable Content-Security-Policy extension](https://chrome.google.com/webstore/detail/disable-content-security/ieelmcmcagommplceebfedjlakkhpden) and activate it for the page you'd like to test. -Note - you will have to hot refresh on a given page after activating the extension. +Note - refresh after enabling the extension on a page but before clicking ANDI. ### Accessibility Scanning From db4e3fcb8900f54f1d6580c16a5f39a5ee0c79bf Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 28 Jul 2023 06:47:56 -0700 Subject: [PATCH 032/164] removed logs and edited url to be used on delete --- src/registrar/views/application.py | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 06e701916..c321f1307 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -67,7 +67,7 @@ class ApplicationWizard(TemplateView): URL_NAMESPACE = "application" # name for accessing /application//edit EDIT_URL_NAME = "edit-application" - NEW_URL_NAME = "register" + NEW_URL_NAME = "/register/" # We need to pass our human-readable step titles as context to the templates. TITLES = { Step.ORGANIZATION_TYPE: _("Type of organization"), @@ -147,7 +147,6 @@ class ApplicationWizard(TemplateView): ) self.storage["application_id"] = self._application.id - logger.debug("applications id %s",str(self.storage["application_id"])) return self._application @property @@ -159,7 +158,6 @@ class ApplicationWizard(TemplateView): @storage.setter def storage(self, value): - logger.debug("requested session is %s", str(self.request.session)) self.request.session[self.prefix] = value self.request.session.modified = True @@ -199,18 +197,15 @@ class ApplicationWizard(TemplateView): def get(self, request, *args, **kwargs): """This method handles GET requests.""" - logger.debug("IN GET application") current_url = resolve(request.path_info).url_name - logger.debug("Current url is %s", current_url) + # if user visited via an "edit" url, associate the id of the # application they are trying to edit to this wizard instance # and remove any prior wizard data from their session if current_url == self.EDIT_URL_NAME and "id" in kwargs: - logger.debug("Storage was %s", self.storage) del self.storage self.storage["application_id"] = kwargs["id"] - logger.debug("storage is now %s", str(self.storage)) - logger.debug("id set to %s", kwargs["id"]) + # if accessing this class directly, redirect to the first step # in other words, if `ApplicationWizard` is called as view # directly by some redirect or url handler, we'll send users @@ -219,24 +214,16 @@ class ApplicationWizard(TemplateView): # send users "to the application wizard" without needing to # know which view is first in the list of steps. if self.__class__ == ApplicationWizard: - ##hmmm could we use this to redirect users if they shouldn't view editable data? - ## this is where it resets to send to first - #this is called when clicking an edit link AND when clicking new app - #not called when navigating between steps (such as clicking save and continue) - logger.debug("Current url is %s", current_url) - if current_url == self.NEW_URL_NAME:## find the right URL - logger.debug("In if check") - + + if request.path_info == self.NEW_URL_NAME: del self.storage self.storage["application_id"] = None #reset the app - logger.debug("calling go to first step") - #del self.storage + return self.goto(self.steps.first) self.steps.current = current_url context = self.get_context_data() context["forms"] = self.get_forms() - logger.debug("Context set to %s", str(context)) return render(request, self.template_name, context) def get_all_forms(self, **kwargs) -> list: @@ -329,7 +316,6 @@ class ApplicationWizard(TemplateView): """This method handles POST requests.""" # if accessing this class directly, redirect to the first step if self.__class__ == ApplicationWizard: - logger.debug("post redirect to first steps") return self.goto(self.steps.first) # which button did the user press? From 9c63383db72145c448ca42252e1f8bad4e1b8f29 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 28 Jul 2023 06:53:44 -0700 Subject: [PATCH 033/164] Removed unneeded code comments --- src/registrar/views/application.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index c321f1307..638bf650d 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -141,7 +141,6 @@ class ApplicationWizard(TemplateView): except DomainApplication.DoesNotExist: logger.debug("Application id %s did not have a DomainApplication" % id) - ##not being called for the new application when one self._application = DomainApplication.objects.create( creator=self.request.user, # type: ignore ) @@ -214,7 +213,7 @@ class ApplicationWizard(TemplateView): # send users "to the application wizard" without needing to # know which view is first in the list of steps. if self.__class__ == ApplicationWizard: - + #if starting a new application, clear the storage if request.path_info == self.NEW_URL_NAME: del self.storage self.storage["application_id"] = None #reset the app @@ -247,15 +246,14 @@ class ApplicationWizard(TemplateView): and from the database if `use_db` is True (provided that record exists). An empty form will be provided if neither of those are true. """ - ##bug is here an empty form is not provided if these are false!!! kwargs = { "files": files, "prefix": self.steps.current, "application": self.application, # this is a property, not an object - }##application here causing the issue? + } if step is None: - forms = self.forms ##what dis? + forms = self.forms else: url = reverse(f"{self.URL_NAMESPACE}:{step}") forms = resolve(url).func.view_class.forms From 8c34c919cc93489e4e94442c6959193e32041edf Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 28 Jul 2023 07:59:33 -0700 Subject: [PATCH 034/164] tests running locally for session --- src/registrar/tests/test_views.py | 45 +++++++++++++++++++++++++++++- src/registrar/views/application.py | 7 +++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 0b89a45a6..bc8763a08 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -130,6 +130,7 @@ class DomainApplicationTests(TestWithUser, WebTest): As we add additional form pages, we need to include them here to make this test work. """ + print("in form submission") num_pages_tested = 0 # elections, type_of_work, tribal_government, no_other_contacts SKIPPED_PAGES = 4 @@ -141,38 +142,55 @@ class DomainApplicationTests(TestWithUser, WebTest): # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + session_wizard=self.app.session['wizard_application'] + + try: + print(self.app.session) + print(self.app.session.items()) + except: + print("cant print session") + print(self.app) # ---- TYPE PAGE ---- type_form = type_page.form type_form["organization_type-organization_type"] = "federal" - + print("submitting form with federal") # test next button and validate data self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard type_result = type_page.form.submit() # should see results in db application = DomainApplication.objects.get() # there's only one + print("domain application is %s", str(application)) self.assertEqual(application.organization_type, "federal") # the post request should return a redirect to the next form in # the application + print("going to organization federal") self.assertEqual(type_result.status_code, 302) self.assertEqual(type_result["Location"], "/register/organization_federal/") num_pages_tested += 1 # ---- FEDERAL BRANCH PAGE ---- # Follow the redirect to the next form page + print("clicking exective") self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard + federal_page = type_result.follow() federal_form = federal_page.form federal_form["organization_federal-federal_type"] = "executive" # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard + federal_result = federal_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one self.assertEqual(application.federal_type, "executive") # the post request should return a redirect to the next form in # the application + print("clicking contact") self.assertEqual(federal_result.status_code, 302) self.assertEqual(federal_result["Location"], "/register/organization_contact/") num_pages_tested += 1 @@ -180,6 +198,8 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- ORG CONTACT PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard + org_contact_page = federal_result.follow() org_contact_form = org_contact_page.form # federal agency so we have to fill in federal_agency @@ -196,6 +216,8 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard + org_contact_result = org_contact_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -217,6 +239,8 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- AUTHORIZING OFFICIAL PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard + ao_page = org_contact_result.follow() ao_form = ao_page.form ao_form["authorizing_official-first_name"] = "Testy ATO" @@ -227,6 +251,7 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard ao_result = ao_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -244,12 +269,14 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- CURRENT SITES PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard current_sites_page = ao_result.follow() current_sites_form = current_sites_page.form current_sites_form["current_sites-0-website"] = "www.city.com" # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard current_sites_result = current_sites_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -266,12 +293,14 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- DOTGOV DOMAIN PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard dotgov_page = current_sites_result.follow() dotgov_form = dotgov_page.form dotgov_form["dotgov_domain-requested_domain"] = "city" dotgov_form["dotgov_domain-0-alternative_domain"] = "city1" self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard dotgov_result = dotgov_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -288,12 +317,14 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- PURPOSE PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard purpose_page = dotgov_result.follow() purpose_form = purpose_page.form purpose_form["purpose-purpose"] = "For all kinds of things." # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard purpose_result = purpose_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -307,6 +338,7 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- YOUR CONTACT INFO PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard your_contact_page = purpose_result.follow() your_contact_form = your_contact_page.form @@ -318,6 +350,7 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard your_contact_result = your_contact_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -335,6 +368,7 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- OTHER CONTACTS PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard other_contacts_page = your_contact_result.follow() other_contacts_form = other_contacts_page.form @@ -346,6 +380,7 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard other_contacts_result = other_contacts_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -375,6 +410,7 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard anything_else_result = anything_else_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -388,6 +424,7 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- REQUIREMENTS PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard requirements_page = anything_else_result.follow() requirements_form = requirements_page.form @@ -408,6 +445,7 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- REVIEW AND FINSIHED PAGES ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard review_page = requirements_result.follow() review_form = review_page.form @@ -444,6 +482,7 @@ class DomainApplicationTests(TestWithUser, WebTest): # final submission results in a redirect to the "finished" URL self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard with less_console_noise(): review_result = review_form.submit() @@ -454,6 +493,7 @@ class DomainApplicationTests(TestWithUser, WebTest): # following this redirect is a GET request, so include the cookie # here too. self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard with less_console_noise(): final_result = review_result.follow() self.assertContains(final_result, "Thanks for your domain request!") @@ -485,13 +525,16 @@ class DomainApplicationTests(TestWithUser, WebTest): # type_page = home_page.click("Edit") session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + session_wizard=self.app.session['wizard_application'] url = reverse("edit-application", kwargs={"id": application.pk}) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard # TODO: The following line results in a django error on middleware response = self.client.get(url, follow=True) self.assertContains(response, "Type of organization") self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + self.app.session["wizard_application"]=session_wizard # TODO: Step through the remaining pages self.assertEqual(num_pages, num_pages_tested) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 638bf650d..6045f9186 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -133,19 +133,22 @@ class ApplicationWizard(TemplateView): if self.has_pk(): id = self.storage["application_id"] try: + logger.debug("Trying to get the application") self._application = DomainApplication.objects.get( creator=self.request.user, # type: ignore pk=id, ) + logger.debug("applicaiton is %s",self._application) return self._application except DomainApplication.DoesNotExist: logger.debug("Application id %s did not have a DomainApplication" % id) - + logger.debug("creating application with next id") self._application = DomainApplication.objects.create( creator=self.request.user, # type: ignore ) self.storage["application_id"] = self._application.id + logger.debug("setting id to %s",self._application.id) return self._application @property @@ -216,7 +219,7 @@ class ApplicationWizard(TemplateView): #if starting a new application, clear the storage if request.path_info == self.NEW_URL_NAME: del self.storage - self.storage["application_id"] = None #reset the app + # self.storage["application_id"] = None #reset the app return self.goto(self.steps.first) From 0f1958110f7a1fd63b1ef07438ee2c696dc3afae Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 28 Jul 2023 08:13:50 -0700 Subject: [PATCH 035/164] removed prints and ran linter --- src/registrar/tests/test_views.py | 41 ++---------------------------- src/registrar/views/application.py | 8 ++---- 2 files changed, 4 insertions(+), 45 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index bc8763a08..56bb8ca51 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -130,7 +130,6 @@ class DomainApplicationTests(TestWithUser, WebTest): As we add additional form pages, we need to include them here to make this test work. """ - print("in form submission") num_pages_tested = 0 # elections, type_of_work, tribal_government, no_other_contacts SKIPPED_PAGES = 4 @@ -142,39 +141,26 @@ class DomainApplicationTests(TestWithUser, WebTest): # of a "session". We are going to do it manually, saving the session ID here # and then setting the cookie on each request. session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - session_wizard=self.app.session['wizard_application'] - - try: - print(self.app.session) - print(self.app.session.items()) - except: - print("cant print session") - print(self.app) # ---- TYPE PAGE ---- type_form = type_page.form type_form["organization_type-organization_type"] = "federal" - print("submitting form with federal") # test next button and validate data self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard type_result = type_page.form.submit() # should see results in db application = DomainApplication.objects.get() # there's only one - print("domain application is %s", str(application)) + self.assertEqual(application.organization_type, "federal") # the post request should return a redirect to the next form in # the application - print("going to organization federal") self.assertEqual(type_result.status_code, 302) self.assertEqual(type_result["Location"], "/register/organization_federal/") num_pages_tested += 1 # ---- FEDERAL BRANCH PAGE ---- # Follow the redirect to the next form page - print("clicking exective") self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard federal_page = type_result.follow() federal_form = federal_page.form @@ -182,7 +168,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard federal_result = federal_form.submit() # validate that data from this step are being saved @@ -190,7 +175,6 @@ class DomainApplicationTests(TestWithUser, WebTest): self.assertEqual(application.federal_type, "executive") # the post request should return a redirect to the next form in # the application - print("clicking contact") self.assertEqual(federal_result.status_code, 302) self.assertEqual(federal_result["Location"], "/register/organization_contact/") num_pages_tested += 1 @@ -198,7 +182,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- ORG CONTACT PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard org_contact_page = federal_result.follow() org_contact_form = org_contact_page.form @@ -216,7 +199,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard org_contact_result = org_contact_form.submit() # validate that data from this step are being saved @@ -239,7 +221,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- AUTHORIZING OFFICIAL PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard ao_page = org_contact_result.follow() ao_form = ao_page.form @@ -251,7 +232,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard ao_result = ao_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -269,14 +249,12 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- CURRENT SITES PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard current_sites_page = ao_result.follow() current_sites_form = current_sites_page.form current_sites_form["current_sites-0-website"] = "www.city.com" # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard current_sites_result = current_sites_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -293,14 +271,12 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- DOTGOV DOMAIN PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard dotgov_page = current_sites_result.follow() dotgov_form = dotgov_page.form dotgov_form["dotgov_domain-requested_domain"] = "city" dotgov_form["dotgov_domain-0-alternative_domain"] = "city1" self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard dotgov_result = dotgov_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -317,14 +293,12 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- PURPOSE PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard purpose_page = dotgov_result.follow() purpose_form = purpose_page.form purpose_form["purpose-purpose"] = "For all kinds of things." # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard purpose_result = purpose_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -338,7 +312,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- YOUR CONTACT INFO PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard your_contact_page = purpose_result.follow() your_contact_form = your_contact_page.form @@ -350,7 +323,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard your_contact_result = your_contact_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -368,7 +340,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- OTHER CONTACTS PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard other_contacts_page = your_contact_result.follow() other_contacts_form = other_contacts_page.form @@ -380,7 +351,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard other_contacts_result = other_contacts_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -410,7 +380,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard anything_else_result = anything_else_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -424,7 +393,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- REQUIREMENTS PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard requirements_page = anything_else_result.follow() requirements_form = requirements_page.form @@ -445,7 +413,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- REVIEW AND FINSIHED PAGES ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard review_page = requirements_result.follow() review_form = review_page.form @@ -482,7 +449,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # final submission results in a redirect to the "finished" URL self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard with less_console_noise(): review_result = review_form.submit() @@ -493,7 +459,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # following this redirect is a GET request, so include the cookie # here too. self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard with less_console_noise(): final_result = review_result.follow() self.assertContains(final_result, "Thanks for your domain request!") @@ -525,16 +490,14 @@ class DomainApplicationTests(TestWithUser, WebTest): # type_page = home_page.click("Edit") session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - session_wizard=self.app.session['wizard_application'] + session_wizard = self.app.session["wizard_application"] url = reverse("edit-application", kwargs={"id": application.pk}) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard # TODO: The following line results in a django error on middleware response = self.client.get(url, follow=True) self.assertContains(response, "Type of organization") self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - self.app.session["wizard_application"]=session_wizard # TODO: Step through the remaining pages self.assertEqual(num_pages, num_pages_tested) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 6045f9186..256f4be40 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -133,22 +133,19 @@ class ApplicationWizard(TemplateView): if self.has_pk(): id = self.storage["application_id"] try: - logger.debug("Trying to get the application") self._application = DomainApplication.objects.get( creator=self.request.user, # type: ignore pk=id, ) - logger.debug("applicaiton is %s",self._application) return self._application except DomainApplication.DoesNotExist: logger.debug("Application id %s did not have a DomainApplication" % id) - logger.debug("creating application with next id") + self._application = DomainApplication.objects.create( creator=self.request.user, # type: ignore ) self.storage["application_id"] = self._application.id - logger.debug("setting id to %s",self._application.id) return self._application @property @@ -216,10 +213,9 @@ class ApplicationWizard(TemplateView): # send users "to the application wizard" without needing to # know which view is first in the list of steps. if self.__class__ == ApplicationWizard: - #if starting a new application, clear the storage + # if starting a new application, clear the storage if request.path_info == self.NEW_URL_NAME: del self.storage - # self.storage["application_id"] = None #reset the app return self.goto(self.steps.first) From ca47d6eb2c1543d9ab8335ff874fb35169def4ea Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 28 Jul 2023 08:47:42 -0700 Subject: [PATCH 036/164] fixed linter issue with unused variable --- src/registrar/tests/test_views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 56bb8ca51..5f8117af5 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -490,7 +490,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # type_page = home_page.click("Edit") session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - session_wizard = self.app.session["wizard_application"] url = reverse("edit-application", kwargs={"id": application.pk}) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) From 9e25baa34655f1ae33d9585a095eed284f3b6cb1 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 28 Jul 2023 09:04:54 -0700 Subject: [PATCH 037/164] removed uneeded endlines --- src/registrar/tests/test_views.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 5f8117af5..feb553bf7 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -150,7 +150,6 @@ class DomainApplicationTests(TestWithUser, WebTest): type_result = type_page.form.submit() # should see results in db application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.organization_type, "federal") # the post request should return a redirect to the next form in # the application @@ -168,7 +167,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_result = federal_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -182,7 +180,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- ORG CONTACT PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_page = federal_result.follow() org_contact_form = org_contact_page.form # federal agency so we have to fill in federal_agency @@ -199,7 +196,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_result = org_contact_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one @@ -221,7 +217,6 @@ class DomainApplicationTests(TestWithUser, WebTest): # ---- AUTHORIZING OFFICIAL PAGE ---- # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page = org_contact_result.follow() ao_form = ao_page.form ao_form["authorizing_official-first_name"] = "Testy ATO" From 8de873ad015fdf3cc769736e175509645043303f Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 1 Aug 2023 15:11:17 -0400 Subject: [PATCH 038/164] caniuse package update --- src/package-lock.json | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index dc1464ee8..87e43f28c 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1072,9 +1072,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001450", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz", - "integrity": "sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew==", + "version": "1.0.30001517", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz", + "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==", "dev": true, "funding": [ { @@ -1084,6 +1084,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -7790,9 +7794,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001450", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz", - "integrity": "sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew==", + "version": "1.0.30001517", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz", + "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==", "dev": true }, "check-types": { From 9562f7c7bfe4e6ce0e5658fea863147fcd4bed9d Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 1 Aug 2023 15:12:23 -0400 Subject: [PATCH 039/164] django admin CSS theming --- src/registrar/assets/sass/_theme/_admin.scss | 91 ++++++++++++++++++++ src/registrar/templates/admin/base_site.html | 4 +- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index afba7f44f..78fe8e109 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -1,3 +1,94 @@ +@use "cisa_colors" as *; +@use "uswds-core" as *; + +// We'll use Django's CSS vars: https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#theming-support +// and assign Uswds theme vars whenever possible +// If needed (see below), we'll use the Uswds hex value +// As a last resort, we'll use CISA colors to supplement the palette +:root { + --primary: #{$theme-color-primary}; + --secondary: #{$theme-color-primary-darkest}; + --accent: #{$theme-color-accent-cool}; + // --primary-fg: #fff; + + // Uswds theme vars that are set to a token, such as #{$theme-color-base-darker} + // would interpolate to 'gray-cool-70' and output invalid CSS, so we use the hex + // source value instead: https://designsystem.digital.gov/design-tokens/color/system-tokens/ + --body-fg: #3d4551; + // --body-bg: #fff; + --body-quiet-color: #{$theme-color-base-dark}; + // --body-loud-color: #000; + + --header-color: var( --primary-fg); + --header-branding-color: var( --primary-fg); + // --header-bg: var(--secondary); + // --header-link-color: var(--primary-fg); + + --breadcrumbs-fg: #{$theme-color-accent-cool-lighter}; + // --breadcrumbs-link-fg: var(--body-bg); + --breadcrumbs-bg: #{$theme-color-primary-dark}; + + // #{$theme-link-color} would interpolate to 'primary', so we use the source value instead + --link-fg: #{$theme-color-primary}; + --link-hover-color: #{$theme-color-primary-darker}; + // $theme-link-visited-color - violet-70v + --link-selected-fg: #54278f; + + --hairline-color: #{$dhs-gray-15}; + // $theme-color-base-lightest - gray-5 + --border-color: #f0f0f0; + + --error-fg: #{$theme-color-error}; + + --message-success-bg: #{$theme-color-success-lighter}; + // $theme-color-warning-lighter - yellow-5 + --message-warning-bg: #faf3d1; + --message-error-bg: #{$theme-color-error-lighter}; + + --darkened-bg: #{$dhs-gray-15}; /* A bit darker than --body-bg */ + --selected-bg: var(--border-color); /* E.g. selected table cells */ + --selected-row: var(--message-warning-bg); + + // --button-fg: #fff; + // --button-bg: var(--secondary); + --button-hover-bg: #{$theme-color-primary-darker}; + --default-button-bg: #{$theme-color-primary-dark}; + --default-button-hover-bg: #{$theme-color-primary-darkest}; + // #{$theme-color-base} - 'gray-cool-50' + --close-button-bg: #71767a; + // #{$theme-color-base-darker} - 'gray-cool-70' + --close-button-hover-bg: #3d4551; + --delete-button-bg: #{$theme-color-error}; + --delete-button-hover-bg: #{$theme-color-error-dark}; + + // --object-tools-fg: var(--button-fg); + // --object-tools-bg: var(--close-button-bg); + // --object-tools-hover-bg: var(--close-button-hover-bg); + + --font-family-primary: + "Public Sans Web", + "Segoe UI", + system-ui, + Roboto, + "Helvetica Neue", + Arial, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji"; +} + +#branding h1 a:link, #branding h1 a:visited { + color: var(--primary-fg); +} + +// 'Delete button' layout bug +.submit-row a.deletelink { + height: auto!important; +} + +// Keep th from collapsing .min-width-25 { min-width: 25px; } diff --git a/src/registrar/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html index 1887ea332..9be44e43f 100644 --- a/src/registrar/templates/admin/base_site.html +++ b/src/registrar/templates/admin/base_site.html @@ -3,7 +3,9 @@ {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} -{% block extrastyle %}{{ block.super }}{% endblock %} +{% block extrastyle %}{{ block.super }} + +{% endblock %} {% block branding %}

.gov admin

From 4e6e98002a50f18a4361fc6d558886a556fbd742 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 1 Aug 2023 15:15:24 -0400 Subject: [PATCH 040/164] 820: action needed email content revision --- .../templates/emails/status_change_action_needed.txt | 9 ++++----- .../emails/status_change_action_needed_subject.txt | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/registrar/templates/emails/status_change_action_needed.txt b/src/registrar/templates/emails/status_change_action_needed.txt index 1edd4bc8a..bce47f070 100644 --- a/src/registrar/templates/emails/status_change_action_needed.txt +++ b/src/registrar/templates/emails/status_change_action_needed.txt @@ -1,7 +1,7 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} Hi {{ application.submitter.first_name }}. -Your .gov domain request requires a follow-up action to help with our review. +We've identified an action needed to complete the review of your .gov domain request. DOMAIN REQUESTED: {{ application.requested_domain.name }} REQUEST RECEIVED ON: {{ application.updated_at|date }} @@ -19,11 +19,10 @@ might add to the wait time. Learn more about withdrawing your request. NEXT STEPS -- You will receive a separate email from one of our analysts that provides details -about the action needed from you. You may need to update your application or -provide additional information. +- You will receive a separate email from our team that provides details about the action needed. +You may need to update your application or provide additional information. -- If you do not receive a separate email with these details, please contact us: +- If you do not receive a separate email with these details within one business day, please contact us: diff --git a/src/registrar/templates/emails/status_change_action_needed_subject.txt b/src/registrar/templates/emails/status_change_action_needed_subject.txt index ab67501c1..eac2bc2fc 100644 --- a/src/registrar/templates/emails/status_change_action_needed_subject.txt +++ b/src/registrar/templates/emails/status_change_action_needed_subject.txt @@ -1 +1 @@ -Your .gov domain request requires a follow-up action \ No newline at end of file +Action needed for your .gov domain request \ No newline at end of file From e639d944e9265ca57ee352423afbf5fd138a7d84 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 1 Aug 2023 15:41:48 -0400 Subject: [PATCH 041/164] Fix unit test for the action needed email --- src/registrar/tests/test_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index bdc4b062c..3c07e7608 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -192,8 +192,8 @@ class TestDomainApplicationAdmin(TestCase): # Assert or perform other checks on the email details expected_string = ( - "Your .gov domain request requires a follow-up action " - "to help with our review." + "We've identified an action needed to complete the " + "review of your .gov domain request." ) self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) self.assertEqual(to_email, EMAIL) From c4dfb5eec03036b23d4904a3448c53f746341fd4 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 1 Aug 2023 16:24:17 -0400 Subject: [PATCH 042/164] Fix primary font family assignment --- src/registrar/assets/sass/_theme/_admin.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 78fe8e109..6c693a2bd 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -66,7 +66,7 @@ // --object-tools-hover-bg: var(--close-button-hover-bg); --font-family-primary: - "Public Sans Web", + "Public Sans Pro Web", "Segoe UI", system-ui, Roboto, From 3d94b7153370ac2f1430482ce18b37925af0f3e1 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 1 Aug 2023 19:02:09 -0400 Subject: [PATCH 043/164] Extend the sublabel to add an optinal link at the end --- .../templates/application_tribal_government.html | 10 +++++++++- .../templates/includes/input_with_errors.html | 7 ++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/application_tribal_government.html b/src/registrar/templates/application_tribal_government.html index 83fbc98ff..f256026ad 100644 --- a/src/registrar/templates/application_tribal_government.html +++ b/src/registrar/templates/application_tribal_government.html @@ -3,7 +3,15 @@ {% block form_fields %} - {% input_with_errors forms.0.tribe_name %} + + {% with sublabel_text="Please include the entire name of your tribe as recognized by the" %} + {% with link_text="Bureau of Indian Affairs" %} + {% with link_href="https://rachid.me" %} + {% input_with_errors forms.0.tribe_name %} + {% endwith %} + {% endwith %} + {% endwith %} +

Is your organization a federally-recognized tribe or a state-recognized tribe? Check all that apply. diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index 4e9de8195..b44c7e775 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -29,7 +29,12 @@ error messages, if necessary. {% endif %} {% if sublabel_text %} -

{{ sublabel_text }}

+

+ {{ sublabel_text }} + {% if link_text %} + {{ link_text }}. + {% endif %} +

{% endif %} {% if field.errors %} From f500a4ff50ebea3a000096e55398b428e14eaea3 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 1 Aug 2023 19:06:40 -0400 Subject: [PATCH 044/164] pseudo code for 796 and 806 --- src/registrar/templates/home.html | 2 ++ src/registrar/views/utility/mixins.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 6163a1ce0..792beec43 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -33,6 +33,8 @@ {% for domain in domains %} + {% comment %} ticket 796 + {% if domain.application_status == "approved" or (domain.application does not exist) %} {% endcomment %} {{ domain.name }} diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 890b10a02..147712582 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -24,6 +24,11 @@ class DomainPermission(PermissionsLoginMixin): The user is in self.request.user and the domain needs to be looked up from the domain's primary key in self.kwargs["pk"] """ + + # ticket 806 + # if self.request.user is staff or admin and domain.application__status = 'approved' or 'rejected' or 'action needed' + # return True + if not self.request.user.is_authenticated: return False @@ -32,6 +37,10 @@ class DomainPermission(PermissionsLoginMixin): user=self.request.user, domain__id=self.kwargs["pk"] ).exists(): return False + + # ticket 796 + # if domain.application__status != 'approved' + # return false # if we need to check more about the nature of role, do it here. return True From 55f5cbd382437f0c6c780300f50179c807b6e30a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 02:06:38 +0000 Subject: [PATCH 045/164] Bump cryptography from 41.0.2 to 41.0.3 in /src Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.2 to 41.0.3. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/41.0.2...41.0.3) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- src/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requirements.txt b/src/requirements.txt index 75bdf8438..5216999cb 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -7,7 +7,7 @@ certifi==2023.5.7 ; python_version >= '3.6' cfenv==0.5.3 cffi==1.15.1 charset-normalizer==3.1.0 ; python_full_version >= '3.7.0' -cryptography==41.0.2 ; python_version >= '3.7' +cryptography==41.0.3 ; python_version >= '3.7' defusedxml==0.7.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' dj-database-url==2.0.0 dj-email-url==1.0.6 From b7f10e2b926b5e10fc3cb66a373c747efc1f3ba1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:04:37 -0600 Subject: [PATCH 046/164] Updated README.md to include a fix for line endings Updated the readme to include a fix for windows machines... On windows, you cannot run manage.py (or other related files) as they are using the wrong file endings. It basically doesn't know where to look. The fix to this is to add .gitattribute locally and rebuild your project, so that it reverts to standard line-endings on your machine (but not elsewhere) --- docs/developer/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/developer/README.md b/docs/developer/README.md index d824ea820..1fe36c4b2 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -18,6 +18,9 @@ If you're new to Django, see [Getting Started with Django](https://www.djangopro Visit the running application at [http://localhost:8080](http://localhost:8080). +Note: If you are using Windows, you may need to change your [line endings](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings). +If you'd rather not change this globally, add a .gitattributes file in the project root with `* text eol=lf` as the text content, and [refresh the repo](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings#refreshing-a-repository-after-changing-line-endings) + ## Branch Conventions We use the branch convention of `initials/branch-topic` (ex: `lmm/fix-footer`). This allows for automated deployment to a developer sandbox namespaced to the initials. From 22a89e030df0abd86a4ebdca51c353b0cc8fcc86 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Wed, 2 Aug 2023 17:34:32 -0400 Subject: [PATCH 047/164] Build capability to add sublabels with links to simple form components, with supporting custom filters and unit tests for the custom filters --- .../application_tribal_government.html | 8 +++-- .../templates/includes/input_with_errors.html | 16 +++++++--- src/registrar/templatetags/custom_filters.py | 17 ++++++++++ src/registrar/tests/test_templatetags.py | 32 ++++++++++++++++--- src/registrar/views/utility/mixins.py | 11 ++++--- 5 files changed, 68 insertions(+), 16 deletions(-) diff --git a/src/registrar/templates/application_tribal_government.html b/src/registrar/templates/application_tribal_government.html index f256026ad..3c2a8999f 100644 --- a/src/registrar/templates/application_tribal_government.html +++ b/src/registrar/templates/application_tribal_government.html @@ -4,10 +4,12 @@ {% block form_fields %} - {% with sublabel_text="Please include the entire name of your tribe as recognized by the" %} + {% with sublabel_text="Please include the entire name of your tribe as recognized by the Bureau of Indian Affairs." %} {% with link_text="Bureau of Indian Affairs" %} - {% with link_href="https://rachid.me" %} - {% input_with_errors forms.0.tribe_name %} + {% with link_href="https://www.federalregister.gov/documents/2023/01/12/2023-00504/indian-entities-recognized-by-and-eligible-to-receive-services-from-the-united-states-bureau-of" %} + {% with target_blank="true" %} + {% input_with_errors forms.0.tribe_name %} + {% endwith %} {% endwith %} {% endwith %} {% endwith %} diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index b44c7e775..da9e039e6 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -3,6 +3,8 @@ Template include for form fields with classes and their corresponding error messages, if necessary. {% endcomment %} +{% load custom_filters %} + {% load widget_tweaks %} {% if widget.attrs.maxlength %} @@ -30,12 +32,18 @@ error messages, if necessary. {% if sublabel_text %}

- {{ sublabel_text }} - {% if link_text %} - {{ link_text }}. + {% comment %} If the link_text appears twice, the first instance will be a link and the second instance will be ignored {% endcomment %} + {% if link_text and link_text in sublabel_text %} + {% with link_index=sublabel_text|find_index:link_text %} + {{ sublabel_text|slice:link_index }} + {% comment %} HTML will convert a new line into a space, resulting with a space before the fullstop in case link_text is at the end of sublabel_text, hence the unfortunate line below {% endcomment %} + {{ link_text }}{% with sublabel_part_after=sublabel_text|slice_after:link_text %}{{ sublabel_part_after }}{% endwith %} + {% endwith %} + {% else %} + {{ sublabel_text }} {% endif %}

- {% endif %} + {% endif %} {% if field.errors %}
diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index a22b15a28..f16408bf8 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -23,3 +23,20 @@ def extract_a_text(value): extracted_text = "" return extracted_text + + +@register.filter +def find_index(haystack, needle): + try: + return haystack.index(needle) + except ValueError: + return -1 + + +@register.filter +def slice_after(value, substring): + index = value.find(substring) + if index != -1: + result = value[index + len(substring) :] + return result + return value diff --git a/src/registrar/tests/test_templatetags.py b/src/registrar/tests/test_templatetags.py index 0d1eed4fc..36325ab5d 100644 --- a/src/registrar/tests/test_templatetags.py +++ b/src/registrar/tests/test_templatetags.py @@ -3,6 +3,12 @@ from django.conf import settings from django.test import TestCase from django.template import Context, Template +from registrar.templatetags.custom_filters import ( + extract_value, + extract_a_text, + find_index, + slice_after, +) class TestTemplateTags(TestCase): @@ -33,8 +39,6 @@ class TestTemplateTags(TestCase): class CustomFiltersTestCase(TestCase): def test_extract_value_filter(self): - from registrar.templatetags.custom_filters import extract_value - html_input = ( '' @@ -50,8 +54,6 @@ class CustomFiltersTestCase(TestCase): self.assertEqual(result, "abc") def test_extract_a_text_filter(self): - from registrar.templatetags.custom_filters import extract_a_text - input_text = 'Link Text' result = extract_a_text(input_text) self.assertEqual(result, "Link Text") @@ -59,3 +61,25 @@ class CustomFiltersTestCase(TestCase): input_text = 'Another Link' result = extract_a_text(input_text) self.assertEqual(result, "Another Link") + + def test_find_index(self): + haystack = "Hello, World!" + needle = "lo" + result = find_index(haystack, needle) + self.assertEqual(result, 3) + + needle = "XYZ" + result = find_index(haystack, needle) + self.assertEqual(result, -1) + + def test_slice_after(self): + value = "Hello, World!" + substring = "lo" + result = slice_after(value, substring) + self.assertEqual(result, ", World!") + + substring = "XYZ" + result = slice_after(value, substring) + self.assertEqual( + result, value + ) # Should return the original value if substring not found diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 147712582..05da75d35 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -24,11 +24,12 @@ class DomainPermission(PermissionsLoginMixin): The user is in self.request.user and the domain needs to be looked up from the domain's primary key in self.kwargs["pk"] """ - + # ticket 806 - # if self.request.user is staff or admin and domain.application__status = 'approved' or 'rejected' or 'action needed' + # if self.request.user is staff or admin and + # domain.application__status = 'approved' or 'rejected' or 'action needed' # return True - + if not self.request.user.is_authenticated: return False @@ -37,10 +38,10 @@ class DomainPermission(PermissionsLoginMixin): user=self.request.user, domain__id=self.kwargs["pk"] ).exists(): return False - + # ticket 796 # if domain.application__status != 'approved' - # return false + # return false # if we need to check more about the nature of role, do it here. return True From e8422eac7d4147ce297a97898d0660fabda2822e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 3 Aug 2023 07:47:38 -0600 Subject: [PATCH 048/164] Updated README.md to be more descriptive Added an explanation for why Windows developers may need to change their line endings --- docs/developer/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/developer/README.md b/docs/developer/README.md index 1fe36c4b2..ea87c99db 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -19,7 +19,8 @@ If you're new to Django, see [Getting Started with Django](https://www.djangopro Visit the running application at [http://localhost:8080](http://localhost:8080). Note: If you are using Windows, you may need to change your [line endings](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings). -If you'd rather not change this globally, add a .gitattributes file in the project root with `* text eol=lf` as the text content, and [refresh the repo](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings#refreshing-a-repository-after-changing-line-endings) +* Unix based operating systems (like macOS or Linux) handle line separators [differently than Windows does](https://superuser.com/questions/374028/how-are-n-and-r-handled-differently-on-linux-and-windows). This can break bash scripts in particular. In the case of manage.py, it uses *#!/usr/bin/env python* to access Python. Since our scripts are still thinking in terms of unix line seperators, they may look for the *'python\r'* rather than *python* since Windows cannot read the carriage return on its own - thus leading to the error `usr/bin/env: 'python\r' no such file or directory` +* If you'd rather not change this globally, add a `.gitattributes` file in the project root with `* text eol=lf` as the text content, and [refresh the repo](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings#refreshing-a-repository-after-changing-line-endings) ## Branch Conventions From 864e31eba3f506938f4d127db8fb94537d8b8132 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 3 Aug 2023 07:49:12 -0600 Subject: [PATCH 049/164] Fixed a typo in README.md --- docs/developer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/README.md b/docs/developer/README.md index ea87c99db..ca35b5891 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -19,7 +19,7 @@ If you're new to Django, see [Getting Started with Django](https://www.djangopro Visit the running application at [http://localhost:8080](http://localhost:8080). Note: If you are using Windows, you may need to change your [line endings](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings). -* Unix based operating systems (like macOS or Linux) handle line separators [differently than Windows does](https://superuser.com/questions/374028/how-are-n-and-r-handled-differently-on-linux-and-windows). This can break bash scripts in particular. In the case of manage.py, it uses *#!/usr/bin/env python* to access Python. Since our scripts are still thinking in terms of unix line seperators, they may look for the *'python\r'* rather than *python* since Windows cannot read the carriage return on its own - thus leading to the error `usr/bin/env: 'python\r' no such file or directory` +* Unix based operating systems (like macOS or Linux) handle line separators [differently than Windows does](https://superuser.com/questions/374028/how-are-n-and-r-handled-differently-on-linux-and-windows). This can break bash scripts in particular. In the case of manage.py, it uses *#!/usr/bin/env python* to access Python. Since our scripts are still thinking in terms of unix line seperators, they may look for *'python\r'* rather than *python* since Windows cannot read the carriage return on its own - thus leading to the error `usr/bin/env: 'python\r' no such file or directory` * If you'd rather not change this globally, add a `.gitattributes` file in the project root with `* text eol=lf` as the text content, and [refresh the repo](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings#refreshing-a-repository-after-changing-line-endings) ## Branch Conventions From 6d721ad6fbc2af3f442a84b1b665462c81d63f3d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 3 Aug 2023 07:55:37 -0600 Subject: [PATCH 050/164] Another minor verbiage change --- docs/developer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/README.md b/docs/developer/README.md index ca35b5891..886b435d9 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -19,7 +19,7 @@ If you're new to Django, see [Getting Started with Django](https://www.djangopro Visit the running application at [http://localhost:8080](http://localhost:8080). Note: If you are using Windows, you may need to change your [line endings](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings). -* Unix based operating systems (like macOS or Linux) handle line separators [differently than Windows does](https://superuser.com/questions/374028/how-are-n-and-r-handled-differently-on-linux-and-windows). This can break bash scripts in particular. In the case of manage.py, it uses *#!/usr/bin/env python* to access Python. Since our scripts are still thinking in terms of unix line seperators, they may look for *'python\r'* rather than *python* since Windows cannot read the carriage return on its own - thus leading to the error `usr/bin/env: 'python\r' no such file or directory` +* Unix based operating systems (like macOS or Linux) handle line separators [differently than Windows does](https://superuser.com/questions/374028/how-are-n-and-r-handled-differently-on-linux-and-windows). This can break bash scripts in particular. In the case of manage.py, it uses *#!/usr/bin/env python* to access the Python executable. Since our scripts are still thinking in terms of unix line seperators, they may look for *'python\r'* rather than *python* since Windows cannot read the carriage return on its own - thus leading to the error `usr/bin/env: 'python\r' no such file or directory` * If you'd rather not change this globally, add a `.gitattributes` file in the project root with `* text eol=lf` as the text content, and [refresh the repo](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings#refreshing-a-repository-after-changing-line-endings) ## Branch Conventions From b4d4b536d9f01f02ec352f700671a629d90acbd1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 3 Aug 2023 08:06:35 -0600 Subject: [PATCH 051/164] Made the text a bit more clear --- docs/developer/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer/README.md b/docs/developer/README.md index 886b435d9..2c129b5fa 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -18,8 +18,8 @@ If you're new to Django, see [Getting Started with Django](https://www.djangopro Visit the running application at [http://localhost:8080](http://localhost:8080). -Note: If you are using Windows, you may need to change your [line endings](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings). -* Unix based operating systems (like macOS or Linux) handle line separators [differently than Windows does](https://superuser.com/questions/374028/how-are-n-and-r-handled-differently-on-linux-and-windows). This can break bash scripts in particular. In the case of manage.py, it uses *#!/usr/bin/env python* to access the Python executable. Since our scripts are still thinking in terms of unix line seperators, they may look for *'python\r'* rather than *python* since Windows cannot read the carriage return on its own - thus leading to the error `usr/bin/env: 'python\r' no such file or directory` +Note: If you are using Windows, you may need to change your [line endings](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings). If not, you may not be able to run manage.py. +* Unix based operating systems (like macOS or Linux) handle line separators [differently than Windows does](https://superuser.com/questions/374028/how-are-n-and-r-handled-differently-on-linux-and-windows). This can break bash scripts in particular. In the case of manage.py, it uses *#!/usr/bin/env python* to access the Python executable. Since the script is still thinking in terms of unix line seperators, it may look for the executable *python\r* rather than *python* (since Windows cannot read the carriage return on its own) - thus leading to the error `usr/bin/env: 'python\r' no such file or directory` * If you'd rather not change this globally, add a `.gitattributes` file in the project root with `* text eol=lf` as the text content, and [refresh the repo](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings#refreshing-a-repository-after-changing-line-endings) ## Branch Conventions From 916857a0809905846c0a147572948c7b2b0e31dc Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Thu, 3 Aug 2023 10:26:41 -0400 Subject: [PATCH 052/164] Tweak comment --- src/registrar/templates/includes/input_with_errors.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index da9e039e6..b35ab1b7a 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -32,7 +32,7 @@ error messages, if necessary. {% if sublabel_text %}

- {% comment %} If the link_text appears twice, the first instance will be a link and the second instance will be ignored {% endcomment %} + {% comment %} If the link_text appears more than once, the first instance will be a link and the other instances will be ignored {% endcomment %} {% if link_text and link_text in sublabel_text %} {% with link_index=sublabel_text|find_index:link_text %} {{ sublabel_text|slice:link_index }} From 5dd2fbaedad1ec0883bb6dec9468ee4e22c21b7c Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Thu, 3 Aug 2023 11:08:09 -0400 Subject: [PATCH 053/164] Fix dark mode --- src/registrar/assets/sass/_theme/_admin.scss | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 6c693a2bd..3bc6be8af 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -79,6 +79,49 @@ "Noto Color Emoji"; } +// Fold dark theme settings into our main CSS +// https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#theming-support > dark theme note +@media (prefers-color-scheme: dark) { + :root, + html[data-theme="dark"] { + // Edit the primary to meet accessibility requ. + --primary: #23485a; + --primary-fg: #f7f7f7; + + --body-fg: #eeeeee; + --body-bg: #121212; + --body-quiet-color: #e0e0e0; + --body-loud-color: #ffffff; + + --breadcrumbs-link-fg: #e0e0e0; + --breadcrumbs-bg: var(--primary); + + --link-fg: #81d4fa; + --link-hover-color: #4ac1f7; + --link-selected-fg: #6f94c6; + + --hairline-color: #272727; + --border-color: #353535; + + --error-fg: #e35f5f; + --message-success-bg: #006b1b; + --message-warning-bg: #583305; + --message-error-bg: #570808; + + --darkened-bg: #212121; + --selected-bg: #1b1b1b; + --selected-row: #00363a; + + --close-button-bg: #333333; + --close-button-hover-bg: #666666; + } + + // Fix dark mode bug + body { + color: var(--body-fg)!important; + } +} + #branding h1 a:link, #branding h1 a:visited { color: var(--primary-fg); } From 35dcc0dbb67850228606e097d761f6b08dccca11 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:27:08 -0600 Subject: [PATCH 054/164] Added new user in Fixtures.py --- src/registrar/fixtures.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index 909ff5f58..2813413ff 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -25,7 +25,6 @@ class UserFixture: in management/commands/load.py, then use `./manage.py load` to run this code. """ - ADMINS = [ { "username": "5f283494-31bd-49b5-b024-a7e7cae00848", @@ -57,6 +56,11 @@ class UserFixture: "first_name": "Ryan", "last_name": "Brooks", }, + { + "username": "30001ee7-0467-4df2-8db2-786e79606060", + "first_name": "Zander", + "last_name": "Adkinson", + }, ] STAFF = [ @@ -69,6 +73,11 @@ class UserFixture: "username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd", "first_name": "Alysia-Analyst", "last_name": "Alysia-Analyst", + }, + { + "username": "2cc0cde8-8313-4a50-99d8-5882e71443e8", + "first_name": "Zander-Analyst", + "last_name": "Adkinson-Analyst", }, ] From 25cda0143c75ca56d9f1054abd5cb41d341aedae Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:28:17 -0600 Subject: [PATCH 055/164] Update fixtures.py --- src/registrar/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index 2813413ff..287e49d92 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -74,7 +74,7 @@ class UserFixture: "first_name": "Alysia-Analyst", "last_name": "Alysia-Analyst", }, - { + { "username": "2cc0cde8-8313-4a50-99d8-5882e71443e8", "first_name": "Zander-Analyst", "last_name": "Adkinson-Analyst", From 266e9546ce6f38a6b564c905aefa500236cf7148 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:40:37 -0600 Subject: [PATCH 056/164] Fixed a formatting issue --- src/registrar/fixtures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index 287e49d92..a649d861b 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -25,6 +25,7 @@ class UserFixture: in management/commands/load.py, then use `./manage.py load` to run this code. """ + ADMINS = [ { "username": "5f283494-31bd-49b5-b024-a7e7cae00848", From 9ea05e016ec11f001e62456da5965ea4839efa29 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:46:49 -0600 Subject: [PATCH 057/164] Linter begone! --- src/registrar/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index a649d861b..9ca20b2a0 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -25,7 +25,7 @@ class UserFixture: in management/commands/load.py, then use `./manage.py load` to run this code. """ - + ADMINS = [ { "username": "5f283494-31bd-49b5-b024-a7e7cae00848", From 03177bdb1fe2c702f1bdaa6979e62c550a4cbba1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:30:30 -0600 Subject: [PATCH 058/164] Updated README.md to include adding an analyst --- docs/developer/README.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/developer/README.md b/docs/developer/README.md index d824ea820..344c50f77 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -66,7 +66,7 @@ The endpoint /admin can be used to view and manage site content, including but n 1. Login via login.gov 2. Go to the home page and make sure you can see the part where you can submit an application 3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4 -4. in src/registrar/fixtures.py add to the ADMINS list in that file by adding your UUID as your username along with your first and last name. See below: +4. in src/registrar/fixtures.py add to the `ADMINS` list in that file by adding your UUID as your username along with your first and last name. See below: ``` ADMINS = [ @@ -78,9 +78,30 @@ The endpoint /admin can be used to view and manage site content, including but n ... ] ``` +5. In the browser, navigate to /admin. To verify that all is working correctly, under "domain applications" you should see fake domains with various fake statuses. -5. In the browser, navigate to /admins. To verify that all is working correctly, under "domain applications" you should see fake domains with various fake statuses. +### Adding an Analyst to /admin +Analysts are a variant of the admin role with limited permissions. The process for adding an Analyst is much the same as adding an admin: +1. Login via login.gov (if you already exist as an admin, you will need to create a separate login.gov account for this: i.e. first.last+1@email.com) +2. Go to the home page and make sure you can see the part where you can submit an application +3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4 (this will be a different UUID than the one obtained from creating an admin) +4. in src/registrar/fixtures.py add to the `STAFF` list in that file by adding your UUID as your username along with your first and last name. See below: + +``` + STAFF = [ + { + "username": "", + "first_name": "", + "last_name": "", + }, + ... + ] +``` + +5. In the browser, navigate to /admin. To verify that all is working correctly, verify that you can only see a sub-section of the modules and some are set to view-only. + +Do note that if you wish to have both an analyst and admin account, append `-Analyst` to your first and last name. Example: `Bob-Analyst` ## Adding to CODEOWNERS (optional) The CODEOWNERS file sets the tagged individuals as default reviewers on any Pull Request that changes files that they are marked as owners of. From dcf8135e5fb2d638465b1655185e02058699aeaa Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Thu, 3 Aug 2023 18:46:41 -0700 Subject: [PATCH 059/164] partially created za sandbox --- ops/manifests/manifest-za.yaml | 29 +++++++++++++++++++++++++++++ src/registrar/config/settings.py | 1 + 2 files changed, 30 insertions(+) create mode 100644 ops/manifests/manifest-za.yaml diff --git a/ops/manifests/manifest-za.yaml b/ops/manifests/manifest-za.yaml new file mode 100644 index 000000000..fbacb6912 --- /dev/null +++ b/ops/manifests/manifest-za.yaml @@ -0,0 +1,29 @@ +--- +applications: +- name: getgov-za + buildpacks: + - python_buildpack + path: ../../src + instances: 1 + memory: 512M + stack: cflinuxfs4 + timeout: 180 + command: ./run.sh + health-check-type: http + health-check-http-endpoint: /health + env: + # Send stdout and stderr straight to the terminal without buffering + PYTHONUNBUFFERED: yup + # Tell Django where to find its configuration + DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: https://getgov-za.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO + # default public site location + GETGOV_PUBLIC_SITE_URL: https://beta.get.gov + routes: + - route: getgov-za.app.cloud.gov + services: + - getgov-credentials + - getgov-za-database diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 90918c929..60e7909e4 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -571,6 +571,7 @@ SECURE_SSL_REDIRECT = True ALLOWED_HOSTS = [ "getgov-stable.app.cloud.gov", "getgov-staging.app.cloud.gov", + "getgov-za.app.cloud.gov", "getgov-gd.app.cloud.gov", "getgov-rb.app.cloud.gov", "getgov-ko.app.cloud.gov", From 889be277f2d8d0a03be94efe956f93cce23268c7 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 4 Aug 2023 07:24:41 -0700 Subject: [PATCH 060/164] updated sandbox for zander --- .github/workflows/deploy-sandbox.yaml | 1 + .github/workflows/migrate.yaml | 1 + .github/workflows/reset-db.yaml | 1 + ops/scripts/create_dev_sandbox.sh | 132 +++++++++++++------------- 4 files changed, 69 insertions(+), 66 deletions(-) diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index 614484a2a..222f76365 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -15,6 +15,7 @@ jobs: || startsWith(github.head_ref, 'rb/') || startsWith(github.head_ref, 'ko/') || startsWith(github.head_ref, 'gd/') + || startsWith(github.head_ref, 'za/') outputs: environment: ${{ steps.var.outputs.environment}} runs-on: "ubuntu-latest" diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index 28447a605..590c63f53 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -15,6 +15,7 @@ on: options: - stable - staging + - za - gd - rb - ko diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index 0d3ed4934..4632b4c16 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -16,6 +16,7 @@ on: options: - stable - staging + - za - gd - rb - ko diff --git a/ops/scripts/create_dev_sandbox.sh b/ops/scripts/create_dev_sandbox.sh index f180ada8d..11cbb7092 100755 --- a/ops/scripts/create_dev_sandbox.sh +++ b/ops/scripts/create_dev_sandbox.sh @@ -2,85 +2,85 @@ # infrastructure needed to run get.gov. It can serve for documentation for running # NOTE: This script was written for MacOS and to be run at the root directory. -if [ -z "$1" ]; then - echo 'Please specify a new space to create (i.e. lmm)' >&2 - exit 1 -fi +# if [ -z "$1" ]; then +# echo 'Please specify a new space to create (i.e. lmm)' >&2 +# exit 1 +# fi -if [ ! $(command -v gh) ] || [ ! $(command -v jq) ] || [ ! $(command -v cf) ]; then - echo "jq, cf, and gh packages must be installed. Please install via your preferred manager." - exit 1 -fi +# if [ ! $(command -v gh) ] || [ ! $(command -v jq) ] || [ ! $(command -v cf) ]; then +# echo "jq, cf, and gh packages must be installed. Please install via your preferred manager." +# exit 1 +# fi -upcase_name=$(printf "%s" "$1" | tr '[:lower:]' '[:upper:]') +# upcase_name=$(printf "%s" "$1" | tr '[:lower:]' '[:upper:]') -read -p "Are you on a new branch? We will have to commit this work. (y/n) " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]] -then - git checkout -b new-dev-sandbox-$1 -fi +# read -p "Are you on a new branch? We will have to commit this work. (y/n) " -n 1 -r +# echo +# if [[ ! $REPLY =~ ^[Yy]$ ]] +# then +# git checkout -b new-dev-sandbox-$1 +# fi -cf target -o cisa-getgov-prototyping +# cf target -o cisa-getgov-prototyping -read -p "Are you logged in to the cisa-getgov-prototyping CF org above? (y/n) " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]] -then - cf login -a https://api.fr.cloud.gov --sso -fi +# read -p "Are you logged in to the cisa-getgov-prototyping CF org above? (y/n) " -n 1 -r +# echo +# if [[ ! $REPLY =~ ^[Yy]$ ]] +# then +# cf login -a https://api.fr.cloud.gov --sso +# fi -gh auth status -read -p "Are you logged into a Github account with access to cisagov/getgov? (y/n) " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]] -then - gh auth login -fi +# gh auth status +# read -p "Are you logged into a Github account with access to cisagov/getgov? (y/n) " -n 1 -r +# echo +# if [[ ! $REPLY =~ ^[Yy]$ ]] +# then +# gh auth login +# fi -echo "Creating manifest for $1..." -cp ops/scripts/manifest-sandbox-template.yaml ops/manifests/manifest-$1.yaml -sed -i '' "s/ENVIRONMENT/$1/" "ops/manifests/manifest-$1.yaml" +# echo "Creating manifest for $1..." +# cp ops/scripts/manifest-sandbox-template.yaml ops/manifests/manifest-$1.yaml +# sed -i '' "s/ENVIRONMENT/$1/" "ops/manifests/manifest-$1.yaml" -echo "Adding new environment to settings.py..." -sed -i '' '/getgov-staging.app.cloud.gov/ {a\ - '\"getgov-$1.app.cloud.gov\"', -}' src/registrar/config/settings.py +# echo "Adding new environment to settings.py..." +# sed -i '' '/getgov-staging.app.cloud.gov/ {a\ +# '\"getgov-$1.app.cloud.gov\"', +# }' src/registrar/config/settings.py -echo "Creating new cloud.gov space for $1..." -cf create-space $1 -cf target -o "cisa-getgov-prototyping" -s $1 -cf bind-security-group public_networks_egress cisa-getgov-prototyping --space $1 -cf bind-security-group trusted_local_networks_egress cisa-getgov-prototyping --space $1 +# echo "Creating new cloud.gov space for $1..." +# cf create-space $1 +# cf target -o "cisa-getgov-prototyping" -s $1 +# cf bind-security-group public_networks_egress cisa-getgov-prototyping --space $1 +# cf bind-security-group trusted_local_networks_egress cisa-getgov-prototyping --space $1 -echo "Creating new cloud.gov DB for $1. This usually takes about 5 minutes..." -cf create-service aws-rds micro-psql getgov-$1-database +# echo "Creating new cloud.gov DB for $1. This usually takes about 5 minutes..." +# cf create-service aws-rds micro-psql getgov-$1-database -until cf service getgov-$1-database | grep -q 'The service instance status is succeeded' -do - echo "Database not up yet, waiting..." - sleep 30 -done +# until cf service getgov-$1-database | grep -q 'The service instance status is succeeded' +# do +# echo "Database not up yet, waiting..." +# sleep 30 +# done -echo "Creating new cloud.gov credentials for $1..." -django_key=$(python3 -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())') -openssl req -nodes -x509 -days 365 -newkey rsa:2048 -keyout private-$1.pem -out public-$1.crt -login_key=$(base64 -i private-$1.pem) -jq -n --arg django_key "$django_key" --arg login_key "$login_key" '{"DJANGO_SECRET_KEY":$django_key,"DJANGO_SECRET_LOGIN_KEY":$login_key}' > credentials-$1.json -cf cups getgov-credentials -p credentials-$1.json +# echo "Creating new cloud.gov credentials for $1..." +# django_key=$(python3 -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())') +# openssl req -nodes -x509 -days 365 -newkey rsa:2048 -keyout private-$1.pem -out public-$1.crt +# login_key=$(base64 -i private-$1.pem) +# jq -n --arg django_key "$django_key" --arg login_key "$login_key" '{"DJANGO_SECRET_KEY":$django_key,"DJANGO_SECRET_LOGIN_KEY":$login_key}' > credentials-$1.json +# cf cups getgov-credentials -p credentials-$1.json -echo "Now you will need to update some things for Login. Please sign-in to https://dashboard.int.identitysandbox.gov/." -echo "Navigate to our application config: https://dashboard.int.identitysandbox.gov/service_providers/2640/edit?" -echo "There are two things to update." -echo "1. You need to upload the public-$1.crt file generated as part of the previous command." -echo "2. You need to add two redirect URIs: https://getgov-$1.app.cloud.gov/openid/callback/login/ and -https://getgov-$1.app.cloud.gov/openid/callback/logout/ to the list of URIs." -read -p "Please confirm when this is done (y/n) " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]] -then - exit 1 -fi +# echo "Now you will need to update some things for Login. Please sign-in to https://dashboard.int.identitysandbox.gov/." +# echo "Navigate to our application config: https://dashboard.int.identitysandbox.gov/service_providers/2640/edit?" +# echo "There are two things to update." +# echo "1. You need to upload the public-$1.crt file generated as part of the previous command." +# echo "2. You need to add two redirect URIs: https://getgov-$1.app.cloud.gov/openid/callback/login/ and +# https://getgov-$1.app.cloud.gov/openid/callback/logout/ to the list of URIs." +# read -p "Please confirm when this is done (y/n) " -n 1 -r +# echo +# if [[ ! $REPLY =~ ^[Yy]$ ]] +# then +# exit 1 +# fi echo "Database create succeeded and credentials created. Deploying the get.gov application to the new space $1..." echo "Building assets..." From 3323fb56fca314e9063419668cb651f04d4fafeb Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 4 Aug 2023 07:26:56 -0700 Subject: [PATCH 061/164] removed code comments --- ops/scripts/create_dev_sandbox.sh | 132 +++++++++++++++--------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/ops/scripts/create_dev_sandbox.sh b/ops/scripts/create_dev_sandbox.sh index 11cbb7092..f180ada8d 100755 --- a/ops/scripts/create_dev_sandbox.sh +++ b/ops/scripts/create_dev_sandbox.sh @@ -2,85 +2,85 @@ # infrastructure needed to run get.gov. It can serve for documentation for running # NOTE: This script was written for MacOS and to be run at the root directory. -# if [ -z "$1" ]; then -# echo 'Please specify a new space to create (i.e. lmm)' >&2 -# exit 1 -# fi +if [ -z "$1" ]; then + echo 'Please specify a new space to create (i.e. lmm)' >&2 + exit 1 +fi -# if [ ! $(command -v gh) ] || [ ! $(command -v jq) ] || [ ! $(command -v cf) ]; then -# echo "jq, cf, and gh packages must be installed. Please install via your preferred manager." -# exit 1 -# fi +if [ ! $(command -v gh) ] || [ ! $(command -v jq) ] || [ ! $(command -v cf) ]; then + echo "jq, cf, and gh packages must be installed. Please install via your preferred manager." + exit 1 +fi -# upcase_name=$(printf "%s" "$1" | tr '[:lower:]' '[:upper:]') +upcase_name=$(printf "%s" "$1" | tr '[:lower:]' '[:upper:]') -# read -p "Are you on a new branch? We will have to commit this work. (y/n) " -n 1 -r -# echo -# if [[ ! $REPLY =~ ^[Yy]$ ]] -# then -# git checkout -b new-dev-sandbox-$1 -# fi +read -p "Are you on a new branch? We will have to commit this work. (y/n) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]] +then + git checkout -b new-dev-sandbox-$1 +fi -# cf target -o cisa-getgov-prototyping +cf target -o cisa-getgov-prototyping -# read -p "Are you logged in to the cisa-getgov-prototyping CF org above? (y/n) " -n 1 -r -# echo -# if [[ ! $REPLY =~ ^[Yy]$ ]] -# then -# cf login -a https://api.fr.cloud.gov --sso -# fi +read -p "Are you logged in to the cisa-getgov-prototyping CF org above? (y/n) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]] +then + cf login -a https://api.fr.cloud.gov --sso +fi -# gh auth status -# read -p "Are you logged into a Github account with access to cisagov/getgov? (y/n) " -n 1 -r -# echo -# if [[ ! $REPLY =~ ^[Yy]$ ]] -# then -# gh auth login -# fi +gh auth status +read -p "Are you logged into a Github account with access to cisagov/getgov? (y/n) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]] +then + gh auth login +fi -# echo "Creating manifest for $1..." -# cp ops/scripts/manifest-sandbox-template.yaml ops/manifests/manifest-$1.yaml -# sed -i '' "s/ENVIRONMENT/$1/" "ops/manifests/manifest-$1.yaml" +echo "Creating manifest for $1..." +cp ops/scripts/manifest-sandbox-template.yaml ops/manifests/manifest-$1.yaml +sed -i '' "s/ENVIRONMENT/$1/" "ops/manifests/manifest-$1.yaml" -# echo "Adding new environment to settings.py..." -# sed -i '' '/getgov-staging.app.cloud.gov/ {a\ -# '\"getgov-$1.app.cloud.gov\"', -# }' src/registrar/config/settings.py +echo "Adding new environment to settings.py..." +sed -i '' '/getgov-staging.app.cloud.gov/ {a\ + '\"getgov-$1.app.cloud.gov\"', +}' src/registrar/config/settings.py -# echo "Creating new cloud.gov space for $1..." -# cf create-space $1 -# cf target -o "cisa-getgov-prototyping" -s $1 -# cf bind-security-group public_networks_egress cisa-getgov-prototyping --space $1 -# cf bind-security-group trusted_local_networks_egress cisa-getgov-prototyping --space $1 +echo "Creating new cloud.gov space for $1..." +cf create-space $1 +cf target -o "cisa-getgov-prototyping" -s $1 +cf bind-security-group public_networks_egress cisa-getgov-prototyping --space $1 +cf bind-security-group trusted_local_networks_egress cisa-getgov-prototyping --space $1 -# echo "Creating new cloud.gov DB for $1. This usually takes about 5 minutes..." -# cf create-service aws-rds micro-psql getgov-$1-database +echo "Creating new cloud.gov DB for $1. This usually takes about 5 minutes..." +cf create-service aws-rds micro-psql getgov-$1-database -# until cf service getgov-$1-database | grep -q 'The service instance status is succeeded' -# do -# echo "Database not up yet, waiting..." -# sleep 30 -# done +until cf service getgov-$1-database | grep -q 'The service instance status is succeeded' +do + echo "Database not up yet, waiting..." + sleep 30 +done -# echo "Creating new cloud.gov credentials for $1..." -# django_key=$(python3 -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())') -# openssl req -nodes -x509 -days 365 -newkey rsa:2048 -keyout private-$1.pem -out public-$1.crt -# login_key=$(base64 -i private-$1.pem) -# jq -n --arg django_key "$django_key" --arg login_key "$login_key" '{"DJANGO_SECRET_KEY":$django_key,"DJANGO_SECRET_LOGIN_KEY":$login_key}' > credentials-$1.json -# cf cups getgov-credentials -p credentials-$1.json +echo "Creating new cloud.gov credentials for $1..." +django_key=$(python3 -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())') +openssl req -nodes -x509 -days 365 -newkey rsa:2048 -keyout private-$1.pem -out public-$1.crt +login_key=$(base64 -i private-$1.pem) +jq -n --arg django_key "$django_key" --arg login_key "$login_key" '{"DJANGO_SECRET_KEY":$django_key,"DJANGO_SECRET_LOGIN_KEY":$login_key}' > credentials-$1.json +cf cups getgov-credentials -p credentials-$1.json -# echo "Now you will need to update some things for Login. Please sign-in to https://dashboard.int.identitysandbox.gov/." -# echo "Navigate to our application config: https://dashboard.int.identitysandbox.gov/service_providers/2640/edit?" -# echo "There are two things to update." -# echo "1. You need to upload the public-$1.crt file generated as part of the previous command." -# echo "2. You need to add two redirect URIs: https://getgov-$1.app.cloud.gov/openid/callback/login/ and -# https://getgov-$1.app.cloud.gov/openid/callback/logout/ to the list of URIs." -# read -p "Please confirm when this is done (y/n) " -n 1 -r -# echo -# if [[ ! $REPLY =~ ^[Yy]$ ]] -# then -# exit 1 -# fi +echo "Now you will need to update some things for Login. Please sign-in to https://dashboard.int.identitysandbox.gov/." +echo "Navigate to our application config: https://dashboard.int.identitysandbox.gov/service_providers/2640/edit?" +echo "There are two things to update." +echo "1. You need to upload the public-$1.crt file generated as part of the previous command." +echo "2. You need to add two redirect URIs: https://getgov-$1.app.cloud.gov/openid/callback/login/ and +https://getgov-$1.app.cloud.gov/openid/callback/logout/ to the list of URIs." +read -p "Please confirm when this is done (y/n) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]] +then + exit 1 +fi echo "Database create succeeded and credentials created. Deploying the get.gov application to the new space $1..." echo "Building assets..." From db53bb8beb0ae93616c986fd1084bb63fda64507 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 4 Aug 2023 08:44:39 -0600 Subject: [PATCH 062/164] Update docs/developer/README.md Added @abroddricks suggestion Co-authored-by: Alysia Broddrick <109625347+abroddrick@users.noreply.github.com> --- docs/developer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/README.md b/docs/developer/README.md index 344c50f77..c3176257d 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -101,7 +101,7 @@ Analysts are a variant of the admin role with limited permissions. The process f 5. In the browser, navigate to /admin. To verify that all is working correctly, verify that you can only see a sub-section of the modules and some are set to view-only. -Do note that if you wish to have both an analyst and admin account, append `-Analyst` to your first and last name. Example: `Bob-Analyst` +Do note that if you wish to have both an analyst and admin account, append `-Analyst` to your first and last name, or use a completely different first/last name to avoid confusion. Example: `Bob-Analyst` ## Adding to CODEOWNERS (optional) The CODEOWNERS file sets the tagged individuals as default reviewers on any Pull Request that changes files that they are marked as owners of. From 952de458b70122083f7529e9e98ca31b2c9856c0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 4 Aug 2023 08:47:48 -0600 Subject: [PATCH 063/164] Update README.md Added a missing newline above line 81 --- docs/developer/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/developer/README.md b/docs/developer/README.md index 344c50f77..3eefeb179 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -78,6 +78,7 @@ The endpoint /admin can be used to view and manage site content, including but n ... ] ``` + 5. In the browser, navigate to /admin. To verify that all is working correctly, under "domain applications" you should see fake domains with various fake statuses. ### Adding an Analyst to /admin From 4296352f0f963549b23b616f8d06ce21a3ce02fe Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Fri, 4 Aug 2023 13:31:49 -0400 Subject: [PATCH 064/164] revert package lock changes, address CSS feedback on typography, color and grid --- src/package-lock.json | 18 ++++----- src/registrar/assets/sass/_theme/_admin.scss | 39 +++++++++++--------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 87e43f28c..ac7977ef2 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1072,9 +1072,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001517", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz", - "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==", + "version": "1.0.30001450", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz", + "integrity": "sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew==", "dev": true, "funding": [ { @@ -1084,10 +1084,6 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" } ] }, @@ -7794,9 +7790,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001517", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz", - "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==", + "version": "1.0.30001450", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz", + "integrity": "sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew==", "dev": true }, "check-types": { @@ -12361,4 +12357,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 3bc6be8af..cb5b101de 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -2,8 +2,8 @@ @use "uswds-core" as *; // We'll use Django's CSS vars: https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#theming-support -// and assign Uswds theme vars whenever possible -// If needed (see below), we'll use the Uswds hex value +// and assign USWDS theme vars whenever possible +// If needed (see below), we'll use the USWDS hex value // As a last resort, we'll use CISA colors to supplement the palette :root { --primary: #{$theme-color-primary}; @@ -11,7 +11,7 @@ --accent: #{$theme-color-accent-cool}; // --primary-fg: #fff; - // Uswds theme vars that are set to a token, such as #{$theme-color-base-darker} + // USWDS theme vars that are set to a token, such as #{$theme-color-base-darker} // would interpolate to 'gray-cool-70' and output invalid CSS, so we use the hex // source value instead: https://designsystem.digital.gov/design-tokens/color/system-tokens/ --body-fg: #3d4551; @@ -24,7 +24,7 @@ // --header-bg: var(--secondary); // --header-link-color: var(--primary-fg); - --breadcrumbs-fg: #{$theme-color-accent-cool-lighter}; + --breadcrumbs-fg: #{$theme-color-accent-cool-lightest}; // --breadcrumbs-link-fg: var(--body-bg); --breadcrumbs-bg: #{$theme-color-primary-dark}; @@ -64,19 +64,6 @@ // --object-tools-fg: var(--button-fg); // --object-tools-bg: var(--close-button-bg); // --object-tools-hover-bg: var(--close-button-hover-bg); - - --font-family-primary: - "Public Sans Pro Web", - "Segoe UI", - system-ui, - Roboto, - "Helvetica Neue", - Arial, - sans-serif, - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", - "Noto Color Emoji"; } // Fold dark theme settings into our main CSS @@ -116,7 +103,9 @@ --close-button-hover-bg: #666666; } - // Fix dark mode bug + // Fix dark mode bug + // The structure of our CSS messes with the cascade and causes some body copy in tables + //to have light mode colors in dark mode, resulting in a severe contrast issue. body { color: var(--body-fg)!important; } @@ -126,6 +115,20 @@ color: var(--primary-fg); } +#branding h1, +h1, h2, h3 { + font-weight: font-weight('bold'); +} + +table > caption > a { + font-weight: font-weight('bold'); + text-transform: none; +} + +#nav-sidebar { + padding-top: 20px; +} + // 'Delete button' layout bug .submit-row a.deletelink { height: auto!important; From 19f67a177ca11321bf29a25131357f932fad3928 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Fri, 4 Aug 2023 13:57:28 -0400 Subject: [PATCH 065/164] cleanup --- src/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package-lock.json b/src/package-lock.json index ac7977ef2..dc1464ee8 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -12357,4 +12357,4 @@ } } } -} \ No newline at end of file +} From f4cea63e96115e08977c86615d6672d7c1cce574 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Fri, 4 Aug 2023 16:35:08 -0400 Subject: [PATCH 066/164] Add Paul to admin and staff --- src/registrar/fixtures.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index 9ca20b2a0..01de666a4 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -62,6 +62,11 @@ class UserFixture: "first_name": "Zander", "last_name": "Adkinson", }, + { + "username": "bb21f687-c773-4df3-9243-111cfd4c0be4", + "first_name": "Paul", + "last_name": "Kuykendall", + }, ] STAFF = [ @@ -80,6 +85,11 @@ class UserFixture: "first_name": "Zander-Analyst", "last_name": "Adkinson-Analyst", }, + { + "username": "57ab5847-7789-49fe-a2f9-21d38076d699", + "first_name": "Paul-Analyst", + "last_name": "Kuykendall-Analyst", + }, ] STAFF_PERMISSIONS = [ From e69b800f59dd4382bf0431c1717929098779b7d6 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 7 Aug 2023 08:47:52 -0700 Subject: [PATCH 067/164] Add new developer sandbox 'rh' infrastructure --- .github/workflows/migrate.yaml | 1 + .github/workflows/reset-db.yaml | 1 + ops/manifests/manifest-rh.yaml | 29 +++++++++++++++++++++++++++++ src/registrar/config/settings.py | 1 + 4 files changed, 32 insertions(+) create mode 100644 ops/manifests/manifest-rh.yaml diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index 28447a605..dcd7eee66 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -15,6 +15,7 @@ on: options: - stable - staging + - rh - gd - rb - ko diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index 0d3ed4934..e5af08808 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -16,6 +16,7 @@ on: options: - stable - staging + - rh - gd - rb - ko diff --git a/ops/manifests/manifest-rh.yaml b/ops/manifests/manifest-rh.yaml new file mode 100644 index 000000000..d8bf4cb77 --- /dev/null +++ b/ops/manifests/manifest-rh.yaml @@ -0,0 +1,29 @@ +--- +applications: +- name: getgov-rh + buildpacks: + - python_buildpack + path: ../../src + instances: 1 + memory: 512M + stack: cflinuxfs4 + timeout: 180 + command: ./run.sh + health-check-type: http + health-check-http-endpoint: /health + env: + # Send stdout and stderr straight to the terminal without buffering + PYTHONUNBUFFERED: yup + # Tell Django where to find its configuration + DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: https://getgov-rh.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO + # default public site location + GETGOV_PUBLIC_SITE_URL: https://beta.get.gov + routes: + - route: getgov-rh.app.cloud.gov + services: + - getgov-credentials + - getgov-rh-database diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 90918c929..183245e55 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -571,6 +571,7 @@ SECURE_SSL_REDIRECT = True ALLOWED_HOSTS = [ "getgov-stable.app.cloud.gov", "getgov-staging.app.cloud.gov", + "getgov-rh.app.cloud.gov", "getgov-gd.app.cloud.gov", "getgov-rb.app.cloud.gov", "getgov-ko.app.cloud.gov", From ac75ac68822ab772a7baf2150ee62dc918955c8e Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 7 Aug 2023 08:52:53 -0700 Subject: [PATCH 068/164] added to deploy script --- .github/workflows/deploy-sandbox.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index 614484a2a..d7ca9d71a 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -15,6 +15,7 @@ jobs: || startsWith(github.head_ref, 'rb/') || startsWith(github.head_ref, 'ko/') || startsWith(github.head_ref, 'gd/') + || startsWith(github.head_ref, 'rh/') outputs: environment: ${{ steps.var.outputs.environment}} runs-on: "ubuntu-latest" From 5a7b7164b4ff56a1d8dbed9d03dc24ef12aa9e7f Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 7 Aug 2023 11:43:29 -0700 Subject: [PATCH 069/164] Add gpg install link and update assignees --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index 92ae9e3a1..2ea8bcb86 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -3,7 +3,7 @@ name: Developer Onboarding about: Onboarding steps for developers. title: 'Developer Onboarding: GH_HANDLE' labels: dev, onboarding -assignees: loganmeetsworld +assignees: abroddrick --- @@ -16,7 +16,7 @@ assignees: loganmeetsworld There are several tools we use locally that you will need to have. - [ ] [Install the cf CLI v7](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-mac) for the ability to deploy -- [ ] Make sure you have `gpg` >2.1.7. Run `gpg --version` to check. +- [ ] Make sure you have `gpg` >2.1.7. Run `gpg --version` to check. If not, [install gnupg](https://formulae.brew.sh/formula/gnupg) - [ ] Install the [Github CLI](https://cli.github.com/) ## Access From 92ed691eb42fd284152eb30dc2762b3abfdbcb32 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 7 Aug 2023 12:05:37 -0700 Subject: [PATCH 070/164] Update for shell rc file --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index 2ea8bcb86..11b2d86fc 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -80,6 +80,15 @@ You may need to add these two lines to your shell's rc file (e.g. `.bashrc` or ` GPG_TTY=$(tty) export GPG_TTY ``` +and then + +```bash +source ~/.bashrc +``` +or +```bash +source ~/.zshrc +``` ## Setting up developer sandbox From b8f01fcf05309800cd71e49c8359f461980bc476 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 7 Aug 2023 18:11:33 -0700 Subject: [PATCH 071/164] Add new developer sandbox 'nl' infrastructure --- .github/workflows/migrate.yaml | 1 + .github/workflows/reset-db.yaml | 1 + ops/manifests/manifest-nl.yaml | 29 +++++++++++++++++++++++++++++ src/registrar/config/settings.py | 1 + 4 files changed, 32 insertions(+) create mode 100644 ops/manifests/manifest-nl.yaml diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index 590c63f53..f98664f9d 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -15,6 +15,7 @@ on: options: - stable - staging + - nl - za - gd - rb diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index 4632b4c16..9bd420124 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -16,6 +16,7 @@ on: options: - stable - staging + - nl - za - gd - rb diff --git a/ops/manifests/manifest-nl.yaml b/ops/manifests/manifest-nl.yaml new file mode 100644 index 000000000..dcdb02794 --- /dev/null +++ b/ops/manifests/manifest-nl.yaml @@ -0,0 +1,29 @@ +--- +applications: +- name: getgov-nl + buildpacks: + - python_buildpack + path: ../../src + instances: 1 + memory: 512M + stack: cflinuxfs4 + timeout: 180 + command: ./run.sh + health-check-type: http + health-check-http-endpoint: /health + env: + # Send stdout and stderr straight to the terminal without buffering + PYTHONUNBUFFERED: yup + # Tell Django where to find its configuration + DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: https://getgov-nl.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO + # default public site location + GETGOV_PUBLIC_SITE_URL: https://beta.get.gov + routes: + - route: getgov-nl.app.cloud.gov + services: + - getgov-credentials + - getgov-nl-database diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 60e7909e4..8a854f63f 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -571,6 +571,7 @@ SECURE_SSL_REDIRECT = True ALLOWED_HOSTS = [ "getgov-stable.app.cloud.gov", "getgov-staging.app.cloud.gov", + "getgov-nl.app.cloud.gov", "getgov-za.app.cloud.gov", "getgov-gd.app.cloud.gov", "getgov-rb.app.cloud.gov", From 861c14322dda3164291385a1f56574ce82ae8f81 Mon Sep 17 00:00:00 2001 From: Gaby Disarli Date: Mon, 7 Aug 2023 20:47:23 -0500 Subject: [PATCH 072/164] updated copy on eleciton question and resolves 774 --- src/registrar/templates/application_org_election.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/application_org_election.html b/src/registrar/templates/application_org_election.html index d2432b79d..73237b96e 100644 --- a/src/registrar/templates/application_org_election.html +++ b/src/registrar/templates/application_org_election.html @@ -6,7 +6,11 @@ Is your organization an election office? * -

Answer “yes” if the primary purpose of your organization is to manage elections.

+

An election office is a government entity whose primary responsibility is overseeing elections and/or conducting voter registration. +
+
+ Answer “yes” only if the main purpose of your organization is to serve as an election office.

+ {% endblock %} {% block form_required_fields_help_text %} From a617da30200e2f625027217ecac651039f410177 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 8 Aug 2023 06:43:25 -0700 Subject: [PATCH 073/164] add nl to deploy script --- .github/workflows/deploy-sandbox.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index 222f76365..a62174345 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -16,6 +16,7 @@ jobs: || startsWith(github.head_ref, 'ko/') || startsWith(github.head_ref, 'gd/') || startsWith(github.head_ref, 'za/') + || startsWith(github.head_ref, 'nl/') outputs: environment: ${{ steps.var.outputs.environment}} runs-on: "ubuntu-latest" From e4cad0d2261aa720ad6caefb505a9fc2e3e15dba Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 8 Aug 2023 12:17:18 -0400 Subject: [PATCH 074/164] Table USWDS markup on change_list, after findings from 831 --- src/registrar/assets/sass/_theme/_admin.scss | 9 +++++++++ src/registrar/templates/admin/change_list_results.html | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index cb5b101de..4717e2103 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -125,6 +125,15 @@ table > caption > a { text-transform: none; } +.change-list { + .usa-table--striped tbody tr:nth-child(odd) td, + .usa-table--striped tbody tr:nth-child(odd) th, + .usa-table td, + .usa-table th { + background-color: transparent; + } +} + #nav-sidebar { padding-top: 20px; } diff --git a/src/registrar/templates/admin/change_list_results.html b/src/registrar/templates/admin/change_list_results.html index d5e1587d0..a9c5e5b4d 100644 --- a/src/registrar/templates/admin/change_list_results.html +++ b/src/registrar/templates/admin/change_list_results.html @@ -13,7 +13,7 @@ Load our custom filters to extract info from the django generated markup. {% endif %} {% if results %}
- +
From 95927753aa377e3baaad05d5a096baea1613250c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:42:08 -0600 Subject: [PATCH 075/164] Progress save Needs some code cleanup --- src/registrar/admin.py | 23 ++++++++++++++++++++++ src/registrar/models/domain_information.py | 3 --- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 182543c19..ee5195fc7 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -22,6 +22,29 @@ class AuditedAdmin(admin.ModelAdmin): object_id=object_id, ) ) + + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + """Used to sort dropdown fields alphabetically but can be expanded upon""" + # Determines what we want to sort by, ex: by name + order_by_list = [] + if db_field.name == "submitter" or db_field.name == "authorizing_official" or db_field.name == "creator" or db_field.name == "investigator": + order_by_list = ['first_name', 'last_name'] + elif db_field.name == "requested_domain": + order_by_list = ['name'] + + return self.formfield_order_helper(order_by_list, db_field, request, **kwargs) + + def formfield_order_helper(self, order_by_list, db_field, request, **kwargs): + """A helper function to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" + formfield = super(AuditedAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) + # Only order if we choose to do so + if order_by_list: + formfield.queryset = formfield.queryset.order_by(*order_by_list) + + return formfield + + class ListHeaderAdmin(AuditedAdmin): diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index b12039e73..084bd0515 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -244,6 +244,3 @@ class DomainInformation(TimeStampedModel): domain_info.domain = domain domain_info.save() return domain_info - - class Meta: - verbose_name_plural = "Domain Information" From 86acb6c24ed4ef927912bd2c8f6b60fad04eab79 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 8 Aug 2023 17:58:24 -0400 Subject: [PATCH 076/164] fix datat-theme light when toggled from datk mode --- src/registrar/assets/sass/_theme/_admin.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 4717e2103..2bd330c7c 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -5,7 +5,8 @@ // and assign USWDS theme vars whenever possible // If needed (see below), we'll use the USWDS hex value // As a last resort, we'll use CISA colors to supplement the palette -:root { +:root, +html[data-theme="light"] { --primary: #{$theme-color-primary}; --secondary: #{$theme-color-primary-darkest}; --accent: #{$theme-color-accent-cool}; From 904b826047ae5c408b278c4a1354550f09c293aa Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 8 Aug 2023 19:18:48 -0400 Subject: [PATCH 077/164] fix tables in dark mode --- src/registrar/assets/sass/_theme/_admin.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 2bd330c7c..f6fcd99d4 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -104,10 +104,10 @@ html[data-theme="light"] { --close-button-hover-bg: #666666; } - // Fix dark mode bug - // The structure of our CSS messes with the cascade and causes some body copy in tables - //to have light mode colors in dark mode, resulting in a severe contrast issue. - body { + // Dark mode django (bug due to scss cascade) and USWDS tables + body, + .usa-table, + .usa-table--striped tbody tr:nth-child(odd) td { color: var(--body-fg)!important; } } From c49be334e11af4bd9da15cd1f681bbf98e469e85 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 8 Aug 2023 19:22:19 -0400 Subject: [PATCH 078/164] exclude non-admin tables from fix --- src/registrar/assets/sass/_theme/_admin.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index f6fcd99d4..8d2a914a6 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -106,8 +106,8 @@ html[data-theme="light"] { // Dark mode django (bug due to scss cascade) and USWDS tables body, - .usa-table, - .usa-table--striped tbody tr:nth-child(odd) td { + .change-list .usa-table, + .change-list .usa-table--striped tbody tr:nth-child(odd) td { color: var(--body-fg)!important; } } From e2ae261cfa53c42bfa2bb4b4dd9ac17a67c104fb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 9 Aug 2023 08:47:09 -0600 Subject: [PATCH 079/164] Formatting changes --- src/registrar/admin.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ee5195fc7..94d7a4108 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -22,17 +22,16 @@ class AuditedAdmin(admin.ModelAdmin): object_id=object_id, ) ) - def formfield_for_foreignkey(self, db_field, request, **kwargs): """Used to sort dropdown fields alphabetically but can be expanded upon""" # Determines what we want to sort by, ex: by name order_by_list = [] - if db_field.name == "submitter" or db_field.name == "authorizing_official" or db_field.name == "creator" or db_field.name == "investigator": + if db_field.name == "submitter" or db_field.name == "authorizing_official" or db_field.name == "creator": order_by_list = ['first_name', 'last_name'] elif db_field.name == "requested_domain": order_by_list = ['name'] - + return self.formfield_order_helper(order_by_list, db_field, request, **kwargs) def formfield_order_helper(self, order_by_list, db_field, request, **kwargs): @@ -42,9 +41,7 @@ class AuditedAdmin(admin.ModelAdmin): if order_by_list: formfield.queryset = formfield.queryset.order_by(*order_by_list) - return formfield - - + return formfield class ListHeaderAdmin(AuditedAdmin): From 8f2b7b7983ae82141b0764fcbb07188747b8f7de Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Wed, 9 Aug 2023 14:22:15 -0400 Subject: [PATCH 080/164] FF darkmode fix --- src/registrar/assets/sass/_theme/_admin.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 8d2a914a6..abb037ca0 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -112,6 +112,15 @@ html[data-theme="light"] { } } +// FF needs this to be specifically set +html[data-theme="dark"] { + body, + .change-list .usa-table, + .change-list .usa-table--striped tbody tr:nth-child(odd) td { + color: var(--body-fg)!important; + } +} + #branding h1 a:link, #branding h1 a:visited { color: var(--primary-fg); } From b8efec75c4e3f203843aa11dac5a28457ce712e4 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 9 Aug 2023 14:58:30 -0700 Subject: [PATCH 081/164] Adding myself to fixtures --- src/registrar/fixtures.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index 01de666a4..2c94a1eb4 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -67,6 +67,11 @@ class UserFixture: "first_name": "Paul", "last_name": "Kuykendall", }, + { + "username": "2a88a97b-be96-4aad-b99e-0b605b492c78", + "first_name": "Rebecca", + "last_name": "Hsieh", + }, ] STAFF = [ @@ -90,6 +95,11 @@ class UserFixture: "first_name": "Paul-Analyst", "last_name": "Kuykendall-Analyst", }, + { + "username": "e474e7a9-71ca-449d-833c-8a6e094dd117", + "first_name": "Rebecca-Analyst", + "last_name": "Hsieh-Analyst", + }, ] STAFF_PERMISSIONS = [ From f1f105a57804f612c6f86b3c47b178a47f0c7e53 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Wed, 9 Aug 2023 18:36:13 -0400 Subject: [PATCH 082/164] cleanup --- src/registrar/assets/sass/_theme/_admin.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index abb037ca0..4488ed398 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -112,7 +112,7 @@ html[data-theme="light"] { } } -// FF needs this to be specifically set +// Firefox needs this to be specifically set html[data-theme="dark"] { body, .change-list .usa-table, From 6a04704cfb4064305f8e60bfedd7295781b0d3ce Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 10 Aug 2023 09:10:30 -0600 Subject: [PATCH 083/164] (Somewhat) Generalized logic and expanded it to all dropdowns --- src/registrar/admin.py | 55 +++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 94d7a4108..d2073c516 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -8,9 +8,20 @@ from . import models logger = logging.getLogger(__name__) +# Used to keep track of how we want to order_by certain FKs +foreignkey_orderby_dict = { + # foreign_key # order_by + "submitter" : ['first_name', 'last_name'], + "authorizing_official" : ['first_name', 'last_name'], + "investigator" : ['first_name', 'last_name'], + "creator" : ['first_name', 'last_name'], + "user" : ['first_name', 'last_name'], + "domain" : ['name'], + "requested_domain" : ['name'], + "domain_application" : ['id'], +} class AuditedAdmin(admin.ModelAdmin): - """Custom admin to make auditing easier.""" def history_view(self, request, object_id, extra_context=None): @@ -22,26 +33,19 @@ class AuditedAdmin(admin.ModelAdmin): object_id=object_id, ) ) - + def formfield_for_foreignkey(self, db_field, request, **kwargs): """Used to sort dropdown fields alphabetically but can be expanded upon""" - # Determines what we want to sort by, ex: by name order_by_list = [] - if db_field.name == "submitter" or db_field.name == "authorizing_official" or db_field.name == "creator": - order_by_list = ['first_name', 'last_name'] - elif db_field.name == "requested_domain": - order_by_list = ['name'] - return self.formfield_order_helper(order_by_list, db_field, request, **kwargs) + # Determines what we want to sort by, ex: by name + if db_field.name in foreignkey_orderby_dict: + order_by_list = foreignkey_orderby_dict.get(db_field.name) + + form_field = super(AuditedAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) + return formfield_order_helper(form_field, order_by_list) - def formfield_order_helper(self, order_by_list, db_field, request, **kwargs): - """A helper function to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" - formfield = super(AuditedAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) - # Only order if we choose to do so - if order_by_list: - formfield.queryset = formfield.queryset.order_by(*order_by_list) - - return formfield + class ListHeaderAdmin(AuditedAdmin): @@ -182,12 +186,21 @@ class DomainAdmin(ListHeaderAdmin): class ContactAdmin(ListHeaderAdmin): - """Custom contact admin class to add search.""" search_fields = ["email", "first_name", "last_name"] search_help_text = "Search by firstname, lastname or email." + def formfield_for_foreignkey(self, db_field, request, **kwargs): + """Used to sort dropdown fields alphabetically but can be expanded upon""" + order_by_list = [] + # Determines what we want to sort by, ex: by name + if db_field.name in foreignkey_orderby_dict: + order_by_list = foreignkey_orderby_dict.get(db_field.name) + + form_field = super(ContactAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) + return formfield_order_helper(form_field, order_by_list) + class DomainApplicationAdmin(ListHeaderAdmin): @@ -324,6 +337,14 @@ class DomainApplicationAdmin(ListHeaderAdmin): return self.readonly_fields +def formfield_order_helper(form_field, order_by_list): + """A helper function to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" + # Only order if we choose to do so + if order_by_list: + form_field.queryset = form_field.queryset.order_by(*order_by_list) + + return form_field + admin.site.register(models.User, MyUserAdmin) admin.site.register(models.UserDomainRole, AuditedAdmin) admin.site.register(models.Contact, ContactAdmin) From 464533e112038cf3dcbdbec8b446531ef9b24c77 Mon Sep 17 00:00:00 2001 From: Gaby Disarli Date: Thu, 10 Aug 2023 12:31:34 -0500 Subject: [PATCH 084/164] fixed semantic tagging of paragraph and used proper tag --- src/registrar/templates/application_org_election.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/registrar/templates/application_org_election.html b/src/registrar/templates/application_org_election.html index 73237b96e..45776af5c 100644 --- a/src/registrar/templates/application_org_election.html +++ b/src/registrar/templates/application_org_election.html @@ -6,10 +6,9 @@ Is your organization an election office? * -

An election office is a government entity whose primary responsibility is overseeing elections and/or conducting voter registration. -
-
- Answer “yes” only if the main purpose of your organization is to serve as an election office.

+

An election office is a government entity whose primary responsibility is overseeing elections and/or conducting voter registration.

+ +

Answer “yes” only if the main purpose of your organization is to serve as an election office.

{% endblock %} From b156c86aef2bab1a518c0d54c249017ae1300e3a Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Thu, 10 Aug 2023 16:15:02 -0400 Subject: [PATCH 085/164] remove broad css selector for body color --- src/registrar/assets/sass/_theme/_admin.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 4488ed398..2e5da018e 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -105,7 +105,6 @@ html[data-theme="light"] { } // Dark mode django (bug due to scss cascade) and USWDS tables - body, .change-list .usa-table, .change-list .usa-table--striped tbody tr:nth-child(odd) td { color: var(--body-fg)!important; @@ -114,7 +113,6 @@ html[data-theme="light"] { // Firefox needs this to be specifically set html[data-theme="dark"] { - body, .change-list .usa-table, .change-list .usa-table--striped tbody tr:nth-child(odd) td { color: var(--body-fg)!important; From 88e1d419da6306753fc3a2698695b2a110e25a88 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Thu, 10 Aug 2023 16:27:00 -0400 Subject: [PATCH 086/164] Fix bug by checking to see if first item in the result set for a change_list_results table row is a form before performing the checkbox slice/markup switch --- .../templates/admin/change_list_results.html | 114 +++++++++++------- 1 file changed, 68 insertions(+), 46 deletions(-) diff --git a/src/registrar/templates/admin/change_list_results.html b/src/registrar/templates/admin/change_list_results.html index a9c5e5b4d..34f733ad4 100644 --- a/src/registrar/templates/admin/change_list_results.html +++ b/src/registrar/templates/admin/change_list_results.html @@ -13,57 +13,49 @@ Load our custom filters to extract info from the django generated markup. {% endif %} {% if results %}
-
+
-{# .gov - hardcode the select all checkbox #} - -{# .gov - don't let django generate the select all checkbox #} -{% for header in result_headers|slice:"1:" %} +{% if results.0.form %} + {# .gov - hardcode the select all checkbox #} + + {# .gov - don't let django generate the select all checkbox #} + {% for header in result_headers|slice:"1:" %} + {% endfor %} -{% endfor %} + + + - - - + {% comment %} + .gov - hardcode the row checkboxes using the custom filters to extract + the value attribute's value, and a label based on the anchor elements's + text. Then edit the for loop to keep django from generating the row select + checkboxes. + {% endcomment %} -{% comment %} -{% for result in results %} -{% if result.form.non_field_errors %} - -{% endif %} -{% for item in result %}{{ item }}{% endfor %} -{% endfor %} -{% endcomment %} - -{% comment %} -.gov - hardcode the row checkboxes using the custom filters to extract -the value attribute's value, and a label based on the anchor elements's -text. Then edit the for loop to keep django from generating the row select -checkboxes. -{% endcomment %} -{% for result in results %} + {% for result in results %} {% if result.form.non_field_errors %} {% endif %} @@ -82,7 +74,37 @@ checkboxes. {{ item }} {% endfor %} -{% endfor %} + {% endfor %} + +{% else %} + + {% for header in result_headers %} + {% endfor %} + + + + + + {% for result in results %} + {% if result.form.non_field_errors %} + + {% endif %} + {% for item in result %}{{ item }}{% endfor %} + {% endfor %} + +{% endif %}
-
- - - - -
-
-
+
+ + + + +
+
+
+ {% if header.sortable %} + {% if header.sort_priority > 0 %} +
+ + {% if num_sorted_fields > 1 %}{{ header.sort_priority }}{% endif %} + +
+ {% endif %} + {% endif %} +
{% if header.sortable %}{{ header.text|capfirst }}{% else %}{{ header.text|capfirst }}{% endif %}
+
+
- {% if header.sortable %} - {% if header.sort_priority > 0 %} -
- - {% if num_sorted_fields > 1 %}{{ header.sort_priority }}{% endif %} - -
- {% endif %} - {% endif %} -
{% if header.sortable %}{{ header.text|capfirst }}{% else %}{{ header.text|capfirst }}{% endif %}
-
-
{{ result.form.non_field_errors }}
{{ result.form.non_field_errors }}
+ {% if header.sortable %} + {% if header.sort_priority > 0 %} +
+ + {% if num_sorted_fields > 1 %}{{ header.sort_priority }}{% endif %} + +
+ {% endif %} + {% endif %} +
{% if header.sortable %}{{ header.text|capfirst }}{% else %}{{ header.text|capfirst }}{% endif %}
+
+
{{ result.form.non_field_errors }}
From 94738ca06f100bd0b870d9070ef52a11f0302905 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 10 Aug 2023 14:56:16 -0600 Subject: [PATCH 087/164] Moved sort logic in a class / generalized --- src/registrar/admin.py | 46 ++++++------------- .../models/utility/admin_form_order_helper.py | 41 +++++++++++++++++ 2 files changed, 54 insertions(+), 33 deletions(-) create mode 100644 src/registrar/models/utility/admin_form_order_helper.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index d2073c516..3d568f441 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -4,22 +4,19 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect from django.urls import reverse + +from registrar.models.utility.admin_form_order_helper import AdminFormOrderHelper, SortingDictInterface from . import models logger = logging.getLogger(__name__) # Used to keep track of how we want to order_by certain FKs -foreignkey_orderby_dict = { - # foreign_key # order_by - "submitter" : ['first_name', 'last_name'], - "authorizing_official" : ['first_name', 'last_name'], - "investigator" : ['first_name', 'last_name'], - "creator" : ['first_name', 'last_name'], - "user" : ['first_name', 'last_name'], - "domain" : ['name'], - "requested_domain" : ['name'], - "domain_application" : ['id'], -} +foreignkey_orderby_dict: [SortingDictInterface] = [ + #foreign_key - order_by + SortingDictInterface(["submitter", "authorizing_official", "investigator", "creator", "user"], ['first_name', 'last_name']).sorting_dict, + SortingDictInterface(["domain", "requested_domain"], ["name"]).sorting_dict, + SortingDictInterface(["domain_application"], ['id']).sorting_dict +] class AuditedAdmin(admin.ModelAdmin): """Custom admin to make auditing easier.""" @@ -36,16 +33,9 @@ class AuditedAdmin(admin.ModelAdmin): def formfield_for_foreignkey(self, db_field, request, **kwargs): """Used to sort dropdown fields alphabetically but can be expanded upon""" - order_by_list = [] - - # Determines what we want to sort by, ex: by name - if db_field.name in foreignkey_orderby_dict: - order_by_list = foreignkey_orderby_dict.get(db_field.name) - form_field = super(AuditedAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) - return formfield_order_helper(form_field, order_by_list) + return form_field_order_helper(form_field, db_field) - class ListHeaderAdmin(AuditedAdmin): @@ -193,13 +183,8 @@ class ContactAdmin(ListHeaderAdmin): def formfield_for_foreignkey(self, db_field, request, **kwargs): """Used to sort dropdown fields alphabetically but can be expanded upon""" - order_by_list = [] - # Determines what we want to sort by, ex: by name - if db_field.name in foreignkey_orderby_dict: - order_by_list = foreignkey_orderby_dict.get(db_field.name) - form_field = super(ContactAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) - return formfield_order_helper(form_field, order_by_list) + return form_field_order_helper(form_field, db_field) class DomainApplicationAdmin(ListHeaderAdmin): @@ -336,14 +321,9 @@ class DomainApplicationAdmin(ListHeaderAdmin): # Regular users can only view the specified fields return self.readonly_fields - -def formfield_order_helper(form_field, order_by_list): - """A helper function to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" - # Only order if we choose to do so - if order_by_list: - form_field.queryset = form_field.queryset.order_by(*order_by_list) - - return form_field +def form_field_order_helper(form_field, db_field): + form = AdminFormOrderHelper(foreignkey_orderby_dict) + return form.get_ordered_form_field(form_field, db_field) admin.site.register(models.User, MyUserAdmin) admin.site.register(models.UserDomainRole, AuditedAdmin) diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py new file mode 100644 index 000000000..4ce56f97b --- /dev/null +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -0,0 +1,41 @@ +import logging +from django.forms import ModelChoiceField +logger = logging.getLogger(__name__) +class SortingDictInterface: + _model_list = {} + _sort_list = [] + sorting_dict = {} + + def __init__(self, model_list, sort_list): + self.sorting_dict = { + "dropDownSelected": model_list, + "sortBy": sort_list + } + + +class AdminFormOrderHelper(): + """A helper class to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" + # Used to keep track of how we want to order_by certain FKs + _sorting_dict: [SortingDictInterface] = [] + + def __init__(self, sort): + self._sorting_dict = sort + + def get_ordered_form_field(self, form_field, db_field) -> (ModelChoiceField | None): + """Orders the queryset for a ModelChoiceField based on the order_by_dict dictionary""" + _order_by_list = [] + + for item in self._sorting_dict: + drop_down_selected = item.get("dropDownSelected") + sort_by = item.get("sortBy") + if db_field.name in drop_down_selected: + _order_by_list = sort_by + break + + # Only order if we choose to do so + if _order_by_list: + form_field.queryset = form_field.queryset.order_by(*_order_by_list) + + return form_field + + From 5e64848a2ecb63bd01d071f2c41df8c04618213e Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 11 Aug 2023 00:51:44 -0400 Subject: [PATCH 088/164] Update developer-onboarding.md Small edits, add link to team onboarding doc, replacing an legacy 18F charter --- .github/ISSUE_TEMPLATE/developer-onboarding.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index 11b2d86fc..8d0f9c2d8 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -24,13 +24,13 @@ There are several tools we use locally that you will need to have. ### Steps for the onboardee - [ ] Setup [commit signing in Github](#setting-up-commit-signing) and with git locally. - [ ] [Create a cloud.gov account](https://cloud.gov/docs/getting-started/accounts/) -- [ ] Have an admin add you to the CISA Github organization and Dotgov Team. +- [ ] Email github@cisa.dhs.gov (cc: Cameron) to add you to the [CISA Github organization](https://github.com/getgov) and [.gov Team](https://github.com/orgs/cisagov/teams/gov). - [ ] Ensure you can login to your cloud.gov account via the CLI ```bash cf login -a api.fr.cloud.gov --sso ``` - [ ] Have an admin add you to cloud.gov org and set up your [sandbox developer space](#setting-up-developer-sandbox). Ensure you can deploy to your sandbox space. -- [ ] Have an admin add you to our login.gov sandbox team (`.gov registrar poc`) via the [dashboard](https://dashboard.int.identitysandbox.gov/). +- [ ] Have an admin add you to our login.gov sandbox team (`.gov Registrar`) via the [dashboard](https://dashboard.int.identitysandbox.gov/). **Note:** As mentioned in the [Login documentation](https://developers.login.gov/testing/), the sandbox Login account is different account from your regular, production Login account. If you have not created a Login account for the sandbox before, you will need to create a new account first. @@ -39,12 +39,12 @@ cf login -a api.fr.cloud.gov --sso ### Steps for the onboarder - [ ] Add the onboardee to cloud.gov org (cisa-getgov-prototyping) - [ ] Setup a [developer specific space for the new developer](#setting-up-developer-sandbox) -- [ ] Add the onboardee to our login.gov sandbox team (`.gov registrar poc`) via the [dashboard](https://dashboard.int.identitysandbox.gov/) +- [ ] Add the onboardee to our login.gov sandbox team (`.gov Registrar`) via the [dashboard](https://dashboard.int.identitysandbox.gov/) ## Documents to Review -- [ ] [Team Charter](https://docs.google.com/document/d/1xhMKlW8bMcxyF7ipsOYxw1SQYVi-lWPkcDHSUS6miNg/edit), in particular our Github Policy +- [ ] [Team Onboarding](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?usp=sharing) - [ ] [Architecture Decision Records](https://github.com/cisagov/dotgov/tree/main/docs/architecture/decisions) - [ ] [Contributing Policy](https://github.com/cisagov/dotgov/tree/main/CONTRIBUTING.md) From a2f0fd9522d3f039e73f0dd0317ef8b7be41ecb1 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Fri, 11 Aug 2023 12:18:24 -0400 Subject: [PATCH 089/164] revert USDWDS table markup which was removed by mistake --- src/registrar/templates/admin/change_list_results.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/admin/change_list_results.html b/src/registrar/templates/admin/change_list_results.html index 34f733ad4..fb77bd785 100644 --- a/src/registrar/templates/admin/change_list_results.html +++ b/src/registrar/templates/admin/change_list_results.html @@ -13,7 +13,7 @@ Load our custom filters to extract info from the django generated markup. {% endif %} {% if results %}
- +
From 02556418f882b4a45b18547d0354e580b18e4a62 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Fri, 11 Aug 2023 12:24:06 -0400 Subject: [PATCH 090/164] explain the 'results doesn't have a form as its first element', which is far away from its if --- src/registrar/templates/admin/change_list_results.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/admin/change_list_results.html b/src/registrar/templates/admin/change_list_results.html index fb77bd785..9ee3f9f59 100644 --- a/src/registrar/templates/admin/change_list_results.html +++ b/src/registrar/templates/admin/change_list_results.html @@ -76,7 +76,7 @@ Load our custom filters to extract info from the django generated markup. {% endfor %} -{% else %} +{% else %} {# results doesn't have a form as its first element #} {% for header in result_headers %}
From d99260f70af0e6ba6c1885bf93f5fb91070e4635 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:41:45 -0600 Subject: [PATCH 091/164] Uncommited changes for tests Test cases still a WIP - unsure as to why these two querysets are ordered differently. Otherwise the sorting logic is there --- src/registrar/admin.py | 3 +- src/registrar/tests/common.py | 78 ++++++++++++++++++++++++++++++- src/registrar/tests/test_admin.py | 49 +++++++++++++++++-- 3 files changed, 123 insertions(+), 7 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 3d568f441..34a14486f 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -14,8 +14,7 @@ logger = logging.getLogger(__name__) foreignkey_orderby_dict: [SortingDictInterface] = [ #foreign_key - order_by SortingDictInterface(["submitter", "authorizing_official", "investigator", "creator", "user"], ['first_name', 'last_name']).sorting_dict, - SortingDictInterface(["domain", "requested_domain"], ["name"]).sorting_dict, - SortingDictInterface(["domain_application"], ['id']).sorting_dict + SortingDictInterface(["domain", "requested_domain"], ["name"]).sorting_dict ] class AuditedAdmin(admin.ModelAdmin): diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 4359fc454..a8ee22a03 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -2,6 +2,8 @@ import os import logging from contextlib import contextmanager +import random +from string import ascii_uppercase from unittest.mock import Mock from typing import List, Dict @@ -149,8 +151,8 @@ def completed_application( phone="(555) 555 5556", ) other, _ = Contact.objects.get_or_create( - first_name="Testy2", - last_name="Tester2", + first_name="Testy", + last_name="Tester", title="Another Tester", email="testy2@town.com", phone="(555) 555 5557", @@ -188,3 +190,75 @@ def completed_application( application.alternative_domains.add(alt) return application + +def multiple_completed_applications(has_other_contacts=True, + has_current_website=True, + has_alternative_gov_domain=True, + has_type_of_work=True, + has_anything_else=True, + status=DomainApplication.STARTED, + user=False,): + applications = [] + list_of_letters = list(ascii_uppercase) + random.shuffle(list_of_letters) + for x in list_of_letters: + if not user: + user = get_user_model().objects.create(username="username{}".format(x)) + ao, _ = Contact.objects.get_or_create( + first_name="{} Testy".format(x), + last_name="{} Tester".format(x), + title="{} Chief Tester".format(x), + email="testy@town.com", + phone="(555) 555 5555", + ) + domain, _ = DraftDomain.objects.get_or_create(name="city{}.gov".format(x)) + alt, _ = Website.objects.get_or_create(website="cityalt{}.gov".format(x)) + current, _ = Website.objects.get_or_create(website="city{}.com".format(x)) + you, _ = Contact.objects.get_or_create( + first_name="{} Testy you".format(x), + last_name="{} Tester you".format(x), + title="{} Admin Tester".format(x), + email="mayor@igorville.gov", + phone="(555) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="{} Testy".format(x), + last_name="{} Tester".format(x), + title="{} Another Tester".format(x), + email="{}testy2@town.com".format(x), + phone="(555) 555 5557", + ) + domain_application_kwargs = dict( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + is_policy_acknowledged=True, + organization_name="{}Testorg".format(x), + address_line1="address 1", + address_line2="address 2", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + requested_domain=domain, + submitter=you, + creator=user, + status=status, + ) + if has_type_of_work: + domain_application_kwargs["type_of_work"] = "e-Government" + if has_anything_else: + domain_application_kwargs["anything_else"] = "There is more" + + application, _ = DomainApplication.objects.get_or_create( + **domain_application_kwargs + ) + + if has_other_contacts: + application.other_contacts.add(other) + if has_current_website: + application.current_websites.add(current) + if has_alternative_gov_domain: + application.alternative_domains.add(alt) + applications.append(application) + + return applications \ No newline at end of file diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 3c07e7608..54b98fda8 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1,15 +1,17 @@ from django.test import TestCase, RequestFactory, Client from django.contrib.admin.sites import AdminSite -from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin, MyUserAdmin +from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin, MyUserAdmin, AuditedAdmin from registrar.models import DomainApplication, DomainInformation, User -from .common import completed_application, mock_user, create_superuser, create_user +from registrar.models.contact import Contact +from .common import completed_application, mock_user, create_superuser, create_user, multiple_completed_applications from django.contrib.auth import get_user_model from django.conf import settings from unittest.mock import MagicMock import boto3_mocking # type: ignore +import logging - +logger = logging.getLogger(__name__) class TestDomainApplicationAdmin(TestCase): def setUp(self): self.site = AdminSite() @@ -367,3 +369,44 @@ class MyUserAdminTest(TestCase): def tearDown(self): User.objects.all().delete() + +class AuditedAdminTest(TestCase): + def setUp(self): + self.site = AdminSite() + self.factory = RequestFactory() + self.client = Client(HTTP_HOST="localhost:8080") + self.superuser = create_superuser() + self.factory.post + + def test_alphabetically_sorted_fk_fields(self): + mock_client = MagicMock() + + #tested_fields = [{"name": "submitter"}, {"name": "authorizing_official"}, {"name": "investigator"}, {"name": "creator"}, {"name": "user"}] + tested_fields = [DomainApplication.authorizing_official.field, DomainApplication.submitter.field, DomainApplication.investigator.field, DomainApplication.creator.field] + with boto3_mocking.clients.handler_for("sesv2", mock_client): + # Create a sample application - review status does not matter + applications = multiple_completed_applications(status=DomainApplication.IN_REVIEW) + # Create a mock request + request = self.factory.post( + "/admin/registrar/domainapplication/{}/change/".format(applications[0].pk) + ) + + model_admin = AuditedAdmin(DomainApplication, self.site) + + for field in tested_fields: + desired_order = model_admin.get_queryset(request).order_by("{}__first_name".format(field.name)) + current_sort_order = model_admin.formfield_for_foreignkey(field, request).queryset + + self.assertEqual(desired_order, current_sort_order, "{} is not ordered alphabetically".format(field.name)) + # Is initalized in alphabetical order + + + for x in model_admin.get_queryset(request).all(): + logger.debug(x.authorizing_official) + + + def tearDown(self): + DomainInformation.objects.all().delete() + DomainApplication.objects.all().delete() + User.objects.all().delete() + self.superuser.delete() From 9df58515e38929ec860a8356f4ee27b4c1b1a0ad Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:42:16 -0600 Subject: [PATCH 092/164] Update test_admin.py --- src/registrar/tests/test_admin.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 54b98fda8..f9ab53863 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -398,11 +398,6 @@ class AuditedAdminTest(TestCase): current_sort_order = model_admin.formfield_for_foreignkey(field, request).queryset self.assertEqual(desired_order, current_sort_order, "{} is not ordered alphabetically".format(field.name)) - # Is initalized in alphabetical order - - - for x in model_admin.get_queryset(request).all(): - logger.debug(x.authorizing_official) def tearDown(self): From 9c26a971632c3d8ace2f0d9cbc0a59f9a5e78ffc Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 11 Aug 2023 12:12:37 -0700 Subject: [PATCH 093/164] Add troubleshooting notes for m1 puppeteer issue --- docs/developer/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/developer/README.md b/docs/developer/README.md index b6938298c..f048f99cf 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -18,9 +18,14 @@ If you're new to Django, see [Getting Started with Django](https://www.djangopro Visit the running application at [http://localhost:8080](http://localhost:8080). -Note: If you are using Windows, you may need to change your [line endings](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings). If not, you may not be able to run manage.py. +**Note:** If you are using Windows, you may need to change your [line endings](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings). If not, you may not be able to run manage.py. * Unix based operating systems (like macOS or Linux) handle line separators [differently than Windows does](https://superuser.com/questions/374028/how-are-n-and-r-handled-differently-on-linux-and-windows). This can break bash scripts in particular. In the case of manage.py, it uses *#!/usr/bin/env python* to access the Python executable. Since the script is still thinking in terms of unix line seperators, it may look for the executable *python\r* rather than *python* (since Windows cannot read the carriage return on its own) - thus leading to the error `usr/bin/env: 'python\r' no such file or directory` * If you'd rather not change this globally, add a `.gitattributes` file in the project root with `* text eol=lf` as the text content, and [refresh the repo](https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings#refreshing-a-repository-after-changing-line-endings) +**Note:** If you are using a Mac with a M1 chip, and see this error `The chromium binary is not available for arm64.` or an error invovling `puppeteer`, try adding this line below into your `.bashrc` or `.zshrc` + +``` +export DOCKER_DEFAULT_PLATFORM=linux/amd64 +``` ## Branch Conventions From 5f6a7cd044dc0d4b4d7c9feae4e74dfb306f2b39 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Sun, 13 Aug 2023 19:40:30 -0600 Subject: [PATCH 094/164] Fixes the logout bug It works! --- src/registrar/config/settings.py | 2 +- src/registrar/config/urls.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index f6873b226..f16efc18f 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -285,7 +285,7 @@ SERVER_EMAIL = "root@get.gov" # Content-Security-Policy configuration # this can be restrictive because we have few external scripts -allowed_sources = ("'self'",) +allowed_sources = ("'self'", "https://idp.int.identitysandbox.gov", "https://idp.int.identitysandbox.gov/openid_connect/logout") CSP_DEFAULT_SRC = allowed_sources # Most things fall back to default-src, but these two do not and should be # explicitly set diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index c21d0206c..6159b387b 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -45,6 +45,10 @@ for step, view in [ urlpatterns = [ path("", views.index, name="home"), + path( + "admin/logout/", + RedirectView.as_view(url="/openid/logout", permanent=False), + ), path("admin/", admin.site.urls), path( "application//edit/", From c7cba16e3c252981887412a459b09bba132a8a43 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Sun, 13 Aug 2023 21:34:00 -0600 Subject: [PATCH 095/164] Test case --- src/registrar/tests/common.py | 28 +++++++---- src/registrar/tests/test_admin.py | 82 ++++++++++++++++++++++--------- 2 files changed, 76 insertions(+), 34 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index a8ee22a03..def9793f3 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -191,7 +191,7 @@ def completed_application( return application -def multiple_completed_applications(has_other_contacts=True, +def multiple_completed_applications_for_alphabetical_test(has_other_contacts=True, has_current_website=True, has_alternative_gov_domain=True, has_type_of_work=True, @@ -202,11 +202,14 @@ def multiple_completed_applications(has_other_contacts=True, list_of_letters = list(ascii_uppercase) random.shuffle(list_of_letters) for x in list_of_letters: - if not user: - user = get_user_model().objects.create(username="username{}".format(x)) + user = get_user_model().objects.create( + first_name="{} First:cre".format(x), + last_name="{} Last:cre".format(x), + username="{} username:cre".format(x) + ) ao, _ = Contact.objects.get_or_create( - first_name="{} Testy".format(x), - last_name="{} Tester".format(x), + first_name="{} First:ao".format(x), + last_name="{} Last:ao".format(x), title="{} Chief Tester".format(x), email="testy@town.com", phone="(555) 555 5555", @@ -215,19 +218,24 @@ def multiple_completed_applications(has_other_contacts=True, alt, _ = Website.objects.get_or_create(website="cityalt{}.gov".format(x)) current, _ = Website.objects.get_or_create(website="city{}.com".format(x)) you, _ = Contact.objects.get_or_create( - first_name="{} Testy you".format(x), - last_name="{} Tester you".format(x), + first_name="{} First:you".format(x), + last_name="{} Last:you".format(x), title="{} Admin Tester".format(x), email="mayor@igorville.gov", phone="(555) 555 5556", ) other, _ = Contact.objects.get_or_create( - first_name="{} Testy".format(x), - last_name="{} Tester".format(x), + first_name="{} First:other".format(x), + last_name="{} Last:other".format(x), title="{} Another Tester".format(x), email="{}testy2@town.com".format(x), phone="(555) 555 5557", ) + inv, _ = User.objects.get_or_create( + first_name="{} First:inv".format(x), + last_name="{} Last:inv".format(x), + username="{} username:inv".format(x) + ) domain_application_kwargs = dict( organization_type="federal", federal_type="executive", @@ -243,6 +251,7 @@ def multiple_completed_applications(has_other_contacts=True, submitter=you, creator=user, status=status, + investigator=inv ) if has_type_of_work: domain_application_kwargs["type_of_work"] = "e-Government" @@ -260,5 +269,4 @@ def multiple_completed_applications(has_other_contacts=True, if has_alternative_gov_domain: application.alternative_domains.add(alt) applications.append(application) - return applications \ No newline at end of file diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f9ab53863..f971ed6c0 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -3,7 +3,7 @@ from django.contrib.admin.sites import AdminSite from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin, MyUserAdmin, AuditedAdmin from registrar.models import DomainApplication, DomainInformation, User from registrar.models.contact import Contact -from .common import completed_application, mock_user, create_superuser, create_user, multiple_completed_applications +from .common import completed_application, mock_user, create_superuser, create_user, multiple_completed_applications_for_alphabetical_test from django.contrib.auth import get_user_model from django.conf import settings @@ -375,33 +375,67 @@ class AuditedAdminTest(TestCase): self.site = AdminSite() self.factory = RequestFactory() self.client = Client(HTTP_HOST="localhost:8080") - self.superuser = create_superuser() - self.factory.post - def test_alphabetically_sorted_fk_fields(self): - mock_client = MagicMock() - - #tested_fields = [{"name": "submitter"}, {"name": "authorizing_official"}, {"name": "investigator"}, {"name": "creator"}, {"name": "user"}] + def test_alphabetically_sorted_fk_fields_domain_application(self): tested_fields = [DomainApplication.authorizing_official.field, DomainApplication.submitter.field, DomainApplication.investigator.field, DomainApplication.creator.field] - with boto3_mocking.clients.handler_for("sesv2", mock_client): - # Create a sample application - review status does not matter - applications = multiple_completed_applications(status=DomainApplication.IN_REVIEW) - # Create a mock request - request = self.factory.post( - "/admin/registrar/domainapplication/{}/change/".format(applications[0].pk) - ) - - model_admin = AuditedAdmin(DomainApplication, self.site) - - for field in tested_fields: - desired_order = model_admin.get_queryset(request).order_by("{}__first_name".format(field.name)) - current_sort_order = model_admin.formfield_for_foreignkey(field, request).queryset - self.assertEqual(desired_order, current_sort_order, "{} is not ordered alphabetically".format(field.name)) - + # Create a sample application - review status does not matter + applications = multiple_completed_applications_for_alphabetical_test(status=DomainApplication.IN_REVIEW) + # Create a mock request + request = self.factory.post( + "/admin/registrar/domainapplication/{}/change/".format(applications[0].pk) + ) + + model_admin = AuditedAdmin(DomainApplication, self.site) + + # Typically we wouldnt want two nested for fields, but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + first_name_field = "{}__first_name".format(field.name) + last_name_field = "{}__last_name".format(field.name) + + desired_order = list(model_admin.get_queryset(request).order_by( + first_name_field, last_name_field).values_list(first_name_field, last_name_field)) + logger.debug(desired_order) + current_sort_order: Contact = list(model_admin.formfield_for_foreignkey(field, request).queryset) + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for contact in current_sort_order: + first_name = contact.first_name + last_name = contact.last_name + + match field.name: + case DomainApplication.authorizing_official.field.name: + name_tuple = self.coerced_fk_field_helper(first_name, last_name, 'ao', ':') + if name_tuple: + current_sort_order_coerced_type.append((first_name, last_name)) + case DomainApplication.submitter.field.name: + name_tuple = self.coerced_fk_field_helper(first_name, last_name, 'you', ':') + if name_tuple: + current_sort_order_coerced_type.append((first_name, last_name)) + case DomainApplication.investigator.field.name: + name_tuple = self.coerced_fk_field_helper(first_name, last_name, 'inv', ':') + if name_tuple: + current_sort_order_coerced_type.append((first_name, last_name)) + case DomainApplication.creator.field.name: + name_tuple = self.coerced_fk_field_helper(first_name, last_name, 'cre', ':') + if name_tuple: + current_sort_order_coerced_type.append((first_name, last_name)) + logger.debug("current: {}".format(current_sort_order_coerced_type)) + + self.assertEqual(desired_order, current_sort_order_coerced_type, "{} is not ordered alphabetically".format(field.name)) + + # I originally spent some time trying to fully generalize this to replace the match/arg fields, + # but I think for this specific use-case its not necessary since it'll only be used here + def coerced_fk_field_helper(self, first_name, last_name, field_name, queryset_shorthand): + if(first_name and first_name.split(queryset_shorthand)[1] == field_name): + return (first_name, last_name) + else: + return None + def tearDown(self): DomainInformation.objects.all().delete() DomainApplication.objects.all().delete() - User.objects.all().delete() - self.superuser.delete() From 0b2faac625afa5a30a981b3878b9f9c3341eddb0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Sun, 13 Aug 2023 21:34:42 -0600 Subject: [PATCH 096/164] Removed logger.debug --- src/registrar/tests/test_admin.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f971ed6c0..79492b01e 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -395,10 +395,9 @@ class AuditedAdminTest(TestCase): first_name_field = "{}__first_name".format(field.name) last_name_field = "{}__last_name".format(field.name) - desired_order = list(model_admin.get_queryset(request).order_by( - first_name_field, last_name_field).values_list(first_name_field, last_name_field)) - logger.debug(desired_order) + desired_order = list(model_admin.get_queryset(request).order_by(first_name_field, last_name_field).values_list(first_name_field, last_name_field)) current_sort_order: Contact = list(model_admin.formfield_for_foreignkey(field, request).queryset) + current_sort_order_coerced_type = [] # This is necessary as .queryset and get_queryset return lists of different types/structures. @@ -424,7 +423,6 @@ class AuditedAdminTest(TestCase): name_tuple = self.coerced_fk_field_helper(first_name, last_name, 'cre', ':') if name_tuple: current_sort_order_coerced_type.append((first_name, last_name)) - logger.debug("current: {}".format(current_sort_order_coerced_type)) self.assertEqual(desired_order, current_sort_order_coerced_type, "{} is not ordered alphabetically".format(field.name)) From 4fa9e786886342ae14b8a2c5fa0d1102aa6a32db Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 14 Aug 2023 08:59:22 -0600 Subject: [PATCH 097/164] Battle of the linter, circa 2023 --- src/registrar/admin.py | 35 +++++--- .../models/utility/admin_form_order_helper.py | 23 ++--- src/registrar/tests/common.py | 15 ++-- src/registrar/tests/test_admin.py | 88 +++++++++++-------- 4 files changed, 98 insertions(+), 63 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 34a14486f..9729e018c 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -4,19 +4,31 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect from django.urls import reverse - -from registrar.models.utility.admin_form_order_helper import AdminFormOrderHelper, SortingDictInterface +from registrar.models.utility.admin_form_order_helper import AdminFormOrderHelper, SortingDictInterface # noqa from . import models logger = logging.getLogger(__name__) +# The linter does not like the length of SortingDictInterface, so these are split here +audited_admin_item_names = ["submitter", "authorizing_official", + "investigator", "creator", "user"] +audited_admin_orderby_names = ['first_name', 'last_name'] +contact_admin_item_names = ["domain", "requested_domain"] +contact_admin_orderby_names = ["name"] # Used to keep track of how we want to order_by certain FKs foreignkey_orderby_dict: [SortingDictInterface] = [ - #foreign_key - order_by - SortingDictInterface(["submitter", "authorizing_official", "investigator", "creator", "user"], ['first_name', 'last_name']).sorting_dict, - SortingDictInterface(["domain", "requested_domain"], ["name"]).sorting_dict + # foreign_key - order_by + SortingDictInterface( + audited_admin_item_names, + audited_admin_orderby_names + ).sorting_dict, + SortingDictInterface( + contact_admin_item_names, + contact_admin_orderby_names + ).sorting_dict ] + class AuditedAdmin(admin.ModelAdmin): """Custom admin to make auditing easier.""" @@ -29,16 +41,14 @@ class AuditedAdmin(admin.ModelAdmin): object_id=object_id, ) ) - + def formfield_for_foreignkey(self, db_field, request, **kwargs): """Used to sort dropdown fields alphabetically but can be expanded upon""" - form_field = super(AuditedAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) + form_field = super().formfield_for_foreignkey(db_field, request, **kwargs) return form_field_order_helper(form_field, db_field) - class ListHeaderAdmin(AuditedAdmin): - """Custom admin to add a descriptive subheader to list views.""" def changelist_view(self, request, extra_context=None): @@ -182,7 +192,7 @@ class ContactAdmin(ListHeaderAdmin): def formfield_for_foreignkey(self, db_field, request, **kwargs): """Used to sort dropdown fields alphabetically but can be expanded upon""" - form_field = super(ContactAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) + form_field = super().formfield_for_foreignkey(db_field, request, **kwargs) return form_field_order_helper(form_field, db_field) @@ -320,10 +330,15 @@ class DomainApplicationAdmin(ListHeaderAdmin): # Regular users can only view the specified fields return self.readonly_fields + +# For readability purposes, but can be replaced with a one liner def form_field_order_helper(form_field, db_field): + """A shorthand for AdminFormOrderHelper(foreignkey_orderby_dict).get_ordered_form_field(form_field, db_field)""" # noqa + form = AdminFormOrderHelper(foreignkey_orderby_dict) return form.get_ordered_form_field(form_field, db_field) + admin.site.register(models.User, MyUserAdmin) admin.site.register(models.UserDomainRole, AuditedAdmin) admin.site.register(models.Contact, ContactAdmin) diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py index 4ce56f97b..3004ec288 100644 --- a/src/registrar/models/utility/admin_form_order_helper.py +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -1,30 +1,35 @@ import logging +from typing import Dict from django.forms import ModelChoiceField + logger = logging.getLogger(__name__) + + class SortingDictInterface: - _model_list = {} - _sort_list = [] + _model_list: Dict[type, type] = {} + _sort_list: list[type] = [] sorting_dict = {} def __init__(self, model_list, sort_list): self.sorting_dict = { "dropDownSelected": model_list, - "sortBy": sort_list + "sortBy": sort_list } class AdminFormOrderHelper(): - """A helper class to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" + """A helper class to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" # noqa + # Used to keep track of how we want to order_by certain FKs - _sorting_dict: [SortingDictInterface] = [] + _sorting_dict: [SortingDictInterface] = [...] def __init__(self, sort): self._sorting_dict = sort - + def get_ordered_form_field(self, form_field, db_field) -> (ModelChoiceField | None): - """Orders the queryset for a ModelChoiceField based on the order_by_dict dictionary""" + """Orders the queryset for a ModelChoiceField based on the order_by_dict dictionary""" # noqa _order_by_list = [] - + for item in self._sorting_dict: drop_down_selected = item.get("dropDownSelected") sort_by = item.get("sortBy") @@ -37,5 +42,3 @@ class AdminFormOrderHelper(): form_field.queryset = form_field.queryset.order_by(*_order_by_list) return form_field - - diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index def9793f3..9597065b7 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -144,8 +144,8 @@ def completed_application( alt, _ = Website.objects.get_or_create(website="city1.gov") current, _ = Website.objects.get_or_create(website="city.com") you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", + first_name="Testy2", + last_name="Tester2", title="Admin Tester", email="mayor@igorville.gov", phone="(555) 555 5556", @@ -191,17 +191,20 @@ def completed_application( return application -def multiple_completed_applications_for_alphabetical_test(has_other_contacts=True, + +def multiple_unalphabetical_applications( + has_other_contacts=True, has_current_website=True, has_alternative_gov_domain=True, has_type_of_work=True, has_anything_else=True, status=DomainApplication.STARTED, - user=False,): + user=False, +): applications = [] list_of_letters = list(ascii_uppercase) random.shuffle(list_of_letters) - for x in list_of_letters: + for x in list_of_letters: user = get_user_model().objects.create( first_name="{} First:cre".format(x), last_name="{} Last:cre".format(x), @@ -269,4 +272,4 @@ def multiple_completed_applications_for_alphabetical_test(has_other_contacts=Tru if has_alternative_gov_domain: application.alternative_domains.add(alt) applications.append(application) - return applications \ No newline at end of file + return applications diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 79492b01e..6575f3a4b 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1,9 +1,9 @@ from django.test import TestCase, RequestFactory, Client from django.contrib.admin.sites import AdminSite -from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin, MyUserAdmin, AuditedAdmin +from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin, MyUserAdmin, AuditedAdmin # noqa from registrar.models import DomainApplication, DomainInformation, User from registrar.models.contact import Contact -from .common import completed_application, mock_user, create_superuser, create_user, multiple_completed_applications_for_alphabetical_test +from .common import completed_application, mock_user, create_superuser, create_user, multiple_unalphabetical_applications # noqa from django.contrib.auth import get_user_model from django.conf import settings @@ -12,6 +12,8 @@ import boto3_mocking # type: ignore import logging logger = logging.getLogger(__name__) + + class TestDomainApplicationAdmin(TestCase): def setUp(self): self.site = AdminSite() @@ -370,6 +372,7 @@ class MyUserAdminTest(TestCase): def tearDown(self): User.objects.all().delete() + class AuditedAdminTest(TestCase): def setUp(self): self.site = AdminSite() @@ -377,63 +380,74 @@ class AuditedAdminTest(TestCase): self.client = Client(HTTP_HOST="localhost:8080") def test_alphabetically_sorted_fk_fields_domain_application(self): - tested_fields = [DomainApplication.authorizing_official.field, DomainApplication.submitter.field, DomainApplication.investigator.field, DomainApplication.creator.field] + tested_fields = [ + # field (0) - field shorthand (1) + # the 'field shorthand' is used for type coercion. + # It is only used here to reduce boilerplate, + # but keep in mind that it is not needed outside this class. + (DomainApplication.authorizing_official.field, 'ao'), + (DomainApplication.submitter.field, 'you'), + (DomainApplication.investigator.field, 'inv'), + (DomainApplication.creator.field, 'cre') + ] # Create a sample application - review status does not matter - applications = multiple_completed_applications_for_alphabetical_test(status=DomainApplication.IN_REVIEW) + applications = multiple_unalphabetical_applications() # Create a mock request request = self.factory.post( "/admin/registrar/domainapplication/{}/change/".format(applications[0].pk) ) - + model_admin = AuditedAdmin(DomainApplication, self.site) - - # Typically we wouldnt want two nested for fields, but both fields are of a fixed length. + + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. # For test case purposes, this should be performant. for field in tested_fields: - first_name_field = "{}__first_name".format(field.name) - last_name_field = "{}__last_name".format(field.name) + field_name = field[0].name + first_name_field = "{}__first_name".format(field_name) + last_name_field = "{}__last_name".format(field_name) - desired_order = list(model_admin.get_queryset(request).order_by(first_name_field, last_name_field).values_list(first_name_field, last_name_field)) - current_sort_order: Contact = list(model_admin.formfield_for_foreignkey(field, request).queryset) + # We want both of these to be lists, as it is richer test wise. + # Not really a fan of how this looks, but as the linter demands... + desired_order = list( + model_admin.get_queryset(request).order_by( + first_name_field, last_name_field).values_list( + first_name_field, last_name_field) + ) + current_sort_order: Contact = list( + model_admin.formfield_for_foreignkey(field[0], request).queryset + ) + # Conforms to the same object structure as desired_order current_sort_order_coerced_type = [] - # This is necessary as .queryset and get_queryset return lists of different types/structures. + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. # We need to parse this data and coerce them into the same type. for contact in current_sort_order: - first_name = contact.first_name - last_name = contact.last_name + first = contact.first_name + last = contact.last_name - match field.name: - case DomainApplication.authorizing_official.field.name: - name_tuple = self.coerced_fk_field_helper(first_name, last_name, 'ao', ':') - if name_tuple: - current_sort_order_coerced_type.append((first_name, last_name)) - case DomainApplication.submitter.field.name: - name_tuple = self.coerced_fk_field_helper(first_name, last_name, 'you', ':') - if name_tuple: - current_sort_order_coerced_type.append((first_name, last_name)) - case DomainApplication.investigator.field.name: - name_tuple = self.coerced_fk_field_helper(first_name, last_name, 'inv', ':') - if name_tuple: - current_sort_order_coerced_type.append((first_name, last_name)) - case DomainApplication.creator.field.name: - name_tuple = self.coerced_fk_field_helper(first_name, last_name, 'cre', ':') - if name_tuple: - current_sort_order_coerced_type.append((first_name, last_name)) + name_tuple = self.coerced_fk_field_helper(first, last, field[1], ':') + if name_tuple: + current_sort_order_coerced_type.append((first, last)) - self.assertEqual(desired_order, current_sort_order_coerced_type, "{} is not ordered alphabetically".format(field.name)) + self.assertEqual(desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field_name)) - # I originally spent some time trying to fully generalize this to replace the match/arg fields, - # but I think for this specific use-case its not necessary since it'll only be used here - def coerced_fk_field_helper(self, first_name, last_name, field_name, queryset_shorthand): - if(first_name and first_name.split(queryset_shorthand)[1] == field_name): + # I originally spent some time trying to fully + # generalize this to replace the match/arg fields, + # but I think for this specific use case + # its not necessary since it'll only be used here. + def coerced_fk_field_helper(self, first_name, last_name, field_name, queryset_shorthand): # noqa + if(first_name and first_name.split(queryset_shorthand)[1] == field_name): # noqa return (first_name, last_name) else: return None - + def tearDown(self): DomainInformation.objects.all().delete() DomainApplication.objects.all().delete() From 5fd842233a1af814838217bfd6e481408db21afd Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 14 Aug 2023 09:13:22 -0600 Subject: [PATCH 098/164] Linting stuff --- src/registrar/models/utility/admin_form_order_helper.py | 4 ++-- src/registrar/tests/test_admin.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py index 3004ec288..e2d70efd9 100644 --- a/src/registrar/models/utility/admin_form_order_helper.py +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) class SortingDictInterface: _model_list: Dict[type, type] = {} _sort_list: list[type] = [] - sorting_dict = {} + sorting_dict: Dict[type, type] = {} def __init__(self, model_list, sort_list): self.sorting_dict = { @@ -21,7 +21,7 @@ class AdminFormOrderHelper(): """A helper class to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" # noqa # Used to keep track of how we want to order_by certain FKs - _sorting_dict: [SortingDictInterface] = [...] + _sorting_dict: [SortingDictInterface] = [...] # noqa def __init__(self, sort): self._sorting_dict = sort diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 6575f3a4b..be62d7c1e 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -436,7 +436,7 @@ class AuditedAdminTest(TestCase): self.assertEqual(desired_order, current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field_name)) + "{} is not ordered alphabetically".format(field.name)) # I originally spent some time trying to fully # generalize this to replace the match/arg fields, From ec7f70e8aef4335d2239874eb8dc63fe76a1194c Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Mon, 14 Aug 2023 11:35:42 -0400 Subject: [PATCH 099/164] Fix table header contract in dark mode --- src/registrar/assets/sass/_theme/_admin.scss | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 2e5da018e..fb7e1e0da 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -106,16 +106,22 @@ html[data-theme="light"] { // Dark mode django (bug due to scss cascade) and USWDS tables .change-list .usa-table, - .change-list .usa-table--striped tbody tr:nth-child(odd) td { - color: var(--body-fg)!important; + .change-list .usa-table--striped tbody tr:nth-child(odd) td, + .change-list .usa-table--borderless thead th, + .change-list .usa-table thead td, + .change-list .usa-table thead th { + color: var(--body-fg); } } // Firefox needs this to be specifically set html[data-theme="dark"] { .change-list .usa-table, - .change-list .usa-table--striped tbody tr:nth-child(odd) td { - color: var(--body-fg)!important; + .change-list .usa-table--striped tbody tr:nth-child(odd) td, + .change-list .usa-table--borderless thead th, + .change-list .usa-table thead td, + .change-list .usa-table thead th { + color: var(--body-fg); } } From 23405cd145deb682876f45e7b31c8549e0b203a8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 14 Aug 2023 09:47:16 -0600 Subject: [PATCH 100/164] Lint --- src/registrar/models/utility/admin_form_order_helper.py | 2 +- src/registrar/tests/test_admin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py index e2d70efd9..aaa0cdce4 100644 --- a/src/registrar/models/utility/admin_form_order_helper.py +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -21,7 +21,7 @@ class AdminFormOrderHelper(): """A helper class to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" # noqa # Used to keep track of how we want to order_by certain FKs - _sorting_dict: [SortingDictInterface] = [...] # noqa + _sorting_dict: list[SortingDictInterface] = [...] # noqa def __init__(self, sort): self._sorting_dict = sort diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index be62d7c1e..6575f3a4b 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -436,7 +436,7 @@ class AuditedAdminTest(TestCase): self.assertEqual(desired_order, current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name)) + "{} is not ordered alphabetically".format(field_name)) # I originally spent some time trying to fully # generalize this to replace the match/arg fields, From 5b78b665a7aeaec03ad2aa562af6eb47f146c068 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 14 Aug 2023 09:57:02 -0600 Subject: [PATCH 101/164] Another lint change --- src/registrar/models/utility/admin_form_order_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py index aaa0cdce4..d29951cf7 100644 --- a/src/registrar/models/utility/admin_form_order_helper.py +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -21,7 +21,7 @@ class AdminFormOrderHelper(): """A helper class to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" # noqa # Used to keep track of how we want to order_by certain FKs - _sorting_dict: list[SortingDictInterface] = [...] # noqa + _sorting_dict: list[SortingDictInterface] = [] # noqa def __init__(self, sort): self._sorting_dict = sort From a3ab395c30a45bb474e9a10e8229a8feeae7841e Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 14 Aug 2023 09:09:04 -0700 Subject: [PATCH 102/164] updated PR template --- .github/pull_request_template.md | 95 +++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 9 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1eeef397b..3b707182c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,13 +1,90 @@ -# # +# Ticket -## 🗣 Description ## +Resolves #00 - - +## Changes -## 💭 Motivation and context ## + +- Change 1 +- Change 2 - - - - \ No newline at end of file + + +## Context for reviewers + + + +## Setup + + + +## Code Review Verification Steps + +### As the original developer, I have + +#### Satisfied acceptance criteria and met development standards + +- [ ] Met the acceptance criteria, or will meet them in a subsequent PR +- [ ] Created/modified automated tests +- [ ] Added at least 2 developers as PR reviewers (only 1 will need to approve) +- [ ] Messaged on Slack or in standup to notify the team that a PR is ready for review + +#### Validated user-facing changes (if applicable) + +- [ ] Checked keyboard navigability +- [ ] Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI) +- [ ] Add at least 1 designer as PR reviewer + +### As code reviewer(s), I have + +#### Reviewed, tested, and left feedback about the changes + +- [ ] Pulled this branch locally and tested it +- [ ] Reviewed this code and left comments +- [ ] Checked that all code is adequately covered by tests +- [ ] Made it clear which comments need to be addressed before this work is merged +- [ ] Considered marking this as accepted even if there are small changes needed + +#### Validated user-facing changes as a developer + +- [ ] Checked keyboard navigability +- [ ] Meets all designs and user flows provided by design/product +- [ ] Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI) + +### As a designer reviewer, I have + +#### Verified that the changes match the design intention + +- [ ] Checked in the design translated visually +- [ ] Checked behavior +- [ ] Checked different states (empty, one, some, error) +- [ ] Checked for landmarks, page heading structure, and links +- [ ] Tried to break the intended flow + +#### Validated user-facing changes as a designer + +- [ ] Checked keyboard navigability +- [ ] Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI) + +## Screenshots + + From e1f27d79286ac4b9a4742b1684404c5f0ceb27f3 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 14 Aug 2023 09:10:53 -0700 Subject: [PATCH 103/164] reduced the size of the ticket header --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3b707182c..b1936f94b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,4 @@ -# Ticket +## Ticket Resolves #00 From 17ac251c36f5100c4cc93c0cd3fa15ebe584c254 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:17:29 -0600 Subject: [PATCH 104/164] Minor cleanup --- src/registrar/config/settings.py | 4 ++-- src/registrar/config/urls.py | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index f16efc18f..a221d88fa 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -285,12 +285,12 @@ SERVER_EMAIL = "root@get.gov" # Content-Security-Policy configuration # this can be restrictive because we have few external scripts -allowed_sources = ("'self'", "https://idp.int.identitysandbox.gov", "https://idp.int.identitysandbox.gov/openid_connect/logout") +allowed_sources = ("'self'") CSP_DEFAULT_SRC = allowed_sources # Most things fall back to default-src, but these two do not and should be # explicitly set CSP_FRAME_ANCESTORS = allowed_sources -CSP_FORM_ACTION = allowed_sources +CSP_FORM_ACTION = ("'self'", "https://idp.int.identitysandbox.gov/openid_connect/logout") # Content-Length header is set by django.middleware.common.CommonMiddleware diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 6159b387b..ad3883f4f 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -47,7 +47,7 @@ urlpatterns = [ path("", views.index, name="home"), path( "admin/logout/", - RedirectView.as_view(url="/openid/logout", permanent=False), + RedirectView.as_view(pattern_name="logout", permanent=False), ), path("admin/", admin.site.urls), path( @@ -125,11 +125,6 @@ if not settings.DEBUG: path( "admin/login/", RedirectView.as_view(pattern_name="login", permanent=False) ), - # redirect to login.gov - path( - "admin/logout/", - RedirectView.as_view(pattern_name="logout", permanent=False), - ), ] # we normally would guard these with `if settings.DEBUG` but tests run with From 6082d8205b733b8987594602711be205ebddf647 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 14 Aug 2023 09:17:43 -0700 Subject: [PATCH 105/164] minor rewording and typo fixes --- .github/pull_request_template.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b1936f94b..d90d83f92 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,7 +26,10 @@ Resolves #00 echo "Code goes here" ``` - Example 2: if the PR was to add a new link with a redirect, this section could simply say go to /path/to/start/page. Click the blue link in the and see the user is redirected to + Example 2: If the PR was to add a new link with a redirect, this section could simply be: + -go to /path/to/start/page + -click the blue link in the + -notice user is redirected to --> ## Code Review Verification Steps @@ -66,7 +69,7 @@ Resolves #00 #### Verified that the changes match the design intention -- [ ] Checked in the design translated visually +- [ ] Checked that the design translated visually - [ ] Checked behavior - [ ] Checked different states (empty, one, some, error) - [ ] Checked for landmarks, page heading structure, and links From c295c22c8d00e5a33b94ef3f366343b686f3afaa Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Mon, 14 Aug 2023 11:28:28 -0500 Subject: [PATCH 106/164] Content Updates --- src/registrar/templates/application_other_contacts.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/application_other_contacts.html b/src/registrar/templates/application_other_contacts.html index 805cb6927..2adf0b99c 100644 --- a/src/registrar/templates/application_other_contacts.html +++ b/src/registrar/templates/application_other_contacts.html @@ -2,7 +2,13 @@ {% load static field_helpers %} {% block form_instructions %} -

We’d like to contact other employees in your organization about your domain request. For example, they could be involved in managing your organization or its technical infrastructure. This information will help us assess your eligibility for a .gov domain. These contacts should be in addition to you and your authorizing official. They should be employees of your organization.

+

To help us assess your eligibility for a .gov domain, please provide contact information for at least two other employees from your organization. +

    +
  • They must be employees who are clearly and publicly affiliated with your organization and familiar with your domain request.
  • +
  • They do not need to be involved with the technical management of your domain (although they can be).
  • +
  • We typically don’t reach out to these employees, but if contact is necessary, our practice is to coordinate with you, the requestor.
  • +
+

{% endblock %} From 24ba547e1433a93025a7f3d66bcc2de0a5efa733 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 14 Aug 2023 09:31:46 -0700 Subject: [PATCH 107/164] Added items from old code review checklist --- .github/pull_request_template.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d90d83f92..7848e2648 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -42,9 +42,18 @@ Resolves #00 - [ ] Created/modified automated tests - [ ] Added at least 2 developers as PR reviewers (only 1 will need to approve) - [ ] Messaged on Slack or in standup to notify the team that a PR is ready for review +- [ ] Changes to “how we do things” are documented in READMEs and or onboarding guide + +Code standards are met: + +- [ ] All new functions and methods are commented using plain language +- [ ] Did dependency updates in Pipfile also get changed in requirements.txt? +- [ ] Interactions with external systems are wrapped in try/except +- [ ] Error handling exists for unusual or missing values #### Validated user-facing changes (if applicable) +- [ ] New pages have been added to .pa11yci file so that they will be tested with our automated accessibility testing - [ ] Checked keyboard navigability - [ ] Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI) - [ ] Add at least 1 designer as PR reviewer @@ -55,12 +64,20 @@ Resolves #00 - [ ] Pulled this branch locally and tested it - [ ] Reviewed this code and left comments +- [ ] All new functions and methods are commented using plain language - [ ] Checked that all code is adequately covered by tests - [ ] Made it clear which comments need to be addressed before this work is merged -- [ ] Considered marking this as accepted even if there are small changes needed + +Code standards are met: + +- [ ] All new functions and methods are commented using plain language +- [ ] Did dependency updates in Pipfile also get changed in requirements.txt? +- [ ] Interactions with external systems are wrapped in try/except +- [ ] Error handling exists for unusual or missing values #### Validated user-facing changes as a developer +- [ ] New pages have been added to .pa11yci file so that they will be tested with our automated accessibility testing - [ ] Checked keyboard navigability - [ ] Meets all designs and user flows provided by design/product - [ ] Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI) From 8dc43211915556d2234ec30307089b00928ad4e7 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 14 Aug 2023 09:34:54 -0700 Subject: [PATCH 108/164] changed subheader --- .github/pull_request_template.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7848e2648..9f8eb1ea1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -44,7 +44,7 @@ Resolves #00 - [ ] Messaged on Slack or in standup to notify the team that a PR is ready for review - [ ] Changes to “how we do things” are documented in READMEs and or onboarding guide -Code standards are met: +* Code standards are met: - [ ] All new functions and methods are commented using plain language - [ ] Did dependency updates in Pipfile also get changed in requirements.txt? @@ -68,7 +68,7 @@ Code standards are met: - [ ] Checked that all code is adequately covered by tests - [ ] Made it clear which comments need to be addressed before this work is merged -Code standards are met: +* Code standards are met: - [ ] All new functions and methods are commented using plain language - [ ] Did dependency updates in Pipfile also get changed in requirements.txt? From f7c9c1c42c4372ced3a59bf334cbc40901ddba2a Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Mon, 14 Aug 2023 12:39:34 -0400 Subject: [PATCH 109/164] Fix the approve() sets status to None bug by calling the transition methods on the current obj (as opposed to the old obj) in django admin --- src/registrar/admin.py | 27 +++++--- src/registrar/models/domain_application.py | 78 ++++++---------------- src/registrar/tests/test_admin.py | 27 ++++++++ 3 files changed, 65 insertions(+), 67 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 182543c19..a3e8e6d2c 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -274,22 +274,27 @@ class DomainApplicationAdmin(ListHeaderAdmin): pass elif obj.status == models.DomainApplication.SUBMITTED: # This is an fsm in model which will throw an error if the - # transition condition is violated, so we call it on the - # original object which has the right status value, and pass - # the updated object which contains the up-to-date data - # for the side effects (like an email send). Same - # comment applies to original_obj method calls below. - original_obj.submit(updated_domain_application=obj) + # transition condition is violated, so we roll back the + # status to what it was before the admn user changed it and + # let the fsm method set it. Same comment applies to + # transition method calls below. + obj.status = original_obj.status + obj.submit() elif obj.status == models.DomainApplication.IN_REVIEW: - original_obj.in_review(updated_domain_application=obj) + obj.status = original_obj.status + obj.in_review() elif obj.status == models.DomainApplication.ACTION_NEEDED: - original_obj.action_needed(updated_domain_application=obj) + obj.status = original_obj.status + obj.action_needed() elif obj.status == models.DomainApplication.APPROVED: - original_obj.approve(updated_domain_application=obj) + obj.status = original_obj.status + obj.approve() elif obj.status == models.DomainApplication.WITHDRAWN: - original_obj.withdraw() + obj.status = original_obj.status + obj.withdraw() elif obj.status == models.DomainApplication.REJECTED: - original_obj.reject(updated_domain_application=obj) + obj.status = original_obj.status + obj.reject() else: logger.warning("Unknown status selected in django admin") diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 1f3d23d8c..67f1ee5d9 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -504,15 +504,10 @@ class DomainApplication(TimeStampedModel): @transition( field="status", source=[STARTED, ACTION_NEEDED, WITHDRAWN], target=SUBMITTED ) - def submit(self, updated_domain_application=None): + def submit(self): """Submit an application that is started. - As a side effect, an email notification is sent. - - This method is called in admin.py on the original application - which has the correct status value, but is passed the changed - application which has the up-to-date data that we'll use - in the email.""" + As a side effect, an email notification is sent.""" # check our conditions here inside the `submit` method so that we # can raise more informative exceptions @@ -528,46 +523,31 @@ class DomainApplication(TimeStampedModel): if not DraftDomain.string_could_be_domain(self.requested_domain.name): raise ValueError("Requested domain is not a valid domain name.") - if updated_domain_application is not None: - # A DomainApplication is being passed to this method (ie from admin) - updated_domain_application._send_status_update_email( - "submission confirmation", - "emails/submission_confirmation.txt", - "emails/submission_confirmation_subject.txt", - ) - else: - # Or this method is called with the right application - # for context, ie from views/application.py - self._send_status_update_email( - "submission confirmation", - "emails/submission_confirmation.txt", - "emails/submission_confirmation_subject.txt", - ) + self._send_status_update_email( + "submission confirmation", + "emails/submission_confirmation.txt", + "emails/submission_confirmation_subject.txt", + ) @transition(field="status", source=SUBMITTED, target=IN_REVIEW) - def in_review(self, updated_domain_application): + def in_review(self): """Investigate an application that has been submitted. - As a side effect, an email notification is sent. + As a side effect, an email notification is sent.""" - This method is called in admin.py on the original application - which has the correct status value, but is passed the changed - application which has the up-to-date data that we'll use - in the email.""" - - updated_domain_application._send_status_update_email( + self._send_status_update_email( "application in review", "emails/status_change_in_review.txt", "emails/status_change_in_review_subject.txt", ) @transition(field="status", source=[IN_REVIEW, REJECTED], target=ACTION_NEEDED) - def action_needed(self, updated_domain_application): + def action_needed(self): """Send back an application that is under investigation or rejected. - As a side effect, an email notification is sent, similar to in_review""" + As a side effect, an email notification is sent.""" - updated_domain_application._send_status_update_email( + self._send_status_update_email( "action needed", "emails/status_change_action_needed.txt", "emails/status_change_action_needed_subject.txt", @@ -576,19 +556,13 @@ class DomainApplication(TimeStampedModel): @transition( field="status", source=[SUBMITTED, IN_REVIEW, REJECTED], target=APPROVED ) - def approve(self, updated_domain_application=None): + def approve(self): """Approve an application that has been submitted. This has substantial side-effects because it creates another database object for the approved Domain and makes the user who created the application into an admin on that domain. It also triggers an email - notification. - - This method is called in admin.py on the original application - which has the correct status value, but is passed the changed - application which has the up-to-date data that we'll use - in the email. - """ + notification.""" # create the domain Domain = apps.get_model("registrar.Domain") @@ -607,31 +581,23 @@ class DomainApplication(TimeStampedModel): user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN ) - if updated_domain_application is not None: - # A DomainApplication is being passed to this method (ie from admin) - updated_domain_application._send_status_update_email( - "application approved", - "emails/status_change_approved.txt", - "emails/status_change_approved_subject.txt", - ) - else: - self._send_status_update_email( - "application approved", - "emails/status_change_approved.txt", - "emails/status_change_approved_subject.txt", - ) + self._send_status_update_email( + "application approved", + "emails/status_change_approved.txt", + "emails/status_change_approved_subject.txt", + ) @transition(field="status", source=[SUBMITTED, IN_REVIEW], target=WITHDRAWN) def withdraw(self): """Withdraw an application that has been submitted.""" @transition(field="status", source=[IN_REVIEW, APPROVED], target=REJECTED) - def reject(self, updated_domain_application): + def reject(self): """Reject an application that has been submitted. As a side effect, an email notification is sent, similar to in_review""" - updated_domain_application._send_status_update_email( + self._send_status_update_email( "action needed", "emails/status_change_rejected.txt", "emails/status_change_rejected_subject.txt", diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 3c07e7608..627e4c627 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -152,6 +152,33 @@ class TestDomainApplicationAdmin(TestCase): # Perform assertions on the mock call itself mock_client_instance.send_email.assert_called_once() + + def test_save_model_sets_approved_domain(self): + # make sure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample application + application = completed_application(status=DomainApplication.IN_REVIEW) + + # Create a mock request + request = self.factory.post( + "/admin/registrar/domainapplication/{}/change/".format(application.pk) + ) + + # Create an instance of the model admin + model_admin = DomainApplicationAdmin(DomainApplication, self.site) + + # Modify the application's property + application.status = DomainApplication.APPROVED + + # Use the model admin's save_model method + model_admin.save_model(request, application, form=None, change=True) + + # Test that approved domain exists and equals requested domain + self.assertEqual( + application.requested_domain.name, application.approved_domain.name + ) @boto3_mocking.patching def test_save_model_sends_action_needed_email(self): From f6f6f54ec7a9109e0e76e3f3c29b9e8d66cd01d2 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Mon, 14 Aug 2023 11:44:09 -0500 Subject: [PATCH 110/164] Content Updates: Authorizing Official --- src/registrar/templates/application_authorizing_official.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/application_authorizing_official.html b/src/registrar/templates/application_authorizing_official.html index dead1974f..0674b3f7b 100644 --- a/src/registrar/templates/application_authorizing_official.html +++ b/src/registrar/templates/application_authorizing_official.html @@ -6,13 +6,13 @@ Who is the authorizing official for your organization? -

Your authorizing official is the person within your organization who can authorize your domain request. This is generally the highest-ranking or highest-elected official in your organization.

+

Your authorizing official is the person within your organization who can authorize your domain request. This must be a person in a role of significant, executive responsibility within the organization.

{% include "includes/ao_example.html" %}
-

We might contact your authorizing official, or their office, to double check that they approve this request. Read more about who can serve as an authorizing official.

+

We typically don’t reach out to the authorizing official, but if contact is necessary, our practice is to coordinate with you, the requestor. Read more about who can serve as an authorizing official.

{% endblock %} From e1d78bda5a44d5e272a8fe4982ac1d579cfcfbf4 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Mon, 14 Aug 2023 12:58:47 -0400 Subject: [PATCH 111/164] lint --- src/registrar/admin.py | 2 +- src/registrar/tests/test_admin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index a3e8e6d2c..96b8aaa33 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -275,7 +275,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): elif obj.status == models.DomainApplication.SUBMITTED: # This is an fsm in model which will throw an error if the # transition condition is violated, so we roll back the - # status to what it was before the admn user changed it and + # status to what it was before the admin user changed it and # let the fsm method set it. Same comment applies to # transition method calls below. obj.status = original_obj.status diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 627e4c627..5f78eac3c 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -152,7 +152,7 @@ class TestDomainApplicationAdmin(TestCase): # Perform assertions on the mock call itself mock_client_instance.send_email.assert_called_once() - + def test_save_model_sets_approved_domain(self): # make sure there is no user with this email EMAIL = "mayor@igorville.gov" From a74ee20bcf072af49c585bf9ae793b71af9bcb23 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:34:57 -0500 Subject: [PATCH 112/164] AO wording adjustment --- src/registrar/templates/application_authorizing_official.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/application_authorizing_official.html b/src/registrar/templates/application_authorizing_official.html index 0674b3f7b..60fce85f8 100644 --- a/src/registrar/templates/application_authorizing_official.html +++ b/src/registrar/templates/application_authorizing_official.html @@ -6,7 +6,7 @@ Who is the authorizing official for your organization? -

Your authorizing official is the person within your organization who can authorize your domain request. This must be a person in a role of significant, executive responsibility within the organization.

+

Your authorizing official is the person within your organization who can authorize your domain request. This person must be in a role of significant, executive responsibility within the organization.

{% include "includes/ao_example.html" %} From 1c60724469dbb83b98b6f88068fc4e64c0c12106 Mon Sep 17 00:00:00 2001 From: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:58:02 -0500 Subject: [PATCH 113/164] Wording adjustments: Other employees --- src/registrar/templates/application_other_contacts.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/application_other_contacts.html b/src/registrar/templates/application_other_contacts.html index 2adf0b99c..74568b592 100644 --- a/src/registrar/templates/application_other_contacts.html +++ b/src/registrar/templates/application_other_contacts.html @@ -2,10 +2,10 @@ {% load static field_helpers %} {% block form_instructions %} -

To help us assess your eligibility for a .gov domain, please provide contact information for at least two other employees from your organization. +

To help us assess your eligibility for a .gov domain, please provide contact information for other employees from your organization.

    -
  • They must be employees who are clearly and publicly affiliated with your organization and familiar with your domain request.
  • -
  • They do not need to be involved with the technical management of your domain (although they can be).
  • +
  • They should be clearly and publicly affiliated with your organization and familiar with your domain request.
  • +
  • They don't need to be involved with the technical management of your domain (although they can be).
  • We typically don’t reach out to these employees, but if contact is necessary, our practice is to coordinate with you, the requestor.

From 14e3548f81b98222ae9c648d461400c65d0a0963 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Mon, 14 Aug 2023 14:39:36 -0400 Subject: [PATCH 114/164] Fix body.dashboard color in dark mode (affects Recent Actions panel in admin index) --- src/registrar/assets/sass/_theme/_admin.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index fb7e1e0da..1a8049086 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -109,7 +109,8 @@ html[data-theme="light"] { .change-list .usa-table--striped tbody tr:nth-child(odd) td, .change-list .usa-table--borderless thead th, .change-list .usa-table thead td, - .change-list .usa-table thead th { + .change-list .usa-table thead th, + body.dashboard { color: var(--body-fg); } } @@ -120,7 +121,8 @@ html[data-theme="dark"] { .change-list .usa-table--striped tbody tr:nth-child(odd) td, .change-list .usa-table--borderless thead th, .change-list .usa-table thead td, - .change-list .usa-table thead th { + .change-list .usa-table thead th, + body.dashboard { color: var(--body-fg); } } From 765502f322b0de8c5dac96926d7b60674567bc74 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Mon, 14 Aug 2023 14:48:16 -0400 Subject: [PATCH 115/164] Edit user fictures to add optional emails --- docs/developer/README.md | 2 ++ src/registrar/fixtures.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/docs/developer/README.md b/docs/developer/README.md index b6938298c..9ef62bba9 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -84,6 +84,7 @@ The endpoint /admin can be used to view and manage site content, including but n ``` 5. In the browser, navigate to /admin. To verify that all is working correctly, under "domain applications" you should see fake domains with various fake statuses. +6. Add an optional email key/value pair ### Adding an Analyst to /admin Analysts are a variant of the admin role with limited permissions. The process for adding an Analyst is much the same as adding an admin: @@ -105,6 +106,7 @@ Analysts are a variant of the admin role with limited permissions. The process f ``` 5. In the browser, navigate to /admin. To verify that all is working correctly, verify that you can only see a sub-section of the modules and some are set to view-only. +6. Add an optional email key/value pair Do note that if you wish to have both an analyst and admin account, append `-Analyst` to your first and last name, or use a completely different first/last name to avoid confusion. Example: `Bob-Analyst` ## Adding to CODEOWNERS (optional) diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index 2c94a1eb4..2291001de 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -79,6 +79,7 @@ class UserFixture: "username": "319c490d-453b-43d9-bc4d-7d6cd8ff6844", "first_name": "Rachid-Analyst", "last_name": "Mrad-Analyst", + "email": "rachid.mrad@gmail.com", }, { "username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd", @@ -129,6 +130,8 @@ class UserFixture: user.is_superuser = True user.first_name = admin["first_name"] user.last_name = admin["last_name"] + if "email" in admin.keys(): + user.email = admin["email"] user.is_staff = True user.is_active = True user.save() @@ -146,6 +149,8 @@ class UserFixture: user.is_superuser = False user.first_name = staff["first_name"] user.last_name = staff["last_name"] + if "email" in admin.keys(): + user.email = admin["email"] user.is_staff = True user.is_active = True From a67e13b04bfbe41a998e204bfa24fa2e83855fdf Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 14 Aug 2023 12:06:06 -0700 Subject: [PATCH 116/164] added mutliple browsers and note for applicant vs analyst --- .github/pull_request_template.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9f8eb1ea1..0bb9ef041 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -44,7 +44,7 @@ Resolves #00 - [ ] Messaged on Slack or in standup to notify the team that a PR is ready for review - [ ] Changes to “how we do things” are documented in READMEs and or onboarding guide -* Code standards are met: +##### Code standards are met (Original Developer) - [ ] All new functions and methods are commented using plain language - [ ] Did dependency updates in Pipfile also get changed in requirements.txt? @@ -64,16 +64,15 @@ Resolves #00 - [ ] Pulled this branch locally and tested it - [ ] Reviewed this code and left comments -- [ ] All new functions and methods are commented using plain language - [ ] Checked that all code is adequately covered by tests - [ ] Made it clear which comments need to be addressed before this work is merged -* Code standards are met: +##### Code standards are met (Code reviewer) - [ ] All new functions and methods are commented using plain language -- [ ] Did dependency updates in Pipfile also get changed in requirements.txt? - [ ] Interactions with external systems are wrapped in try/except - [ ] Error handling exists for unusual or missing values +- [ ] (Rarely needed) Did dependency updates in Pipfile also get changed in requirements.txt? #### Validated user-facing changes as a developer @@ -82,6 +81,14 @@ Resolves #00 - [ ] Meets all designs and user flows provided by design/product - [ ] Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI) +- [ ] Tested with multiple browsers, the suggestion is to use ones that the developer didn't (check off which ones were used) + - [ ] Chrome + - [ ] Microsoft Edge + - [ ] FireFox + - [ ] Safari + +- [ ] (Rarely needed) Test as both an analyst and applicant user + ### As a designer reviewer, I have #### Verified that the changes match the design intention @@ -97,6 +104,14 @@ Resolves #00 - [ ] Checked keyboard navigability - [ ] Tested general usability, landmarks, page header structure, and links with a screen reader (such as Voiceover or ANDI) +- [ ] Tested with multiple browsers (check off which ones were used) + - [ ] Chrome + - [ ] Microsoft Edge + - [ ] FireFox + - [ ] Safari + +- [ ] (Rarely needed) Test as both an analyst and applicant user + ## Screenshots ## Changes @@ -10,7 +15,7 @@ Resolves #00 ## Context for reviewers From 2a47df7eab4ca3b00db9b4f030f5e434efa74a17 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 14 Aug 2023 14:22:06 -0700 Subject: [PATCH 124/164] fixed typo --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3c4313f55..73fabb9f2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -96,7 +96,7 @@ All other changes require just a single approving review.--> - [ ] (Rarely needed) Tested as both an analyst and applicant user -**Note:** Multiple code reviews can share the checklists above, a second reviewers should not make a duplicate checklist +**Note:** Multiple code reviewers can share the checklists above, a second reviewers should not make a duplicate checklist ### As a designer reviewer, I have From 6faa21168abca9577b26442dfe6c112c75af3641 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 14 Aug 2023 20:16:04 -0700 Subject: [PATCH 125/164] Temporarily remove Export Domains section --- src/registrar/templates/home.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 792beec43..c34c244c3 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -125,13 +125,14 @@

You don't have any archived domains

-
+ +
From 94ab136058dd8dac5c8ca0b8cf1c6607987eda9d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 15 Aug 2023 07:26:34 -0600 Subject: [PATCH 126/164] Update src/registrar/config/settings.py allowed_sources was not a tuple: https://django-csp.readthedocs.io/en/latest/configuration.html Co-authored-by: Neil MartinsenBurrell --- src/registrar/config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 39917ad42..f6873b226 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -285,7 +285,7 @@ SERVER_EMAIL = "root@get.gov" # Content-Security-Policy configuration # this can be restrictive because we have few external scripts -allowed_sources = ("'self'") +allowed_sources = ("'self'",) CSP_DEFAULT_SRC = allowed_sources # Most things fall back to default-src, but these two do not and should be # explicitly set From 3a6acd9c2958fc4819ec5f2f49218334741ca24c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 15 Aug 2023 07:56:09 -0600 Subject: [PATCH 127/164] Added translation Requires {% load i18n %} within this scope as per docs: https://docs.djangoproject.com/en/4.1/topics/i18n/translation/#internationalization-in-template-code --- src/registrar/templates/admin/base_site.html | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/registrar/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html index 72bd7af21..6b641722f 100644 --- a/src/registrar/templates/admin/base_site.html +++ b/src/registrar/templates/admin/base_site.html @@ -1,5 +1,6 @@ {% extends "admin/base.html" %} {% load static %} +{% load i18n %} {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} @@ -13,20 +14,25 @@ {% include "admin/color_theme_toggle.html" %} {% endif %} {% endblock %} +{% comment %} + This was copied from the 'userlinks' template, with a few minor changes. + You can find that here: + https://github.com/django/django/blob/d25f3892114466d689fd6936f79f3bd9a9acc30e/django/contrib/admin/templates/admin/base.html#L59 +{% endcomment %} {% block userlinks %} {% if site_url %} - View site / + {% translate 'View site' %} / {% endif %} {% if user.is_active and user.is_staff %} {% url 'django-admindocs-docroot' as docsroot %} {% if docsroot %} - Documentation / + {% translate 'Documentation' %} / {% endif %} {% endif %} {% if user.has_usable_password %} - Change password / + {% translate 'Change password' %} / {% endif %} - Log out + {% translate 'Log out' %} {% include "admin/color_theme_toggle.html" %} {% endblock %} {% block nav-global %}{% endblock %} \ No newline at end of file From df04f2c31255cadf4a3b83214bddb115f42ad6d8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 15 Aug 2023 09:20:34 -0600 Subject: [PATCH 128/164] Update urls.py --- src/registrar/config/urls.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 67ab630e1..133a44a96 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -118,17 +118,6 @@ urlpatterns = [ ), ] -# What is the purpose of this? -# This behaviour gets overwritten, so this doesn't do anything... -# Login in particular -if not settings.DEBUG: - urlpatterns += [ - # redirect to login.gov - path( - "admin/login/", RedirectView.as_view(pattern_name="login", permanent=False) - ), - ] - # we normally would guard these with `if settings.DEBUG` but tests run with # DEBUG = False even when these apps have been loaded because settings.DEBUG # was actually True. Instead, let's add these URLs any time we are able to From 32abc8fd3073b30cfe0115bae3fbba7c31c95749 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 15 Aug 2023 09:24:45 -0600 Subject: [PATCH 129/164] Removed unused import --- src/registrar/config/urls.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 133a44a96..0f136c932 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -4,7 +4,6 @@ For more information see: https://docs.djangoproject.com/en/4.0/topics/http/urls/ """ -from django.conf import settings from django.contrib import admin from django.urls import include, path from django.views.generic import RedirectView From 18b34af9daf4c74cab6d8ddb5693e1524ea7090a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 15 Aug 2023 11:33:17 -0700 Subject: [PATCH 130/164] Update zap for false positives --- src/zap.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/zap.conf b/src/zap.conf index e5e7b4d04..d44447b8e 100644 --- a/src/zap.conf +++ b/src/zap.conf @@ -30,6 +30,8 @@ # UNCLEAR WHY THIS ONE IS FAILING. Giving 404 error. 10027 OUTOFSCOPE http://app:8080/public/js/uswds-init.min.js # get-gov.js contains suspicious word "from" as in `Array.from()` +10027 OUTOFSCOPE http://app:8080/public/src/registrar/templates/home.html +# Contains suspicious word "TODO" which isn't that suspicious 10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js 10028 FAIL (Open Redirect - Passive/beta) 10029 FAIL (Cookie Poisoning - Passive/beta) From 170e07eeff0fe8ba6ce2023701c219bba15f8172 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 15 Aug 2023 11:42:48 -0700 Subject: [PATCH 131/164] Remove from Zap and change wording --- src/registrar/templates/home.html | 2 +- src/zap.conf | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index c34c244c3..30724de8e 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -125,7 +125,7 @@

You don't have any archived domains

- + From a0cf217551f062ff1434fb4cb7b2a4289cb5d7ba Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 15 Aug 2023 11:57:20 -0700 Subject: [PATCH 133/164] Update to just note --- src/registrar/templates/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 32ef48bcf..7b901e747 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -129,7 +129,7 @@ From 068694fa77154cf7b0b86578c5096215a8b338b1 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 15 Aug 2023 12:04:02 -0700 Subject: [PATCH 134/164] Testing out a diff urlpattern to not use todo wording --- src/registrar/templates/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 7b901e747..638367904 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -129,7 +129,7 @@ From 962d70dccc6661e685ea45f78ff2f28e69e39544 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 15 Aug 2023 12:08:18 -0700 Subject: [PATCH 135/164] Add back todo path and attempt zap again --- src/registrar/templates/home.html | 2 +- src/zap.conf | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 638367904..30724de8e 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -129,7 +129,7 @@ diff --git a/src/zap.conf b/src/zap.conf index e5e7b4d04..c1782760b 100644 --- a/src/zap.conf +++ b/src/zap.conf @@ -30,6 +30,8 @@ # UNCLEAR WHY THIS ONE IS FAILING. Giving 404 error. 10027 OUTOFSCOPE http://app:8080/public/js/uswds-init.min.js # get-gov.js contains suspicious word "from" as in `Array.from()` +10027 OUTOFSCOPE http://app:8080/todo +# Ignore wording of "TODO" 10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js 10028 FAIL (Open Redirect - Passive/beta) 10029 FAIL (Cookie Poisoning - Passive/beta) From d4de131ecdebb048d4c9b2f937d9cdd9dcb310f0 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 15 Aug 2023 12:13:29 -0700 Subject: [PATCH 136/164] Add comment to the top correctly this time --- src/zap.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zap.conf b/src/zap.conf index c1782760b..1f9e831fb 100644 --- a/src/zap.conf +++ b/src/zap.conf @@ -30,9 +30,9 @@ # UNCLEAR WHY THIS ONE IS FAILING. Giving 404 error. 10027 OUTOFSCOPE http://app:8080/public/js/uswds-init.min.js # get-gov.js contains suspicious word "from" as in `Array.from()` -10027 OUTOFSCOPE http://app:8080/todo -# Ignore wording of "TODO" 10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js +# Ignore wording of "TODO" +10027 OUTOFSCOPE http://app:8080/todo 10028 FAIL (Open Redirect - Passive/beta) 10029 FAIL (Cookie Poisoning - Passive/beta) 10030 FAIL (User Controllable Charset - Passive/beta) From b8beccd24ac779a792e6f7a1c89b9551e49eb841 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 15 Aug 2023 12:24:23 -0700 Subject: [PATCH 137/164] Switch to tab instead of spaces --- src/zap.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zap.conf b/src/zap.conf index 1f9e831fb..adf51c72c 100644 --- a/src/zap.conf +++ b/src/zap.conf @@ -32,7 +32,7 @@ # get-gov.js contains suspicious word "from" as in `Array.from()` 10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js # Ignore wording of "TODO" -10027 OUTOFSCOPE http://app:8080/todo +10027 OUTOFSCOPE http://app:8080/todo 10028 FAIL (Open Redirect - Passive/beta) 10029 FAIL (Cookie Poisoning - Passive/beta) 10030 FAIL (User Controllable Charset - Passive/beta) From 1dc0d22a1e0e269aa870b4f47ad29dc9a2914e24 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 15 Aug 2023 12:36:55 -0700 Subject: [PATCH 138/164] Update path --- src/zap.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zap.conf b/src/zap.conf index adf51c72c..bdd6b017d 100644 --- a/src/zap.conf +++ b/src/zap.conf @@ -32,7 +32,7 @@ # get-gov.js contains suspicious word "from" as in `Array.from()` 10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js # Ignore wording of "TODO" -10027 OUTOFSCOPE http://app:8080/todo +10027 OUTOFSCOPE http://app:8080.*$ 10028 FAIL (Open Redirect - Passive/beta) 10029 FAIL (Cookie Poisoning - Passive/beta) 10030 FAIL (User Controllable Charset - Passive/beta) From f3d4a90d884d361dba6b1c7a414b50171873fe8c Mon Sep 17 00:00:00 2001 From: Alysia Broddrick <109625347+abroddrick@users.noreply.github.com> Date: Tue, 15 Aug 2023 12:40:22 -0700 Subject: [PATCH 139/164] removed # for subheader --- .github/pull_request_template.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 73fabb9f2..f65007b2b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -50,7 +50,7 @@ All other changes require just a single approving review.--> - [ ] Changes to “how we do things” are documented in READMEs and or onboarding guide - [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the assoicated migrations file has been commited. -##### Ensured code standards are met (Original Developer) +#### Ensured code standards are met (Original Developer) - [ ] All new functions and methods are commented using plain language - [ ] Did dependency updates in Pipfile also get changed in requirements.txt? @@ -74,7 +74,7 @@ All other changes require just a single approving review.--> - [ ] Made it clear which comments need to be addressed before this work is merged - [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the assoicated migrations file has been commited. -##### Ensured code standards are met (Code reviewer) +#### Ensured code standards are met (Code reviewer) - [ ] All new functions and methods are commented using plain language - [ ] Interactions with external systems are wrapped in try/except From 4c9ddf727ed330e919b2f1409f49bc16f272d7a5 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Wed, 16 Aug 2023 11:53:55 -0400 Subject: [PATCH 140/164] Fix contrast issue with filter headers in django admin --- src/registrar/assets/sass/_theme/_admin.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 1a8049086..b87257344 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -110,7 +110,9 @@ html[data-theme="light"] { .change-list .usa-table--borderless thead th, .change-list .usa-table thead td, .change-list .usa-table thead th, - body.dashboard { + body.dashboard, + body.change-list, + body.change-form { color: var(--body-fg); } } @@ -122,7 +124,9 @@ html[data-theme="dark"] { .change-list .usa-table--borderless thead th, .change-list .usa-table thead td, .change-list .usa-table thead th, - body.dashboard { + body.dashboard, + body.change-list, + body.change-form { color: var(--body-fg); } } From 5e24e3473db83d419e7221318cd1cacdef3de43c Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Wed, 16 Aug 2023 14:05:52 -0400 Subject: [PATCH 141/164] lint --- src/registrar/fixtures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index 2291001de..0b1b8926d 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -130,7 +130,7 @@ class UserFixture: user.is_superuser = True user.first_name = admin["first_name"] user.last_name = admin["last_name"] - if "email" in admin.keys(): + if "email" in admin.keys(): user.email = admin["email"] user.is_staff = True user.is_active = True @@ -149,7 +149,7 @@ class UserFixture: user.is_superuser = False user.first_name = staff["first_name"] user.last_name = staff["last_name"] - if "email" in admin.keys(): + if "email" in admin.keys(): user.email = admin["email"] user.is_staff = True user.is_active = True From 738b75a7df41904c4d87537b8e0579d0c9e53373 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 16 Aug 2023 12:32:17 -0600 Subject: [PATCH 142/164] Updated test cases / common.py helpers Expanded some test cases and added some common.py helpers for future tests --- src/registrar/admin.py | 2 +- .../models/utility/admin_form_order_helper.py | 5 +- src/registrar/tests/common.py | 252 ++++++++++++------ src/registrar/tests/test_admin.py | 170 ++++++++++-- 4 files changed, 322 insertions(+), 107 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 9729e018c..124553c31 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -16,7 +16,7 @@ audited_admin_orderby_names = ['first_name', 'last_name'] contact_admin_item_names = ["domain", "requested_domain"] contact_admin_orderby_names = ["name"] # Used to keep track of how we want to order_by certain FKs -foreignkey_orderby_dict: [SortingDictInterface] = [ +foreignkey_orderby_dict: list[SortingDictInterface] = [ # foreign_key - order_by SortingDictInterface( audited_admin_item_names, diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py index d29951cf7..8957262b3 100644 --- a/src/registrar/models/utility/admin_form_order_helper.py +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -31,14 +31,17 @@ class AdminFormOrderHelper(): _order_by_list = [] for item in self._sorting_dict: + # Used to disable black as this is a false positive + # fmt: off drop_down_selected = item.get("dropDownSelected") + # fmt: on sort_by = item.get("sortBy") if db_field.name in drop_down_selected: _order_by_list = sort_by break # Only order if we choose to do so - if _order_by_list: + if not _order_by_list is None: form_field.queryset = form_field.queryset.order_by(*_order_by_list) return form_field diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 9597065b7..ef4428177 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -10,8 +10,13 @@ from typing import List, Dict from django.conf import settings from django.contrib.auth import get_user_model, login -from registrar.models import Contact, DraftDomain, Website, DomainApplication, User +from registrar.models import Contact, DraftDomain, Website, DomainApplication, DomainInvitation, User +import logging +from registrar.models.domain import Domain +from registrar.models.domain_information import DomainInformation + +logger = logging.getLogger(__name__) def get_handlers(): """Obtain pointers to all StreamHandlers.""" @@ -90,6 +95,170 @@ class MockSESClient(Mock): self.EMAILS_SENT.append({"args": args, "kwargs": kwargs}) +class AuditedAdminMockData: + """Creates simple data mocks for AuditedAdminTest""" + + # Constants for different domain object types + INFORMATION = "information" + APPLICATION = "application" + INVITATION = "invitation" + + # These all can likely be generalized more if necessary, particulary with shorthands. + # These are kept basic for now. + # As for why we have shorthands to begin with: + # .queryset returns a list of all objects of the same type, + # rather than by seperating out those fields. + # For such scenarios, the shorthand allows us to not only id a field, + # but append additional information to it. + # This is useful for that scenario and outside it for identifying if values swapped unexpectedly + def dummy_user(self, item_name, shorthand): + """Creates a dummy user object, + but with a shorthand and support for multiple""" + user = User.objects.get_or_create( + first_name="{} First:{}".format(item_name, shorthand), + last_name="{} Last:{}".format(item_name, shorthand), + username="{} username:{}".format(item_name, shorthand) + )[0] + return user + + def dummy_contact(self, item_name, shorthand): + """Creates a dummy contact object""" + contact = Contact.objects.get_or_create( + first_name="{} First:{}".format(item_name, shorthand), + last_name="{} Last:{}".format(item_name, shorthand), + title="{} title:{}".format(item_name, shorthand), + email="{}testy@town.com".format(item_name), + phone="(555) 555 5555" + )[0] + return contact + + def dummy_draft_domain(self,item_name): + """Creates a dummy draft domain object""" + return DraftDomain.objects.get_or_create(name="city{}.gov".format(item_name))[0] + + def dummy_domain(self,item_name): + """Creates a dummy domain object""" + return Domain.objects.get_or_create(name="city{}.gov".format(item_name))[0] + + def dummy_alt(self, item_name): + """Creates a dummy website object for alternates""" + return Website.objects.get_or_create(website="cityalt{}.gov".format(item_name))[0] + + def dummy_current(self, item_name): + """Creates a dummy website object for current""" + return Website.objects.get_or_create(website="city{}.com".format(item_name))[0] + + def get_common_domain_arg_dictionary(self, item_name, org_type="federal", federal_type="executive", purpose="Purpose of the site"): + """Generates a generic argument list for most domains""" + common_args = dict( + organization_type=org_type, + federal_type=federal_type, + purpose=purpose, + organization_name="{} Testorg".format(item_name), + address_line1="{} address 1".format(item_name), + address_line2="{} address 2".format(item_name), + is_policy_acknowledged=True, + state_territory="NY", + zipcode="10002", + type_of_work = 'e-Government', + anything_else = "There is more", + authorizing_official = self.dummy_contact(item_name, 'authorizing_official'), + submitter = self.dummy_contact(item_name, 'submitter'), + creator = self.dummy_user(item_name, 'creator'), + ) + return common_args + + # This can be boiled down more, though for our purposes this is OK + def dummy_kwarg_boilerplate(self, domain_type, item_name, status, org_type="federal", federal_type="executive", purpose="Purpose of the site"): + """Returns kwargs for different domain object types""" + common_args = self.get_common_domain_arg_dictionary(item_name, org_type, federal_type, purpose) + full_arg_list = None + match domain_type: + case self.APPLICATION: + full_arg_list = dict( + **common_args, + requested_domain = self.dummy_draft_domain(item_name), + investigator = self.dummy_user(item_name, 'investigator'), + status = status, + ) + case self.INFORMATION: + full_arg_list = dict( + **common_args, + domain = self.dummy_domain(item_name), + domain_application = self.create_full_dummy_domain_application(item_name) + ) + case self.INVITATION: + full_arg_list = dict( + email = "test_mail@mail.com", + domain = self.dummy_domain(item_name), + status = DomainInvitation.INVITED + ) + return full_arg_list + + def create_full_dummy_domain_application( + self, + object_name, + status=DomainApplication.STARTED + ): + """Creates a dummy domain application object""" + domain_application_kwargs = self.dummy_kwarg_boilerplate(self.APPLICATION, object_name, status) + application = DomainApplication.objects.get_or_create(**domain_application_kwargs)[0] + return application + + def create_full_dummy_domain_information( + self, + object_name, + status=DomainApplication.STARTED + ): + """Creates a dummy domain information object""" + domain_application_kwargs = self.dummy_kwarg_boilerplate(self.INFORMATION, object_name, status) + application = DomainInformation.objects.get_or_create(**domain_application_kwargs)[0] + return application + + def create_full_dummy_domain_invitation( + self, + object_name, + status=DomainApplication.STARTED + ): + """Creates a dummy domain invitation object""" + domain_application_kwargs = self.dummy_kwarg_boilerplate(self.INVITATION, object_name, status) + application = DomainInvitation.objects.get_or_create(**domain_application_kwargs)[0] + + return application + + def create_full_dummy_domain_object( + self, + domain_type, + object_name, + has_other_contacts=True, + has_current_website=True, + has_alternative_gov_domain=True, + status=DomainApplication.STARTED + ): + """A helper to create a dummy domain application object""" + application = None + match domain_type: + case self.APPLICATION: + application = self.create_full_dummy_domain_application(object_name, status) + case self.INVITATION: + application = self.create_full_dummy_domain_invitation(object_name, status) + case self.INFORMATION: + application = self.create_full_dummy_domain_information(object_name, status) + case _: + raise ValueError("Invalid domain_type, must conform to given constants") + + if has_other_contacts and domain_type != self.INVITATION: + other = self.dummy_contact(object_name, 'other') + application.other_contacts.add(other) + if has_current_website and domain_type == self.APPLICATION: + current = self.dummy_current(object_name) + application.current_websites.add(current) + if has_alternative_gov_domain and domain_type == self.APPLICATION: + alt = self.dummy_alt(object_name) + application.alternative_domains.add(alt) + + return application + def mock_user(): """A simple user.""" user_kwargs = dict( @@ -192,84 +361,15 @@ def completed_application( return application -def multiple_unalphabetical_applications( - has_other_contacts=True, - has_current_website=True, - has_alternative_gov_domain=True, - has_type_of_work=True, - has_anything_else=True, - status=DomainApplication.STARTED, - user=False, -): +def multiple_unalphabetical_domain_objects(domain_type = AuditedAdminMockData.APPLICATION): + """Returns a list of generic domain objects for testing purposes""" applications = [] list_of_letters = list(ascii_uppercase) random.shuffle(list_of_letters) - for x in list_of_letters: - user = get_user_model().objects.create( - first_name="{} First:cre".format(x), - last_name="{} Last:cre".format(x), - username="{} username:cre".format(x) - ) - ao, _ = Contact.objects.get_or_create( - first_name="{} First:ao".format(x), - last_name="{} Last:ao".format(x), - title="{} Chief Tester".format(x), - email="testy@town.com", - phone="(555) 555 5555", - ) - domain, _ = DraftDomain.objects.get_or_create(name="city{}.gov".format(x)) - alt, _ = Website.objects.get_or_create(website="cityalt{}.gov".format(x)) - current, _ = Website.objects.get_or_create(website="city{}.com".format(x)) - you, _ = Contact.objects.get_or_create( - first_name="{} First:you".format(x), - last_name="{} Last:you".format(x), - title="{} Admin Tester".format(x), - email="mayor@igorville.gov", - phone="(555) 555 5556", - ) - other, _ = Contact.objects.get_or_create( - first_name="{} First:other".format(x), - last_name="{} Last:other".format(x), - title="{} Another Tester".format(x), - email="{}testy2@town.com".format(x), - phone="(555) 555 5557", - ) - inv, _ = User.objects.get_or_create( - first_name="{} First:inv".format(x), - last_name="{} Last:inv".format(x), - username="{} username:inv".format(x) - ) - domain_application_kwargs = dict( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - is_policy_acknowledged=True, - organization_name="{}Testorg".format(x), - address_line1="address 1", - address_line2="address 2", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - requested_domain=domain, - submitter=you, - creator=user, - status=status, - investigator=inv - ) - if has_type_of_work: - domain_application_kwargs["type_of_work"] = "e-Government" - if has_anything_else: - domain_application_kwargs["anything_else"] = "There is more" - application, _ = DomainApplication.objects.get_or_create( - **domain_application_kwargs - ) - - if has_other_contacts: - application.other_contacts.add(other) - if has_current_website: - application.current_websites.add(current) - if has_alternative_gov_domain: - application.alternative_domains.add(alt) + mock = AuditedAdminMockData() + for object_name in list_of_letters: + application = mock.create_full_dummy_domain_object(domain_type, object_name) applications.append(application) return applications + diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 6575f3a4b..490442f00 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1,9 +1,14 @@ from django.test import TestCase, RequestFactory, Client from django.contrib.admin.sites import AdminSite -from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin, MyUserAdmin, AuditedAdmin # noqa +from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin +# Need to split these up due to the linter +from registrar.admin import MyUserAdmin, AuditedAdmin from registrar.models import DomainApplication, DomainInformation, User from registrar.models.contact import Contact -from .common import completed_application, mock_user, create_superuser, create_user, multiple_unalphabetical_applications # noqa +from registrar.models.domain_invitation import DomainInvitation +from .common import completed_application, mock_user, create_superuser, create_user +# Need to split these up due to the linter +from .common import multiple_unalphabetical_domain_objects from django.contrib.auth import get_user_model from django.conf import settings @@ -379,20 +384,32 @@ class AuditedAdminTest(TestCase): self.factory = RequestFactory() self.client = Client(HTTP_HOST="localhost:8080") + def order_by_desired_field_helper(self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names): + formatted_sort_fields = [] + for obj in obj_names: + formatted_sort_fields.append("{}__{}".format(field_name, obj)) + + # Not really a fan of how this looks, but as the linter demands... + ordered_list = list( + obj_to_sort.get_queryset(request).order_by( + *formatted_sort_fields).values_list( + *formatted_sort_fields) + ) + + return ordered_list + + # Q: These three tests can be generalized into an object, + # is it worth the time investment to do so? def test_alphabetically_sorted_fk_fields_domain_application(self): tested_fields = [ - # field (0) - field shorthand (1) - # the 'field shorthand' is used for type coercion. - # It is only used here to reduce boilerplate, - # but keep in mind that it is not needed outside this class. - (DomainApplication.authorizing_official.field, 'ao'), - (DomainApplication.submitter.field, 'you'), - (DomainApplication.investigator.field, 'inv'), - (DomainApplication.creator.field, 'cre') + DomainApplication.authorizing_official.field, + DomainApplication.submitter.field, + DomainApplication.investigator.field, + DomainApplication.creator.field, ] # Create a sample application - review status does not matter - applications = multiple_unalphabetical_applications() + applications = multiple_unalphabetical_domain_objects("application") # Create a mock request request = self.factory.post( @@ -405,19 +422,11 @@ class AuditedAdminTest(TestCase): # but both fields are of a fixed length. # For test case purposes, this should be performant. for field in tested_fields: - field_name = field[0].name - first_name_field = "{}__first_name".format(field_name) - last_name_field = "{}__last_name".format(field_name) # We want both of these to be lists, as it is richer test wise. - # Not really a fan of how this looks, but as the linter demands... - desired_order = list( - model_admin.get_queryset(request).order_by( - first_name_field, last_name_field).values_list( - first_name_field, last_name_field) - ) + desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, "first_name", "last_name") current_sort_order: Contact = list( - model_admin.formfield_for_foreignkey(field[0], request).queryset + model_admin.formfield_for_foreignkey(field, request).queryset ) # Conforms to the same object structure as desired_order @@ -430,24 +439,127 @@ class AuditedAdminTest(TestCase): first = contact.first_name last = contact.last_name - name_tuple = self.coerced_fk_field_helper(first, last, field[1], ':') + name_tuple = self.coerced_fk_field_helper(first, last, field.name, ':') if name_tuple: current_sort_order_coerced_type.append((first, last)) self.assertEqual(desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field_name)) + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field.name)) + + def test_alphabetically_sorted_fk_fields_domain_information(self): + tested_fields = [ + DomainInformation.authorizing_official.field, + DomainInformation.submitter.field, + DomainInformation.domain.field, + DomainInformation.creator.field + ] + + # Create a sample application - review status does not matter + applications = multiple_unalphabetical_domain_objects("information") + + # Create a mock request + request = self.factory.post( + "/admin/registrar/domaininformation/{}/change/".format(applications[0].pk) + ) + + model_admin = AuditedAdmin(DomainInformation, self.site) + + sorted_fields = [] + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + isNamefield: bool = (field == DomainInformation.domain.field) + if(isNamefield): + sorted_fields = ["name"] + else: + sorted_fields = ["first_name", "last_name"] + # We want both of these to be lists, as it is richer test wise. + + desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) + current_sort_order = list( + model_admin.formfield_for_foreignkey(field, request).queryset + ) + + # Conforms to the same object structure as desired_order + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for contact in current_sort_order: + if not isNamefield: + first = contact.first_name + last = contact.last_name + else: + first = contact.name + last = None + + name_tuple = self.coerced_fk_field_helper(first, last, field.name, ':') + if not name_tuple is None: + current_sort_order_coerced_type.append(name_tuple) + + self.assertEqual(desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field.name)) + + def test_alphabetically_sorted_fk_fields_domain_invitation(self): + tested_fields = [DomainInvitation.domain.field] + + # Create a sample application - review status does not matter + applications = multiple_unalphabetical_domain_objects("invitation") + + # Create a mock request + request = self.factory.post( + "/admin/registrar/domaininvitation/{}/change/".format(applications[0].pk) + ) + + model_admin = AuditedAdmin(DomainInvitation, self.site) + + sorted_fields = [] + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + sorted_fields = ["name"] + # We want both of these to be lists, as it is richer test wise. + + desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) + current_sort_order = list( + model_admin.formfield_for_foreignkey(field, request).queryset + ) + + # Conforms to the same object structure as desired_order + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for contact in current_sort_order: + first = contact.name + last = None + + name_tuple = self.coerced_fk_field_helper(first, last, field.name, ':') + if not name_tuple is None: + current_sort_order_coerced_type.append(name_tuple) + + self.assertEqual(desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field.name)) - # I originally spent some time trying to fully - # generalize this to replace the match/arg fields, - # but I think for this specific use case - # its not necessary since it'll only be used here. def coerced_fk_field_helper(self, first_name, last_name, field_name, queryset_shorthand): # noqa + returned_tuple = (first_name, last_name) + # Handles edge case for names - structured strangely + if last_name is None: + return (first_name,) + if(first_name and first_name.split(queryset_shorthand)[1] == field_name): # noqa - return (first_name, last_name) + return returned_tuple else: return None def tearDown(self): DomainInformation.objects.all().delete() DomainApplication.objects.all().delete() + DomainInvitation.objects.all().delete() From 051b852cd9f8d663344d21731edc51a8627073eb Mon Sep 17 00:00:00 2001 From: rachidatecs <107004823+rachidatecs@users.noreply.github.com> Date: Wed, 16 Aug 2023 14:56:37 -0400 Subject: [PATCH 143/164] Update docs/architecture/decisions/0021-django-admin.md Co-authored-by: Neil MartinsenBurrell --- docs/architecture/decisions/0021-django-admin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/decisions/0021-django-admin.md b/docs/architecture/decisions/0021-django-admin.md index bca51c2ef..add6992cd 100644 --- a/docs/architecture/decisions/0021-django-admin.md +++ b/docs/architecture/decisions/0021-django-admin.md @@ -34,7 +34,7 @@ In contrast to building an admin interface from scratch where development activi involve _building up_, leveraging Django Admin will require carefully _pairing back_ the functionalities available to users such as analysts. -On accessibility: Django admin is almost fully accessible out-of-the-box, the expections being tables, checkboxes, and +On accessibility: Django admin is almost fully accessible out-of-the-box, the exceptions being tables, checkboxes, and color contrast. We have remedied the first 2 with template overrides and the 3rd with theming (see below). On USWDS and theming: Django admin brings its own high level design framework. We have determined that theming on top of Django (scss) From f31258d2b0274d54de4fd42d09db146df27e9bae Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 16 Aug 2023 14:26:17 -0600 Subject: [PATCH 144/164] Linter things / Rachid suggestions --- src/registrar/admin.py | 18 +-- .../models/utility/admin_form_order_helper.py | 10 +- src/registrar/tests/common.py | 123 ++++++++++++------ src/registrar/tests/test_admin.py | 79 ++++++++--- 4 files changed, 157 insertions(+), 73 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 124553c31..d19231755 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -13,8 +13,8 @@ audited_admin_item_names = ["submitter", "authorizing_official", "investigator", "creator", "user"] audited_admin_orderby_names = ['first_name', 'last_name'] -contact_admin_item_names = ["domain", "requested_domain"] -contact_admin_orderby_names = ["name"] +special_audited_admin_item_names = ["domain", "requested_domain"] +special_audited_admin_orderby_names = ["name"] # Used to keep track of how we want to order_by certain FKs foreignkey_orderby_dict: list[SortingDictInterface] = [ # foreign_key - order_by @@ -22,9 +22,10 @@ foreignkey_orderby_dict: list[SortingDictInterface] = [ audited_admin_item_names, audited_admin_orderby_names ).sorting_dict, + # Handles fields that are sorted by 'name' SortingDictInterface( - contact_admin_item_names, - contact_admin_orderby_names + special_audited_admin_item_names, + special_audited_admin_orderby_names ).sorting_dict ] @@ -186,15 +187,9 @@ class DomainAdmin(ListHeaderAdmin): class ContactAdmin(ListHeaderAdmin): """Custom contact admin class to add search.""" - search_fields = ["email", "first_name", "last_name"] search_help_text = "Search by firstname, lastname or email." - def formfield_for_foreignkey(self, db_field, request, **kwargs): - """Used to sort dropdown fields alphabetically but can be expanded upon""" - form_field = super().formfield_for_foreignkey(db_field, request, **kwargs) - return form_field_order_helper(form_field, db_field) - class DomainApplicationAdmin(ListHeaderAdmin): @@ -333,7 +328,8 @@ class DomainApplicationAdmin(ListHeaderAdmin): # For readability purposes, but can be replaced with a one liner def form_field_order_helper(form_field, db_field): - """A shorthand for AdminFormOrderHelper(foreignkey_orderby_dict).get_ordered_form_field(form_field, db_field)""" # noqa + """A shorthand for AdminFormOrderHelper(foreignkey_orderby_dict) + .get_ordered_form_field(form_field, db_field)""" form = AdminFormOrderHelper(foreignkey_orderby_dict) return form.get_ordered_form_field(form_field, db_field) diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py index 8957262b3..ff32ead93 100644 --- a/src/registrar/models/utility/admin_form_order_helper.py +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -18,16 +18,18 @@ class SortingDictInterface: class AdminFormOrderHelper(): - """A helper class to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" # noqa + """A helper class to order a dropdown field in Django Admin, + takes the fields you want to order by as an array""" # Used to keep track of how we want to order_by certain FKs - _sorting_dict: list[SortingDictInterface] = [] # noqa + _sorting_dict: list[SortingDictInterface] = [] def __init__(self, sort): self._sorting_dict = sort def get_ordered_form_field(self, form_field, db_field) -> (ModelChoiceField | None): - """Orders the queryset for a ModelChoiceField based on the order_by_dict dictionary""" # noqa + """Orders the queryset for a ModelChoiceField + based on the order_by_dict dictionary""" _order_by_list = [] for item in self._sorting_dict: @@ -41,7 +43,7 @@ class AdminFormOrderHelper(): break # Only order if we choose to do so - if not _order_by_list is None: + if _order_by_list is not None: form_field.queryset = form_field.queryset.order_by(*_order_by_list) return form_field diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index ef4428177..b92fcf351 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -10,14 +10,13 @@ from typing import List, Dict from django.conf import settings from django.contrib.auth import get_user_model, login -from registrar.models import Contact, DraftDomain, Website, DomainApplication, DomainInvitation, User -import logging -from registrar.models.domain import Domain - -from registrar.models.domain_information import DomainInformation +from registrar.models import Contact, DraftDomain, Website, DomainApplication +# For the linter +from registrar.models import DomainInvitation, User, DomainInformation, Domain logger = logging.getLogger(__name__) + def get_handlers(): """Obtain pointers to all StreamHandlers.""" handlers = {} @@ -103,14 +102,16 @@ class AuditedAdminMockData: APPLICATION = "application" INVITATION = "invitation" - # These all can likely be generalized more if necessary, particulary with shorthands. + # These all can likely be generalized more if necessary, + # particulary with shorthands. # These are kept basic for now. # As for why we have shorthands to begin with: # .queryset returns a list of all objects of the same type, # rather than by seperating out those fields. # For such scenarios, the shorthand allows us to not only id a field, # but append additional information to it. - # This is useful for that scenario and outside it for identifying if values swapped unexpectedly + # This is useful for that scenario and outside it for + # identifying if values swapped unexpectedly def dummy_user(self, item_name, shorthand): """Creates a dummy user object, but with a shorthand and support for multiple""" @@ -132,23 +133,31 @@ class AuditedAdminMockData: )[0] return contact - def dummy_draft_domain(self,item_name): + def dummy_draft_domain(self, item_name): """Creates a dummy draft domain object""" return DraftDomain.objects.get_or_create(name="city{}.gov".format(item_name))[0] - def dummy_domain(self,item_name): + def dummy_domain(self, item_name): """Creates a dummy domain object""" return Domain.objects.get_or_create(name="city{}.gov".format(item_name))[0] def dummy_alt(self, item_name): """Creates a dummy website object for alternates""" - return Website.objects.get_or_create(website="cityalt{}.gov".format(item_name))[0] + return Website.objects.get_or_create( + website="cityalt{}.gov".format(item_name) + )[0] def dummy_current(self, item_name): """Creates a dummy website object for current""" return Website.objects.get_or_create(website="city{}.com".format(item_name))[0] - def get_common_domain_arg_dictionary(self, item_name, org_type="federal", federal_type="executive", purpose="Purpose of the site"): + def get_common_domain_arg_dictionary( + self, + item_name, + org_type="federal", + federal_type="executive", + purpose="Purpose of the site" + ): """Generates a generic argument list for most domains""" common_args = dict( organization_type=org_type, @@ -160,38 +169,52 @@ class AuditedAdminMockData: is_policy_acknowledged=True, state_territory="NY", zipcode="10002", - type_of_work = 'e-Government', - anything_else = "There is more", - authorizing_official = self.dummy_contact(item_name, 'authorizing_official'), - submitter = self.dummy_contact(item_name, 'submitter'), - creator = self.dummy_user(item_name, 'creator'), + type_of_work='e-Government', + anything_else="There is more", + authorizing_official=self.dummy_contact(item_name, 'authorizing_official'), + submitter=self.dummy_contact(item_name, 'submitter'), + creator=self.dummy_user(item_name, 'creator'), ) return common_args # This can be boiled down more, though for our purposes this is OK - def dummy_kwarg_boilerplate(self, domain_type, item_name, status, org_type="federal", federal_type="executive", purpose="Purpose of the site"): + def dummy_kwarg_boilerplate( + self, + domain_type, + item_name, + status, + org_type="federal", + federal_type="executive", + purpose="Purpose of the site" + ): """Returns kwargs for different domain object types""" - common_args = self.get_common_domain_arg_dictionary(item_name, org_type, federal_type, purpose) + common_args = self.get_common_domain_arg_dictionary( + item_name, + org_type, + federal_type, + purpose + ) full_arg_list = None match domain_type: case self.APPLICATION: full_arg_list = dict( **common_args, - requested_domain = self.dummy_draft_domain(item_name), - investigator = self.dummy_user(item_name, 'investigator'), - status = status, + requested_domain=self.dummy_draft_domain(item_name), + investigator=self.dummy_user(item_name, 'investigator'), + status=status, ) case self.INFORMATION: + domain_app = self.create_full_dummy_domain_application(item_name) full_arg_list = dict( **common_args, - domain = self.dummy_domain(item_name), - domain_application = self.create_full_dummy_domain_application(item_name) + domain=self.dummy_domain(item_name), + domain_application=domain_app ) case self.INVITATION: full_arg_list = dict( - email = "test_mail@mail.com", - domain = self.dummy_domain(item_name), - status = DomainInvitation.INVITED + email="test_mail@mail.com", + domain=self.dummy_domain(item_name), + status=DomainInvitation.INVITED ) return full_arg_list @@ -201,8 +224,14 @@ class AuditedAdminMockData: status=DomainApplication.STARTED ): """Creates a dummy domain application object""" - domain_application_kwargs = self.dummy_kwarg_boilerplate(self.APPLICATION, object_name, status) - application = DomainApplication.objects.get_or_create(**domain_application_kwargs)[0] + domain_application_kwargs = self.dummy_kwarg_boilerplate( + self.APPLICATION, + object_name, + status + ) + application = DomainApplication.objects.get_or_create( + **domain_application_kwargs + )[0] return application def create_full_dummy_domain_information( @@ -211,8 +240,14 @@ class AuditedAdminMockData: status=DomainApplication.STARTED ): """Creates a dummy domain information object""" - domain_application_kwargs = self.dummy_kwarg_boilerplate(self.INFORMATION, object_name, status) - application = DomainInformation.objects.get_or_create(**domain_application_kwargs)[0] + domain_application_kwargs = self.dummy_kwarg_boilerplate( + self.INFORMATION, + object_name, + status + ) + application = DomainInformation.objects.get_or_create( + **domain_application_kwargs + )[0] return application def create_full_dummy_domain_invitation( @@ -221,8 +256,14 @@ class AuditedAdminMockData: status=DomainApplication.STARTED ): """Creates a dummy domain invitation object""" - domain_application_kwargs = self.dummy_kwarg_boilerplate(self.INVITATION, object_name, status) - application = DomainInvitation.objects.get_or_create(**domain_application_kwargs)[0] + domain_application_kwargs = self.dummy_kwarg_boilerplate( + self.INVITATION, + object_name, + status + ) + application = DomainInvitation.objects.get_or_create( + **domain_application_kwargs + )[0] return application @@ -239,11 +280,17 @@ class AuditedAdminMockData: application = None match domain_type: case self.APPLICATION: - application = self.create_full_dummy_domain_application(object_name, status) + application = self.create_full_dummy_domain_application( + object_name, status + ) case self.INVITATION: - application = self.create_full_dummy_domain_invitation(object_name, status) + application = self.create_full_dummy_domain_invitation( + object_name, status + ) case self.INFORMATION: - application = self.create_full_dummy_domain_information(object_name, status) + application = self.create_full_dummy_domain_information( + object_name, status + ) case _: raise ValueError("Invalid domain_type, must conform to given constants") @@ -259,6 +306,7 @@ class AuditedAdminMockData: return application + def mock_user(): """A simple user.""" user_kwargs = dict( @@ -361,7 +409,9 @@ def completed_application( return application -def multiple_unalphabetical_domain_objects(domain_type = AuditedAdminMockData.APPLICATION): +def multiple_unalphabetical_domain_objects( + domain_type=AuditedAdminMockData.APPLICATION +): """Returns a list of generic domain objects for testing purposes""" applications = [] list_of_letters = list(ascii_uppercase) @@ -372,4 +422,3 @@ def multiple_unalphabetical_domain_objects(domain_type = AuditedAdminMockData.AP application = mock.create_full_dummy_domain_object(domain_type, object_name) applications.append(application) return applications - diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 490442f00..cd66c9bbd 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -384,7 +384,13 @@ class AuditedAdminTest(TestCase): self.factory = RequestFactory() self.client = Client(HTTP_HOST="localhost:8080") - def order_by_desired_field_helper(self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names): + def order_by_desired_field_helper( + self, + obj_to_sort: AuditedAdmin, + request, + field_name, + *obj_names + ): formatted_sort_fields = [] for obj in obj_names: formatted_sort_fields.append("{}__{}".format(field_name, obj)) @@ -408,7 +414,7 @@ class AuditedAdminTest(TestCase): DomainApplication.creator.field, ] - # Create a sample application - review status does not matter + # Creates multiple domain applications - review status does not matter applications = multiple_unalphabetical_domain_objects("application") # Create a mock request @@ -424,7 +430,13 @@ class AuditedAdminTest(TestCase): for field in tested_fields: # We want both of these to be lists, as it is richer test wise. - desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, "first_name", "last_name") + desired_order = self.order_by_desired_field_helper( + model_admin, + request, + field.name, + "first_name", + "last_name" + ) current_sort_order: Contact = list( model_admin.formfield_for_foreignkey(field, request).queryset ) @@ -443,9 +455,11 @@ class AuditedAdminTest(TestCase): if name_tuple: current_sort_order_coerced_type.append((first, last)) - self.assertEqual(desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name)) + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field.name) + ) def test_alphabetically_sorted_fk_fields_domain_information(self): tested_fields = [ @@ -455,7 +469,7 @@ class AuditedAdminTest(TestCase): DomainInformation.creator.field ] - # Create a sample application - review status does not matter + # Creates multiple domain applications - review status does not matter applications = multiple_unalphabetical_domain_objects("information") # Create a mock request @@ -471,13 +485,18 @@ class AuditedAdminTest(TestCase): # For test case purposes, this should be performant. for field in tested_fields: isNamefield: bool = (field == DomainInformation.domain.field) - if(isNamefield): + if (isNamefield): sorted_fields = ["name"] else: sorted_fields = ["first_name", "last_name"] # We want both of these to be lists, as it is richer test wise. - desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) + desired_order = self.order_by_desired_field_helper( + model_admin, + request, + field.name, + *sorted_fields + ) current_sort_order = list( model_admin.formfield_for_foreignkey(field, request).queryset ) @@ -497,17 +516,19 @@ class AuditedAdminTest(TestCase): last = None name_tuple = self.coerced_fk_field_helper(first, last, field.name, ':') - if not name_tuple is None: + if name_tuple is not None: current_sort_order_coerced_type.append(name_tuple) - self.assertEqual(desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name)) + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field.name) + ) def test_alphabetically_sorted_fk_fields_domain_invitation(self): tested_fields = [DomainInvitation.domain.field] - # Create a sample application - review status does not matter + # Creates multiple domain applications - review status does not matter applications = multiple_unalphabetical_domain_objects("invitation") # Create a mock request @@ -525,7 +546,12 @@ class AuditedAdminTest(TestCase): sorted_fields = ["name"] # We want both of these to be lists, as it is richer test wise. - desired_order = self.order_by_desired_field_helper(model_admin, request, field.name, *sorted_fields) + desired_order = self.order_by_desired_field_helper( + model_admin, + request, + field.name, + *sorted_fields + ) current_sort_order = list( model_admin.formfield_for_foreignkey(field, request).queryset ) @@ -541,20 +567,31 @@ class AuditedAdminTest(TestCase): last = None name_tuple = self.coerced_fk_field_helper(first, last, field.name, ':') - if not name_tuple is None: + if name_tuple is not None: current_sort_order_coerced_type.append(name_tuple) - self.assertEqual(desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name)) + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field.name) + ) + + def coerced_fk_field_helper( + self, + first_name, + last_name, + field_name, + queryset_shorthand + ): + if first_name is None: + raise ValueError('Invalid value for first_name, must be defined') - def coerced_fk_field_helper(self, first_name, last_name, field_name, queryset_shorthand): # noqa returned_tuple = (first_name, last_name) # Handles edge case for names - structured strangely if last_name is None: return (first_name,) - if(first_name and first_name.split(queryset_shorthand)[1] == field_name): # noqa + if(first_name.split(queryset_shorthand)[1] == field_name): return returned_tuple else: return None From b389c9f831afb6f500dae71bcf01ff8857ba018e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 16 Aug 2023 14:32:11 -0600 Subject: [PATCH 145/164] Fixed unintentional model change Not sure how this snuck in --- src/registrar/models/domain_information.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 084bd0515..438567003 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -244,3 +244,6 @@ class DomainInformation(TimeStampedModel): domain_info.domain = domain domain_info.save() return domain_info + + class Meta: + verbose_name_plural = "Domain Information" \ No newline at end of file From 13718371034080cc5e24b350c00ca96662ff79b0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 16 Aug 2023 15:05:54 -0600 Subject: [PATCH 146/164] Update admin_form_order_helper.py --- .../models/utility/admin_form_order_helper.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py index ff32ead93..2057a2276 100644 --- a/src/registrar/models/utility/admin_form_order_helper.py +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -10,9 +10,20 @@ class SortingDictInterface: _sort_list: list[type] = [] sorting_dict: Dict[type, type] = {} + # _model_list and _sort_list can be + # any length, and will be called multiple times. + # We want the perf advantage of a dictionary, + # while making creating new SortingDictInterface + # items pretty straight forward and easy (aka as a list) + def convert_list_to_dict(self, value_list): + dictionary: Dict[type, type] = {} + for item in value_list: + dictionary[item] = item + return dictionary + def __init__(self, model_list, sort_list): self.sorting_dict = { - "dropDownSelected": model_list, + "dropDownSelected": self.convert_list_to_dict(model_list), "sortBy": sort_list } @@ -33,11 +44,9 @@ class AdminFormOrderHelper(): _order_by_list = [] for item in self._sorting_dict: - # Used to disable black as this is a false positive - # fmt: off - drop_down_selected = item.get("dropDownSelected") - # fmt: on - sort_by = item.get("sortBy") + drop_down_selected = item["dropDownSelected"] + sort_by = item["sortBy"] + if db_field.name in drop_down_selected: _order_by_list = sort_by break From ed26035763f87239b168065a49c6523545027744 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 17 Aug 2023 09:54:16 -0600 Subject: [PATCH 147/164] Fixe some linting issues / comments --- src/registrar/admin.py | 29 +++++++-------- .../models/utility/admin_form_order_helper.py | 35 +++++++++++-------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index d19231755..66215733c 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -4,29 +4,26 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect from django.urls import reverse -from registrar.models.utility.admin_form_order_helper import AdminFormOrderHelper, SortingDictInterface # noqa +from registrar.models.utility.admin_form_order_helper import AdminFormOrderHelper, SortingDict +# Split up for the linter +from registrar.models.utility.admin_form_order_helper import SortingDict from . import models logger = logging.getLogger(__name__) -# The linter does not like the length of SortingDictInterface, so these are split here -audited_admin_item_names = ["submitter", "authorizing_official", - "investigator", "creator", "user"] -audited_admin_orderby_names = ['first_name', 'last_name'] -special_audited_admin_item_names = ["domain", "requested_domain"] -special_audited_admin_orderby_names = ["name"] # Used to keep track of how we want to order_by certain FKs -foreignkey_orderby_dict: list[SortingDictInterface] = [ +foreignkey_orderby_dict: list[SortingDict] = [ # foreign_key - order_by - SortingDictInterface( - audited_admin_item_names, - audited_admin_orderby_names - ).sorting_dict, + # Handles fields that are sorted by 'first_name / last_name + SortingDict( + ["submitter", "authorizing_official", "investigator", "creator", "user"], + ['first_name', 'last_name'] + ), # Handles fields that are sorted by 'name' - SortingDictInterface( - special_audited_admin_item_names, - special_audited_admin_orderby_names - ).sorting_dict + SortingDict( + ["domain", "requested_domain"], + ["name"] + ) ] diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py index 2057a2276..fc16f5a31 100644 --- a/src/registrar/models/utility/admin_form_order_helper.py +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -5,16 +5,14 @@ from django.forms import ModelChoiceField logger = logging.getLogger(__name__) -class SortingDictInterface: - _model_list: Dict[type, type] = {} - _sort_list: list[type] = [] - sorting_dict: Dict[type, type] = {} +class SortingDict: + _sorting_dict: Dict[type, type] = {} - # _model_list and _sort_list can be - # any length, and will be called multiple times. - # We want the perf advantage of a dictionary, - # while making creating new SortingDictInterface - # items pretty straight forward and easy (aka as a list) + # model_list can be will be called multiple times. + # Not super necessary, but it'd be nice + # to have the perf advantage of a dictionary, + # while minimizing typing when + # adding a new SortingDict (input as a list) def convert_list_to_dict(self, value_list): dictionary: Dict[type, type] = {} for item in value_list: @@ -22,30 +20,37 @@ class SortingDictInterface: return dictionary def __init__(self, model_list, sort_list): - self.sorting_dict = { + self._sorting_dict = { "dropDownSelected": self.convert_list_to_dict(model_list), "sortBy": sort_list } + def get_dict(self): + # This should never happen so we need to log this + if self._sorting_dict is None: + raise ValueError("_sorting_dict was None") + return self._sorting_dict + class AdminFormOrderHelper(): """A helper class to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" # Used to keep track of how we want to order_by certain FKs - _sorting_dict: list[SortingDictInterface] = [] + _sorting_list: list[SortingDict] = [] def __init__(self, sort): - self._sorting_dict = sort + self._sorting_list = sort def get_ordered_form_field(self, form_field, db_field) -> (ModelChoiceField | None): """Orders the queryset for a ModelChoiceField based on the order_by_dict dictionary""" _order_by_list = [] - for item in self._sorting_dict: - drop_down_selected = item["dropDownSelected"] - sort_by = item["sortBy"] + for item in self._sorting_list: + item_dict = item.get_dict() + drop_down_selected = item_dict.get("dropDownSelected") + sort_by = item_dict.get("sortBy") if db_field.name in drop_down_selected: _order_by_list = sort_by From 080a70e613109f9188c03aeab26b48bfbfd7f868 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 17 Aug 2023 11:24:35 -0600 Subject: [PATCH 148/164] Why must the linter lint? --- src/registrar/admin.py | 10 +++++----- src/registrar/models/domain_information.py | 2 +- .../models/utility/admin_form_order_helper.py | 6 +++++- src/registrar/tests/test_admin.py | 3 ++- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 66215733c..8c9f8512c 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -4,7 +4,7 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect from django.urls import reverse -from registrar.models.utility.admin_form_order_helper import AdminFormOrderHelper, SortingDict +from registrar.models.utility.admin_form_order_helper import AdminFormOrderHelper # Split up for the linter from registrar.models.utility.admin_form_order_helper import SortingDict from . import models @@ -16,13 +16,13 @@ foreignkey_orderby_dict: list[SortingDict] = [ # foreign_key - order_by # Handles fields that are sorted by 'first_name / last_name SortingDict( - ["submitter", "authorizing_official", "investigator", "creator", "user"], - ['first_name', 'last_name'] + ["submitter", "authorizing_official", "investigator", "creator", "user"], + ['first_name', 'last_name'] ), # Handles fields that are sorted by 'name' SortingDict( - ["domain", "requested_domain"], - ["name"] + ["domain", "requested_domain"], + ["name"] ) ] diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 438567003..b12039e73 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -246,4 +246,4 @@ class DomainInformation(TimeStampedModel): return domain_info class Meta: - verbose_name_plural = "Domain Information" \ No newline at end of file + verbose_name_plural = "Domain Information" diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py index fc16f5a31..e017db66f 100644 --- a/src/registrar/models/utility/admin_form_order_helper.py +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -6,6 +6,7 @@ logger = logging.getLogger(__name__) class SortingDict: + """Stores a sorting dictionary object""" _sorting_dict: Dict[type, type] = {} # model_list can be will be called multiple times. @@ -14,6 +15,7 @@ class SortingDict: # while minimizing typing when # adding a new SortingDict (input as a list) def convert_list_to_dict(self, value_list): + """Used internally to convert model_list to a dictionary""" dictionary: Dict[type, type] = {} for item in value_list: dictionary[item] = item @@ -26,6 +28,8 @@ class SortingDict: } def get_dict(self): + """Grabs the associated dictionary item, + has two fields: 'dropDownSelected': model_list and 'sortBy': sort_list""" # This should never happen so we need to log this if self._sorting_dict is None: raise ValueError("_sorting_dict was None") @@ -39,7 +43,7 @@ class AdminFormOrderHelper(): # Used to keep track of how we want to order_by certain FKs _sorting_list: list[SortingDict] = [] - def __init__(self, sort): + def __init__(self, sort: list[SortingDict]): self._sorting_list = sort def get_ordered_form_field(self, form_field, db_field) -> (ModelChoiceField | None): diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index cd66c9bbd..5978d2159 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -583,6 +583,7 @@ class AuditedAdminTest(TestCase): field_name, queryset_shorthand ): + """Handles edge cases for test cases""" if first_name is None: raise ValueError('Invalid value for first_name, must be defined') @@ -591,7 +592,7 @@ class AuditedAdminTest(TestCase): if last_name is None: return (first_name,) - if(first_name.split(queryset_shorthand)[1] == field_name): + if (first_name.split(queryset_shorthand)[1] == field_name): return returned_tuple else: return None From 40b5d7ec5c41552ddfe5e2fc2218ada17c9b60d1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 17 Aug 2023 11:41:07 -0600 Subject: [PATCH 149/164] Black formatting --- src/registrar/admin.py | 9 +- .../models/utility/admin_form_order_helper.py | 7 +- src/registrar/tests/common.py | 94 ++++++++----------- src/registrar/tests/test_admin.py | 85 +++++++---------- 4 files changed, 82 insertions(+), 113 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8c9f8512c..7ff3d8652 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect from django.urls import reverse from registrar.models.utility.admin_form_order_helper import AdminFormOrderHelper + # Split up for the linter from registrar.models.utility.admin_form_order_helper import SortingDict from . import models @@ -17,13 +18,10 @@ foreignkey_orderby_dict: list[SortingDict] = [ # Handles fields that are sorted by 'first_name / last_name SortingDict( ["submitter", "authorizing_official", "investigator", "creator", "user"], - ['first_name', 'last_name'] + ["first_name", "last_name"], ), # Handles fields that are sorted by 'name' - SortingDict( - ["domain", "requested_domain"], - ["name"] - ) + SortingDict(["domain", "requested_domain"], ["name"]), ] @@ -184,6 +182,7 @@ class DomainAdmin(ListHeaderAdmin): class ContactAdmin(ListHeaderAdmin): """Custom contact admin class to add search.""" + search_fields = ["email", "first_name", "last_name"] search_help_text = "Search by firstname, lastname or email." diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py index e017db66f..28e653830 100644 --- a/src/registrar/models/utility/admin_form_order_helper.py +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -7,6 +7,7 @@ logger = logging.getLogger(__name__) class SortingDict: """Stores a sorting dictionary object""" + _sorting_dict: Dict[type, type] = {} # model_list can be will be called multiple times. @@ -24,7 +25,7 @@ class SortingDict: def __init__(self, model_list, sort_list): self._sorting_dict = { "dropDownSelected": self.convert_list_to_dict(model_list), - "sortBy": sort_list + "sortBy": sort_list, } def get_dict(self): @@ -36,7 +37,7 @@ class SortingDict: return self._sorting_dict -class AdminFormOrderHelper(): +class AdminFormOrderHelper: """A helper class to order a dropdown field in Django Admin, takes the fields you want to order by as an array""" @@ -46,7 +47,7 @@ class AdminFormOrderHelper(): def __init__(self, sort: list[SortingDict]): self._sorting_list = sort - def get_ordered_form_field(self, form_field, db_field) -> (ModelChoiceField | None): + def get_ordered_form_field(self, form_field, db_field) -> ModelChoiceField | None: """Orders the queryset for a ModelChoiceField based on the order_by_dict dictionary""" _order_by_list = [] diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index b92fcf351..f838220e8 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -11,6 +11,7 @@ from django.conf import settings from django.contrib.auth import get_user_model, login from registrar.models import Contact, DraftDomain, Website, DomainApplication + # For the linter from registrar.models import DomainInvitation, User, DomainInformation, Domain @@ -118,7 +119,7 @@ class AuditedAdminMockData: user = User.objects.get_or_create( first_name="{} First:{}".format(item_name, shorthand), last_name="{} Last:{}".format(item_name, shorthand), - username="{} username:{}".format(item_name, shorthand) + username="{} username:{}".format(item_name, shorthand), )[0] return user @@ -129,7 +130,7 @@ class AuditedAdminMockData: last_name="{} Last:{}".format(item_name, shorthand), title="{} title:{}".format(item_name, shorthand), email="{}testy@town.com".format(item_name), - phone="(555) 555 5555" + phone="(555) 555 5555", )[0] return contact @@ -143,9 +144,9 @@ class AuditedAdminMockData: def dummy_alt(self, item_name): """Creates a dummy website object for alternates""" - return Website.objects.get_or_create( - website="cityalt{}.gov".format(item_name) - )[0] + return Website.objects.get_or_create(website="cityalt{}.gov".format(item_name))[ + 0 + ] def dummy_current(self, item_name): """Creates a dummy website object for current""" @@ -156,7 +157,7 @@ class AuditedAdminMockData: item_name, org_type="federal", federal_type="executive", - purpose="Purpose of the site" + purpose="Purpose of the site", ): """Generates a generic argument list for most domains""" common_args = dict( @@ -169,11 +170,11 @@ class AuditedAdminMockData: is_policy_acknowledged=True, state_territory="NY", zipcode="10002", - type_of_work='e-Government', + type_of_work="e-Government", anything_else="There is more", - authorizing_official=self.dummy_contact(item_name, 'authorizing_official'), - submitter=self.dummy_contact(item_name, 'submitter'), - creator=self.dummy_user(item_name, 'creator'), + authorizing_official=self.dummy_contact(item_name, "authorizing_official"), + submitter=self.dummy_contact(item_name, "submitter"), + creator=self.dummy_user(item_name, "creator"), ) return common_args @@ -185,14 +186,11 @@ class AuditedAdminMockData: status, org_type="federal", federal_type="executive", - purpose="Purpose of the site" + purpose="Purpose of the site", ): """Returns kwargs for different domain object types""" common_args = self.get_common_domain_arg_dictionary( - item_name, - org_type, - federal_type, - purpose + item_name, org_type, federal_type, purpose ) full_arg_list = None match domain_type: @@ -200,7 +198,7 @@ class AuditedAdminMockData: full_arg_list = dict( **common_args, requested_domain=self.dummy_draft_domain(item_name), - investigator=self.dummy_user(item_name, 'investigator'), + investigator=self.dummy_user(item_name, "investigator"), status=status, ) case self.INFORMATION: @@ -208,62 +206,50 @@ class AuditedAdminMockData: full_arg_list = dict( **common_args, domain=self.dummy_domain(item_name), - domain_application=domain_app + domain_application=domain_app, ) case self.INVITATION: full_arg_list = dict( email="test_mail@mail.com", domain=self.dummy_domain(item_name), - status=DomainInvitation.INVITED + status=DomainInvitation.INVITED, ) return full_arg_list def create_full_dummy_domain_application( - self, - object_name, - status=DomainApplication.STARTED + self, object_name, status=DomainApplication.STARTED ): """Creates a dummy domain application object""" domain_application_kwargs = self.dummy_kwarg_boilerplate( - self.APPLICATION, - object_name, - status - ) + self.APPLICATION, object_name, status + ) application = DomainApplication.objects.get_or_create( - **domain_application_kwargs - )[0] + **domain_application_kwargs + )[0] return application def create_full_dummy_domain_information( - self, - object_name, - status=DomainApplication.STARTED + self, object_name, status=DomainApplication.STARTED ): """Creates a dummy domain information object""" domain_application_kwargs = self.dummy_kwarg_boilerplate( - self.INFORMATION, - object_name, - status - ) + self.INFORMATION, object_name, status + ) application = DomainInformation.objects.get_or_create( - **domain_application_kwargs - )[0] + **domain_application_kwargs + )[0] return application def create_full_dummy_domain_invitation( - self, - object_name, - status=DomainApplication.STARTED + self, object_name, status=DomainApplication.STARTED ): """Creates a dummy domain invitation object""" domain_application_kwargs = self.dummy_kwarg_boilerplate( - self.INVITATION, - object_name, - status - ) + self.INVITATION, object_name, status + ) application = DomainInvitation.objects.get_or_create( - **domain_application_kwargs - )[0] + **domain_application_kwargs + )[0] return application @@ -274,28 +260,28 @@ class AuditedAdminMockData: has_other_contacts=True, has_current_website=True, has_alternative_gov_domain=True, - status=DomainApplication.STARTED + status=DomainApplication.STARTED, ): """A helper to create a dummy domain application object""" application = None match domain_type: case self.APPLICATION: application = self.create_full_dummy_domain_application( - object_name, status - ) + object_name, status + ) case self.INVITATION: application = self.create_full_dummy_domain_invitation( - object_name, status - ) + object_name, status + ) case self.INFORMATION: application = self.create_full_dummy_domain_information( - object_name, status - ) + object_name, status + ) case _: raise ValueError("Invalid domain_type, must conform to given constants") if has_other_contacts and domain_type != self.INVITATION: - other = self.dummy_contact(object_name, 'other') + other = self.dummy_contact(object_name, "other") application.other_contacts.add(other) if has_current_website and domain_type == self.APPLICATION: current = self.dummy_current(object_name) @@ -410,7 +396,7 @@ def completed_application( def multiple_unalphabetical_domain_objects( - domain_type=AuditedAdminMockData.APPLICATION + domain_type=AuditedAdminMockData.APPLICATION, ): """Returns a list of generic domain objects for testing purposes""" applications = [] diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 5978d2159..461b7d217 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1,12 +1,14 @@ from django.test import TestCase, RequestFactory, Client from django.contrib.admin.sites import AdminSite from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin + # Need to split these up due to the linter from registrar.admin import MyUserAdmin, AuditedAdmin from registrar.models import DomainApplication, DomainInformation, User from registrar.models.contact import Contact from registrar.models.domain_invitation import DomainInvitation from .common import completed_application, mock_user, create_superuser, create_user + # Need to split these up due to the linter from .common import multiple_unalphabetical_domain_objects from django.contrib.auth import get_user_model @@ -385,11 +387,7 @@ class AuditedAdminTest(TestCase): self.client = Client(HTTP_HOST="localhost:8080") def order_by_desired_field_helper( - self, - obj_to_sort: AuditedAdmin, - request, - field_name, - *obj_names + self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names ): formatted_sort_fields = [] for obj in obj_names: @@ -397,10 +395,10 @@ class AuditedAdminTest(TestCase): # Not really a fan of how this looks, but as the linter demands... ordered_list = list( - obj_to_sort.get_queryset(request).order_by( - *formatted_sort_fields).values_list( - *formatted_sort_fields) - ) + obj_to_sort.get_queryset(request) + .order_by(*formatted_sort_fields) + .values_list(*formatted_sort_fields) + ) return ordered_list @@ -408,11 +406,11 @@ class AuditedAdminTest(TestCase): # is it worth the time investment to do so? def test_alphabetically_sorted_fk_fields_domain_application(self): tested_fields = [ - DomainApplication.authorizing_official.field, - DomainApplication.submitter.field, - DomainApplication.investigator.field, - DomainApplication.creator.field, - ] + DomainApplication.authorizing_official.field, + DomainApplication.submitter.field, + DomainApplication.investigator.field, + DomainApplication.creator.field, + ] # Creates multiple domain applications - review status does not matter applications = multiple_unalphabetical_domain_objects("application") @@ -428,18 +426,13 @@ class AuditedAdminTest(TestCase): # but both fields are of a fixed length. # For test case purposes, this should be performant. for field in tested_fields: - # We want both of these to be lists, as it is richer test wise. desired_order = self.order_by_desired_field_helper( - model_admin, - request, - field.name, - "first_name", - "last_name" - ) + model_admin, request, field.name, "first_name", "last_name" + ) current_sort_order: Contact = list( model_admin.formfield_for_foreignkey(field, request).queryset - ) + ) # Conforms to the same object structure as desired_order current_sort_order_coerced_type = [] @@ -451,23 +444,23 @@ class AuditedAdminTest(TestCase): first = contact.first_name last = contact.last_name - name_tuple = self.coerced_fk_field_helper(first, last, field.name, ':') + name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") if name_tuple: current_sort_order_coerced_type.append((first, last)) self.assertEqual( desired_order, current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name) + "{} is not ordered alphabetically".format(field.name), ) def test_alphabetically_sorted_fk_fields_domain_information(self): tested_fields = [ - DomainInformation.authorizing_official.field, - DomainInformation.submitter.field, - DomainInformation.domain.field, - DomainInformation.creator.field - ] + DomainInformation.authorizing_official.field, + DomainInformation.submitter.field, + DomainInformation.domain.field, + DomainInformation.creator.field, + ] # Creates multiple domain applications - review status does not matter applications = multiple_unalphabetical_domain_objects("information") @@ -484,19 +477,16 @@ class AuditedAdminTest(TestCase): # but both fields are of a fixed length. # For test case purposes, this should be performant. for field in tested_fields: - isNamefield: bool = (field == DomainInformation.domain.field) - if (isNamefield): + isNamefield: bool = field == DomainInformation.domain.field + if isNamefield: sorted_fields = ["name"] else: sorted_fields = ["first_name", "last_name"] # We want both of these to be lists, as it is richer test wise. desired_order = self.order_by_desired_field_helper( - model_admin, - request, - field.name, - *sorted_fields - ) + model_admin, request, field.name, *sorted_fields + ) current_sort_order = list( model_admin.formfield_for_foreignkey(field, request).queryset ) @@ -515,14 +505,14 @@ class AuditedAdminTest(TestCase): first = contact.name last = None - name_tuple = self.coerced_fk_field_helper(first, last, field.name, ':') + name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") if name_tuple is not None: current_sort_order_coerced_type.append(name_tuple) self.assertEqual( desired_order, current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name) + "{} is not ordered alphabetically".format(field.name), ) def test_alphabetically_sorted_fk_fields_domain_invitation(self): @@ -547,10 +537,7 @@ class AuditedAdminTest(TestCase): # We want both of these to be lists, as it is richer test wise. desired_order = self.order_by_desired_field_helper( - model_admin, - request, - field.name, - *sorted_fields + model_admin, request, field.name, *sorted_fields ) current_sort_order = list( model_admin.formfield_for_foreignkey(field, request).queryset @@ -566,33 +553,29 @@ class AuditedAdminTest(TestCase): first = contact.name last = None - name_tuple = self.coerced_fk_field_helper(first, last, field.name, ':') + name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") if name_tuple is not None: current_sort_order_coerced_type.append(name_tuple) self.assertEqual( desired_order, current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name) + "{} is not ordered alphabetically".format(field.name), ) def coerced_fk_field_helper( - self, - first_name, - last_name, - field_name, - queryset_shorthand + self, first_name, last_name, field_name, queryset_shorthand ): """Handles edge cases for test cases""" if first_name is None: - raise ValueError('Invalid value for first_name, must be defined') + raise ValueError("Invalid value for first_name, must be defined") returned_tuple = (first_name, last_name) # Handles edge case for names - structured strangely if last_name is None: return (first_name,) - if (first_name.split(queryset_shorthand)[1] == field_name): + if first_name.split(queryset_shorthand)[1] == field_name: return returned_tuple else: return None From 4c937f1b0922d9da78d811a55e2d8ce184a7764b Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Thu, 17 Aug 2023 16:00:14 -0400 Subject: [PATCH 150/164] Update contributing doc with branch naming convention and approvals information --- CONTRIBUTING.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d745f76c7..ab15c660f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,6 +9,22 @@ There are a handful of things we do not commit to the repository: - Compliance documentation that includes IP addresses - Secrets of any kind +## Branch naming convention + +For developers, you can auto-deploy your code to your sandbox (if applicable) by naming your branch thusly: jsd/123-feature-description +Where 'jsd' stands for your initials and sandbox environment name (if you were called John Smith Doe), and 123 matches the ticket number if applicable. + +## Approvals + +When a code change is made that is not user facing, then the following is required: +- a developer approves the PR + +When a code change is made that is user facing, beyond content updates, then the following are required: +- a developer approves the PR +- a designer approves the PR or checks off all relevant items in this checklist + +Content or document updates require a single person to approve. + ## Project Management We use [Github Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects) for project management and tracking. From aef9df5296e1ffb388d1869d7a0ff7048b44b367 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 17 Aug 2023 14:55:21 -0600 Subject: [PATCH 151/164] PR changes --- src/registrar/admin.py | 5 +- .../models/utility/admin_form_order_helper.py | 21 ++---- src/registrar/tests/common.py | 75 ++++++++++++------- src/registrar/tests/test_admin.py | 17 +---- 4 files changed, 60 insertions(+), 58 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 7ff3d8652..ed16e25b5 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -4,10 +4,7 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect from django.urls import reverse -from registrar.models.utility.admin_form_order_helper import AdminFormOrderHelper - -# Split up for the linter -from registrar.models.utility.admin_form_order_helper import SortingDict +from registrar.models.utility.admin_form_order_helper import AdminFormOrderHelper, SortingDict # noqa from . import models logger = logging.getLogger(__name__) diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py index 28e653830..fe081fc9b 100644 --- a/src/registrar/models/utility/admin_form_order_helper.py +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -10,24 +10,17 @@ class SortingDict: _sorting_dict: Dict[type, type] = {} - # model_list can be will be called multiple times. - # Not super necessary, but it'd be nice - # to have the perf advantage of a dictionary, - # while minimizing typing when - # adding a new SortingDict (input as a list) - def convert_list_to_dict(self, value_list): - """Used internally to convert model_list to a dictionary""" - dictionary: Dict[type, type] = {} - for item in value_list: - dictionary[item] = item - return dictionary - def __init__(self, model_list, sort_list): self._sorting_dict = { "dropDownSelected": self.convert_list_to_dict(model_list), "sortBy": sort_list, } + # Used in __init__ for model_list for performance reasons + def convert_list_to_dict(self, value_list): + """Used internally to convert model_list to a dictionary""" + return {item: item for item in value_list} + def get_dict(self): """Grabs the associated dictionary item, has two fields: 'dropDownSelected': model_list and 'sortBy': sort_list""" @@ -59,10 +52,12 @@ class AdminFormOrderHelper: if db_field.name in drop_down_selected: _order_by_list = sort_by + # Exit loop when order_by_list is found break # Only order if we choose to do so - if _order_by_list is not None: + # noqa for the linter... reduces readability otherwise + if _order_by_list is not None and _order_by_list != []: # noqa form_field.queryset = form_field.queryset.order_by(*_order_by_list) return form_field diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index f838220e8..0ff255bca 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -10,10 +10,7 @@ from typing import List, Dict from django.conf import settings from django.contrib.auth import get_user_model, login -from registrar.models import Contact, DraftDomain, Website, DomainApplication - -# For the linter -from registrar.models import DomainInvitation, User, DomainInformation, Domain +from registrar.models import Contact, DraftDomain, Website, DomainApplication, DomainInvitation, User, DomainInformation, Domain # noqa logger = logging.getLogger(__name__) @@ -96,39 +93,51 @@ class MockSESClient(Mock): class AuditedAdminMockData: - """Creates simple data mocks for AuditedAdminTest""" + """Creates simple data mocks for AuditedAdminTest. + Can likely be more generalized, but the primary purpose of this class is to simplify + mock data creation, especially for lists of items, + by making the assumption that for most use cases we don't have to worry about + data 'accuracy' ('testy 2' is not an accurate first_name for example), we just care about + implementing some kind of patterning, especially with lists of items. + + Two variables are used across multiple functions: + + *item_name* - Used in patterning. Will be appended en masse to multiple string fields, + like first_name. For example, item_name 'egg' will return a user object of: + + first_name: 'egg first_name:user', + last_name: 'egg last_name:user', + username: 'egg username:user' + + where 'user' is the short_hand + + *short_hand* - Used in patterning. Certain fields will have ':{shorthand}' appended to it, + as a way to optionally include metadata in the string itself. Can be further expanded on. + Came from a bug where different querysets used in testing would effectively be 'anonymized', wherein + it would only display a list of types, but not include the variable name. + """ # noqa # Constants for different domain object types INFORMATION = "information" APPLICATION = "application" INVITATION = "invitation" - # These all can likely be generalized more if necessary, - # particulary with shorthands. - # These are kept basic for now. - # As for why we have shorthands to begin with: - # .queryset returns a list of all objects of the same type, - # rather than by seperating out those fields. - # For such scenarios, the shorthand allows us to not only id a field, - # but append additional information to it. - # This is useful for that scenario and outside it for - # identifying if values swapped unexpectedly - def dummy_user(self, item_name, shorthand): + def dummy_user(self, item_name, short_hand): """Creates a dummy user object, but with a shorthand and support for multiple""" user = User.objects.get_or_create( - first_name="{} First:{}".format(item_name, shorthand), - last_name="{} Last:{}".format(item_name, shorthand), - username="{} username:{}".format(item_name, shorthand), + first_name="{} first_name:{}".format(item_name, short_hand), + last_name="{} last_name:{}".format(item_name, short_hand), + username="{} username:{}".format(item_name, short_hand), )[0] return user - def dummy_contact(self, item_name, shorthand): + def dummy_contact(self, item_name, short_hand): """Creates a dummy contact object""" contact = Contact.objects.get_or_create( - first_name="{} First:{}".format(item_name, shorthand), - last_name="{} Last:{}".format(item_name, shorthand), - title="{} title:{}".format(item_name, shorthand), + first_name="{} first_name:{}".format(item_name, short_hand), + last_name="{} last_name:{}".format(item_name, short_hand), + title="{} title:{}".format(item_name, short_hand), email="{}testy@town.com".format(item_name), phone="(555) 555 5555", )[0] @@ -164,9 +173,9 @@ class AuditedAdminMockData: organization_type=org_type, federal_type=federal_type, purpose=purpose, - organization_name="{} Testorg".format(item_name), - address_line1="{} address 1".format(item_name), - address_line2="{} address 2".format(item_name), + organization_name="{} organization".format(item_name), + address_line1="{} address_line1".format(item_name), + address_line2="{} address_line2".format(item_name), is_policy_acknowledged=True, state_territory="NY", zipcode="10002", @@ -178,7 +187,6 @@ class AuditedAdminMockData: ) return common_args - # This can be boiled down more, though for our purposes this is OK def dummy_kwarg_boilerplate( self, domain_type, @@ -188,7 +196,18 @@ class AuditedAdminMockData: federal_type="executive", purpose="Purpose of the site", ): - """Returns kwargs for different domain object types""" + """ + A helper function that returns premade kwargs for easily creating different domain object types. + There is a decent amount of boilerplate associated with + creating new domain objects (such as domain_application, or domain_information), + so for test case purposes, we can make some assumptions and utilize that to simplify + the object creation process. + + *domain_type* uses constants. Used to identify what kind of 'Domain' object you'd like to make. + + In more detail: domain_type specifies what kind of domain object you'd like to create, i.e. + domain_application (APPLICATION), or domain_information (INFORMATION). + """ common_args = self.get_common_domain_arg_dictionary( item_name, org_type, federal_type, purpose ) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 461b7d217..8f039d9bf 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1,16 +1,10 @@ from django.test import TestCase, RequestFactory, Client from django.contrib.admin.sites import AdminSite -from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin +# noqa is used on all three of these as the linter doesn't like the length of this line +from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin, MyUserAdmin, AuditedAdmin # noqa +from registrar.models import DomainApplication, DomainInformation, User, Contact, DomainInvitation # noqa +from .common import completed_application, mock_user, create_superuser, create_user, multiple_unalphabetical_domain_objects # noqa -# Need to split these up due to the linter -from registrar.admin import MyUserAdmin, AuditedAdmin -from registrar.models import DomainApplication, DomainInformation, User -from registrar.models.contact import Contact -from registrar.models.domain_invitation import DomainInvitation -from .common import completed_application, mock_user, create_superuser, create_user - -# Need to split these up due to the linter -from .common import multiple_unalphabetical_domain_objects from django.contrib.auth import get_user_model from django.conf import settings @@ -393,7 +387,6 @@ class AuditedAdminTest(TestCase): for obj in obj_names: formatted_sort_fields.append("{}__{}".format(field_name, obj)) - # Not really a fan of how this looks, but as the linter demands... ordered_list = list( obj_to_sort.get_queryset(request) .order_by(*formatted_sort_fields) @@ -402,8 +395,6 @@ class AuditedAdminTest(TestCase): return ordered_list - # Q: These three tests can be generalized into an object, - # is it worth the time investment to do so? def test_alphabetically_sorted_fk_fields_domain_application(self): tested_fields = [ DomainApplication.authorizing_official.field, From 9ed83a51da8bb049db885f28b3fbb9fdaf870357 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 17 Aug 2023 15:02:42 -0600 Subject: [PATCH 152/164] Linter changes --- src/registrar/tests/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 0ff255bca..e23384822 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -207,7 +207,7 @@ class AuditedAdminMockData: In more detail: domain_type specifies what kind of domain object you'd like to create, i.e. domain_application (APPLICATION), or domain_information (INFORMATION). - """ + """ # noqa common_args = self.get_common_domain_arg_dictionary( item_name, org_type, federal_type, purpose ) From 2db74e6b007e667bffe8077b0bafd4730bb0d6c9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 17 Aug 2023 15:32:19 -0600 Subject: [PATCH 153/164] Black changes - would not pass otherwise --- src/registrar/admin.py | 5 +++- .../models/utility/admin_form_order_helper.py | 2 +- src/registrar/tests/common.py | 15 ++++++++--- src/registrar/tests/test_admin.py | 25 ++++++++++++++++--- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ed16e25b5..63b57138b 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -4,7 +4,10 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect from django.urls import reverse -from registrar.models.utility.admin_form_order_helper import AdminFormOrderHelper, SortingDict # noqa +from registrar.models.utility.admin_form_order_helper import ( + AdminFormOrderHelper, + SortingDict, +) from . import models logger = logging.getLogger(__name__) diff --git a/src/registrar/models/utility/admin_form_order_helper.py b/src/registrar/models/utility/admin_form_order_helper.py index fe081fc9b..acc26db11 100644 --- a/src/registrar/models/utility/admin_form_order_helper.py +++ b/src/registrar/models/utility/admin_form_order_helper.py @@ -57,7 +57,7 @@ class AdminFormOrderHelper: # Only order if we choose to do so # noqa for the linter... reduces readability otherwise - if _order_by_list is not None and _order_by_list != []: # noqa + if _order_by_list is not None and _order_by_list != []: # noqa form_field.queryset = form_field.queryset.order_by(*_order_by_list) return form_field diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index e23384822..49c4d567f 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -10,7 +10,16 @@ from typing import List, Dict from django.conf import settings from django.contrib.auth import get_user_model, login -from registrar.models import Contact, DraftDomain, Website, DomainApplication, DomainInvitation, User, DomainInformation, Domain # noqa +from registrar.models import ( + Contact, + DraftDomain, + Website, + DomainApplication, + DomainInvitation, + User, + DomainInformation, + Domain, +) logger = logging.getLogger(__name__) @@ -115,7 +124,7 @@ class AuditedAdminMockData: as a way to optionally include metadata in the string itself. Can be further expanded on. Came from a bug where different querysets used in testing would effectively be 'anonymized', wherein it would only display a list of types, but not include the variable name. - """ # noqa + """ # noqa # Constants for different domain object types INFORMATION = "information" @@ -207,7 +216,7 @@ class AuditedAdminMockData: In more detail: domain_type specifies what kind of domain object you'd like to create, i.e. domain_application (APPLICATION), or domain_information (INFORMATION). - """ # noqa + """ # noqa common_args = self.get_common_domain_arg_dictionary( item_name, org_type, federal_type, purpose ) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 8f039d9bf..a1c8486e7 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1,9 +1,26 @@ from django.test import TestCase, RequestFactory, Client from django.contrib.admin.sites import AdminSite -# noqa is used on all three of these as the linter doesn't like the length of this line -from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin, MyUserAdmin, AuditedAdmin # noqa -from registrar.models import DomainApplication, DomainInformation, User, Contact, DomainInvitation # noqa -from .common import completed_application, mock_user, create_superuser, create_user, multiple_unalphabetical_domain_objects # noqa + +from registrar.admin import ( + DomainApplicationAdmin, + ListHeaderAdmin, + MyUserAdmin, + AuditedAdmin, +) +from registrar.models import ( + DomainApplication, + DomainInformation, + User, + Contact, + DomainInvitation, +) +from .common import ( + completed_application, + mock_user, + create_superuser, + create_user, + multiple_unalphabetical_domain_objects, +) from django.contrib.auth import get_user_model From 7cd229fa8855bbac29b76fe98b47c19c1eece0c7 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Thu, 17 Aug 2023 18:47:58 -0400 Subject: [PATCH 154/164] filter applications by excluding approved --- src/registrar/tests/test_views.py | 16 ++++++++++++++++ src/registrar/views/index.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index feb553bf7..b2d6290f6 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1500,3 +1500,19 @@ class TestApplicationStatus(TestWithUser, WebTest): reverse(url_name, kwargs={"pk": application.pk}) ) self.assertEqual(page.status_code, 403) + + +def test_approved_application_not_in_active_requests(self): + """An approved application is not shown in the Active + Requests table on home.html.""" + application = completed_application( + status=DomainApplication.APPROVED, user=self.user + ) + application.save() + + home_page = self.app.get("/") + # This works in our test environemnt because creating + # an approved application here does not generate a + # domain object, so we do not expect to see 'city.gov' + # in either the Domains or Requests tables. + self.assertNotContains(home_page, "city.gov") diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py index 35a67bceb..876983c4e 100644 --- a/src/registrar/views/index.py +++ b/src/registrar/views/index.py @@ -9,7 +9,7 @@ def index(request): context = {} if request.user.is_authenticated: applications = DomainApplication.objects.filter(creator=request.user) - context["domain_applications"] = applications + context["domain_applications"] = applications.exclude(status="approved") domains = request.user.permissions.values( "role", From 3beb23262bed8044e1a0d8669507771936dc1893 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 18 Aug 2023 08:51:04 -0600 Subject: [PATCH 155/164] Sort on domain_application --- src/registrar/admin.py | 2 + src/registrar/tests/common.py | 26 +++---- src/registrar/tests/test_admin.py | 119 +++++++++++++++++------------- 3 files changed, 84 insertions(+), 63 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 63b57138b..a2c8e0c09 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -4,6 +4,7 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect from django.urls import reverse +from registrar.models.domain_application import DomainApplication from registrar.models.utility.admin_form_order_helper import ( AdminFormOrderHelper, SortingDict, @@ -22,6 +23,7 @@ foreignkey_orderby_dict: list[SortingDict] = [ ), # Handles fields that are sorted by 'name' SortingDict(["domain", "requested_domain"], ["name"]), + SortingDict(['domain_application'], ['requested_domain__name']), ] diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 49c4d567f..ea22303ea 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -245,11 +245,11 @@ class AuditedAdminMockData: return full_arg_list def create_full_dummy_domain_application( - self, object_name, status=DomainApplication.STARTED + self, item_name, status=DomainApplication.STARTED ): """Creates a dummy domain application object""" domain_application_kwargs = self.dummy_kwarg_boilerplate( - self.APPLICATION, object_name, status + self.APPLICATION, item_name, status ) application = DomainApplication.objects.get_or_create( **domain_application_kwargs @@ -257,11 +257,11 @@ class AuditedAdminMockData: return application def create_full_dummy_domain_information( - self, object_name, status=DomainApplication.STARTED + self, item_name, status=DomainApplication.STARTED ): """Creates a dummy domain information object""" domain_application_kwargs = self.dummy_kwarg_boilerplate( - self.INFORMATION, object_name, status + self.INFORMATION, item_name, status ) application = DomainInformation.objects.get_or_create( **domain_application_kwargs @@ -269,11 +269,11 @@ class AuditedAdminMockData: return application def create_full_dummy_domain_invitation( - self, object_name, status=DomainApplication.STARTED + self, item_name, status=DomainApplication.STARTED ): """Creates a dummy domain invitation object""" domain_application_kwargs = self.dummy_kwarg_boilerplate( - self.INVITATION, object_name, status + self.INVITATION, item_name, status ) application = DomainInvitation.objects.get_or_create( **domain_application_kwargs @@ -284,7 +284,7 @@ class AuditedAdminMockData: def create_full_dummy_domain_object( self, domain_type, - object_name, + item_name, has_other_contacts=True, has_current_website=True, has_alternative_gov_domain=True, @@ -295,27 +295,27 @@ class AuditedAdminMockData: match domain_type: case self.APPLICATION: application = self.create_full_dummy_domain_application( - object_name, status + item_name, status ) case self.INVITATION: application = self.create_full_dummy_domain_invitation( - object_name, status + item_name, status ) case self.INFORMATION: application = self.create_full_dummy_domain_information( - object_name, status + item_name, status ) case _: raise ValueError("Invalid domain_type, must conform to given constants") if has_other_contacts and domain_type != self.INVITATION: - other = self.dummy_contact(object_name, "other") + other = self.dummy_contact(item_name, "other") application.other_contacts.add(other) if has_current_website and domain_type == self.APPLICATION: - current = self.dummy_current(object_name) + current = self.dummy_current(item_name) application.current_websites.add(current) if has_alternative_gov_domain and domain_type == self.APPLICATION: - alt = self.dummy_alt(object_name) + alt = self.dummy_alt(item_name) application.alternative_domains.add(alt) return application diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index a1c8486e7..f6ef7feeb 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -418,6 +418,7 @@ class AuditedAdminTest(TestCase): DomainApplication.submitter.field, DomainApplication.investigator.field, DomainApplication.creator.field, + DomainApplication.requested_domain.field, ] # Creates multiple domain applications - review status does not matter @@ -430,62 +431,13 @@ class AuditedAdminTest(TestCase): model_admin = AuditedAdmin(DomainApplication, self.site) - # Typically we wouldn't want two nested for fields, - # but both fields are of a fixed length. - # For test case purposes, this should be performant. - for field in tested_fields: - # We want both of these to be lists, as it is richer test wise. - desired_order = self.order_by_desired_field_helper( - model_admin, request, field.name, "first_name", "last_name" - ) - current_sort_order: Contact = list( - model_admin.formfield_for_foreignkey(field, request).queryset - ) - - # Conforms to the same object structure as desired_order - current_sort_order_coerced_type = [] - - # This is necessary as .queryset and get_queryset - # return lists of different types/structures. - # We need to parse this data and coerce them into the same type. - for contact in current_sort_order: - first = contact.first_name - last = contact.last_name - - name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":") - if name_tuple: - current_sort_order_coerced_type.append((first, last)) - - self.assertEqual( - desired_order, - current_sort_order_coerced_type, - "{} is not ordered alphabetically".format(field.name), - ) - - def test_alphabetically_sorted_fk_fields_domain_information(self): - tested_fields = [ - DomainInformation.authorizing_official.field, - DomainInformation.submitter.field, - DomainInformation.domain.field, - DomainInformation.creator.field, - ] - - # Creates multiple domain applications - review status does not matter - applications = multiple_unalphabetical_domain_objects("information") - - # Create a mock request - request = self.factory.post( - "/admin/registrar/domaininformation/{}/change/".format(applications[0].pk) - ) - - model_admin = AuditedAdmin(DomainInformation, self.site) sorted_fields = [] # Typically we wouldn't want two nested for fields, # but both fields are of a fixed length. # For test case purposes, this should be performant. for field in tested_fields: - isNamefield: bool = field == DomainInformation.domain.field + isNamefield: bool = field == DomainApplication.requested_domain.field if isNamefield: sorted_fields = ["name"] else: @@ -523,6 +475,73 @@ class AuditedAdminTest(TestCase): "{} is not ordered alphabetically".format(field.name), ) + def test_alphabetically_sorted_fk_fields_domain_information(self): + tested_fields = [ + DomainInformation.authorizing_official.field, + DomainInformation.submitter.field, + DomainInformation.creator.field, + (DomainInformation.domain.field, ['name']), + (DomainInformation.domain_application.field, ['requested_domain__name']), + ] + # Creates multiple domain applications - review status does not matter + applications = multiple_unalphabetical_domain_objects("information") + + # Create a mock request + request = self.factory.post( + "/admin/registrar/domaininformation/{}/change/".format(applications[0].pk) + ) + + model_admin = AuditedAdmin(DomainInformation, self.site) + + sorted_fields = [] + # Typically we wouldn't want two nested for fields, + # but both fields are of a fixed length. + # For test case purposes, this should be performant. + for field in tested_fields: + isOtherOrderfield: bool = isinstance(field, tuple) + field_obj = None + if isOtherOrderfield: + sorted_fields = field[1] + field_obj = field[0] + else: + sorted_fields = ["first_name", "last_name"] + field_obj = field + # We want both of these to be lists, as it is richer test wise. + desired_order = self.order_by_desired_field_helper( + model_admin, request, field_obj.name, *sorted_fields + ) + current_sort_order = list( + model_admin.formfield_for_foreignkey(field_obj, request).queryset + ) + + # Conforms to the same object structure as desired_order + current_sort_order_coerced_type = [] + + # This is necessary as .queryset and get_queryset + # return lists of different types/structures. + # We need to parse this data and coerce them into the same type. + for obj in current_sort_order: + if not isOtherOrderfield: + first = obj.first_name + last = obj.last_name + elif field_obj == DomainInformation.domain.field: + first = obj.name + last = None + elif field_obj.name == DomainInformation.domain_application.field: + first = obj.requested_domain + last = None + + name_tuple = self.coerced_fk_field_helper(first, last, field_obj.name, ":") + if name_tuple is not None: + logger.debug(name_tuple) + current_sort_order_coerced_type.append(name_tuple) + + self.assertEqual( + desired_order, + current_sort_order_coerced_type, + "{} is not ordered alphabetically".format(field_obj.name), + ) + def test_alphabetically_sorted_fk_fields_domain_invitation(self): tested_fields = [DomainInvitation.domain.field] From 0978c91450159ca5a55b920b922f8a11f16921f8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 18 Aug 2023 09:15:12 -0600 Subject: [PATCH 156/164] Test fix --- src/registrar/admin.py | 3 +-- src/registrar/tests/test_admin.py | 18 ++++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index a2c8e0c09..c1267343e 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -4,7 +4,6 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect from django.urls import reverse -from registrar.models.domain_application import DomainApplication from registrar.models.utility.admin_form_order_helper import ( AdminFormOrderHelper, SortingDict, @@ -23,7 +22,7 @@ foreignkey_orderby_dict: list[SortingDict] = [ ), # Handles fields that are sorted by 'name' SortingDict(["domain", "requested_domain"], ["name"]), - SortingDict(['domain_application'], ['requested_domain__name']), + SortingDict(["domain_application"], ["requested_domain__name"]), ] diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f6ef7feeb..8a288eb15 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -11,7 +11,6 @@ from registrar.models import ( DomainApplication, DomainInformation, User, - Contact, DomainInvitation, ) from .common import ( @@ -431,7 +430,6 @@ class AuditedAdminTest(TestCase): model_admin = AuditedAdmin(DomainApplication, self.site) - sorted_fields = [] # Typically we wouldn't want two nested for fields, # but both fields are of a fixed length. @@ -480,8 +478,8 @@ class AuditedAdminTest(TestCase): DomainInformation.authorizing_official.field, DomainInformation.submitter.field, DomainInformation.creator.field, - (DomainInformation.domain.field, ['name']), - (DomainInformation.domain_application.field, ['requested_domain__name']), + (DomainInformation.domain.field, ["name"]), + (DomainInformation.domain_application.field, ["requested_domain__name"]), ] # Creates multiple domain applications - review status does not matter applications = multiple_unalphabetical_domain_objects("information") @@ -521,19 +519,19 @@ class AuditedAdminTest(TestCase): # return lists of different types/structures. # We need to parse this data and coerce them into the same type. for obj in current_sort_order: + last = None if not isOtherOrderfield: first = obj.first_name last = obj.last_name elif field_obj == DomainInformation.domain.field: first = obj.name - last = None - elif field_obj.name == DomainInformation.domain_application.field: - first = obj.requested_domain - last = None + elif field_obj == DomainInformation.domain_application.field: + first = obj.requested_domain.name - name_tuple = self.coerced_fk_field_helper(first, last, field_obj.name, ":") + name_tuple = self.coerced_fk_field_helper( + first, last, field_obj.name, ":" + ) if name_tuple is not None: - logger.debug(name_tuple) current_sort_order_coerced_type.append(name_tuple) self.assertEqual( From 74571069c3378205fbe9b091c7fd744859980bf2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 18 Aug 2023 15:50:44 -0600 Subject: [PATCH 157/164] AdminSortFields class --- src/registrar/admin.py | 31 ++----------------- .../models/utility/admin_sort_fields.py | 27 ++++++++++++++++ 2 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 src/registrar/models/utility/admin_sort_fields.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index c1267343e..0fa019a13 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -4,29 +4,13 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect from django.urls import reverse -from registrar.models.utility.admin_form_order_helper import ( - AdminFormOrderHelper, - SortingDict, -) +from registrar.models.utility.admin_sort_fields import AdminSortFields from . import models logger = logging.getLogger(__name__) -# Used to keep track of how we want to order_by certain FKs -foreignkey_orderby_dict: list[SortingDict] = [ - # foreign_key - order_by - # Handles fields that are sorted by 'first_name / last_name - SortingDict( - ["submitter", "authorizing_official", "investigator", "creator", "user"], - ["first_name", "last_name"], - ), - # Handles fields that are sorted by 'name' - SortingDict(["domain", "requested_domain"], ["name"]), - SortingDict(["domain_application"], ["requested_domain__name"]), -] - -class AuditedAdmin(admin.ModelAdmin): +class AuditedAdmin(admin.ModelAdmin, AdminSortFields): """Custom admin to make auditing easier.""" def history_view(self, request, object_id, extra_context=None): @@ -42,7 +26,7 @@ class AuditedAdmin(admin.ModelAdmin): def formfield_for_foreignkey(self, db_field, request, **kwargs): """Used to sort dropdown fields alphabetically but can be expanded upon""" form_field = super().formfield_for_foreignkey(db_field, request, **kwargs) - return form_field_order_helper(form_field, db_field) + return self.form_field_order_helper(form_field, db_field) class ListHeaderAdmin(AuditedAdmin): @@ -323,15 +307,6 @@ class DomainApplicationAdmin(ListHeaderAdmin): return self.readonly_fields -# For readability purposes, but can be replaced with a one liner -def form_field_order_helper(form_field, db_field): - """A shorthand for AdminFormOrderHelper(foreignkey_orderby_dict) - .get_ordered_form_field(form_field, db_field)""" - - form = AdminFormOrderHelper(foreignkey_orderby_dict) - return form.get_ordered_form_field(form_field, db_field) - - admin.site.register(models.User, MyUserAdmin) admin.site.register(models.UserDomainRole, AuditedAdmin) admin.site.register(models.Contact, ContactAdmin) diff --git a/src/registrar/models/utility/admin_sort_fields.py b/src/registrar/models/utility/admin_sort_fields.py new file mode 100644 index 000000000..8037c6df0 --- /dev/null +++ b/src/registrar/models/utility/admin_sort_fields.py @@ -0,0 +1,27 @@ +from registrar.models.utility.admin_form_order_helper import ( + AdminFormOrderHelper, + SortingDict, +) + + +class AdminSortFields: + # Used to keep track of how we want to order_by certain FKs + foreignkey_orderby_dict: list[SortingDict] = [ + # foreign_key - order_by + # Handles fields that are sorted by 'first_name / last_name + SortingDict( + ["submitter", "authorizing_official", "investigator", "creator", "user"], + ["first_name", "last_name"], + ), + # Handles fields that are sorted by 'name' + SortingDict(["domain", "requested_domain"], ["name"]), + SortingDict(["domain_application"], ["requested_domain__name"]), + ] + + # For readability purposes, but can be replaced with a one liner + def form_field_order_helper(self, form_field, db_field): + """A shorthand for AdminFormOrderHelper(foreignkey_orderby_dict) + .get_ordered_form_field(form_field, db_field)""" + + form = AdminFormOrderHelper(self.foreignkey_orderby_dict) + return form.get_ordered_form_field(form_field, db_field) From 587702496280c29704de08d4c27729d01067a368 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 18 Aug 2023 16:20:02 -0700 Subject: [PATCH 158/164] changed received to submitted --- src/registrar/templates/application_status.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/application_status.html b/src/registrar/templates/application_status.html index 2488bb449..99f6a1d4c 100644 --- a/src/registrar/templates/application_status.html +++ b/src/registrar/templates/application_status.html @@ -22,7 +22,7 @@ {% if domainapplication.status == 'approved' %} Approved {% elif domainapplication.status == 'in review' %} In Review {% elif domainapplication.status == 'rejected' %} Rejected - {% elif domainapplication.status == 'submitted' %} Received + {% elif domainapplication.status == 'submitted' %} Submitted {% else %}ERROR Please contact technical support/dev {% endif %}

From 892ad926df1f585b1fd57f3f0eaa9199bc4c9abb Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Mon, 21 Aug 2023 12:22:56 -0400 Subject: [PATCH 159/164] Add comment explaining why we're excluding approved applications --- src/registrar/views/index.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py index 876983c4e..273ba8cb0 100644 --- a/src/registrar/views/index.py +++ b/src/registrar/views/index.py @@ -9,6 +9,9 @@ def index(request): context = {} if request.user.is_authenticated: applications = DomainApplication.objects.filter(creator=request.user) + # Let's exclude the approved applications since our + # domain_applications context will be used to populate + # the active applications table context["domain_applications"] = applications.exclude(status="approved") domains = request.user.permissions.values( From 279a986ffdb03b8b9513d96f85e86ed9f96b7e72 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Mon, 21 Aug 2023 16:34:43 -0400 Subject: [PATCH 160/164] Add dk yaml, add dk to workflows, update org name --- .../ISSUE_TEMPLATE/developer-onboarding.md | 2 +- .github/workflows/deploy-sandbox.yaml | 3 +- .github/workflows/deploy-stable.yaml | 2 +- .github/workflows/deploy-staging.yaml | 2 +- .github/workflows/migrate.yaml | 3 +- .github/workflows/reset-db.yaml | 7 +++-- ops/manifests/manifest-dk.yaml | 29 +++++++++++++++++++ ops/scripts/create_dev_sandbox.sh | 12 ++++---- ops/scripts/deploy.sh | 2 +- ops/scripts/destroy_dev_sandbox.sh | 4 +-- ops/scripts/rotate_cloud_secrets.sh | 4 +-- src/registrar/config/settings.py | 1 + src/registrar/fixtures.py | 5 ++++ 13 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 ops/manifests/manifest-dk.yaml diff --git a/.github/ISSUE_TEMPLATE/developer-onboarding.md b/.github/ISSUE_TEMPLATE/developer-onboarding.md index 8d0f9c2d8..94b2a367d 100644 --- a/.github/ISSUE_TEMPLATE/developer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/developer-onboarding.md @@ -37,7 +37,7 @@ cf login -a api.fr.cloud.gov --sso - [ ] Optional- add yourself as a codeowner if desired. See the [Developer readme](https://github.com/cisagov/getgov/blob/main/docs/developer/README.md) for how to do this and what it does. ### Steps for the onboarder -- [ ] Add the onboardee to cloud.gov org (cisa-getgov-prototyping) +- [ ] Add the onboardee to cloud.gov org (cisa-dotgov) - [ ] Setup a [developer specific space for the new developer](#setting-up-developer-sandbox) - [ ] Add the onboardee to our login.gov sandbox team (`.gov Registrar`) via the [dashboard](https://dashboard.int.identitysandbox.gov/) diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index 3b418c4d5..5d9000401 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -18,6 +18,7 @@ jobs: || startsWith(github.head_ref, 'za/') || startsWith(github.head_ref, 'rh/') || startsWith(github.head_ref, 'nl/') + || startsWith(github.head_ref, 'dk/') outputs: environment: ${{ steps.var.outputs.environment}} runs-on: "ubuntu-latest" @@ -52,7 +53,7 @@ jobs: with: cf_username: ${{ secrets[env.CF_USERNAME] }} cf_password: ${{ secrets[env.CF_PASSWORD] }} - cf_org: cisa-getgov-prototyping + cf_org: cisa-dotgov cf_space: ${{ env.ENVIRONMENT }} push_arguments: "-f ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml" comment: diff --git a/.github/workflows/deploy-stable.yaml b/.github/workflows/deploy-stable.yaml index 2f1a2a6b4..0a40ac097 100644 --- a/.github/workflows/deploy-stable.yaml +++ b/.github/workflows/deploy-stable.yaml @@ -36,6 +36,6 @@ jobs: with: cf_username: ${{ secrets.CF_STABLE_USERNAME }} cf_password: ${{ secrets.CF_STABLE_PASSWORD }} - cf_org: cisa-getgov-prototyping + cf_org: cisa-dotgov cf_space: stable push_arguments: "-f ops/manifests/manifest-stable.yaml" diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index 068751c30..1db63e2a2 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -36,6 +36,6 @@ jobs: with: cf_username: ${{ secrets.CF_STAGING_USERNAME }} cf_password: ${{ secrets.CF_STAGING_PASSWORD }} - cf_org: cisa-getgov-prototyping + cf_org: cisa-dotgov cf_space: staging push_arguments: "-f ops/manifests/manifest-staging.yaml" diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index fbfc7f17a..705014af1 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -24,6 +24,7 @@ on: - ab - bl - rjm + - dk jobs: migrate: @@ -37,6 +38,6 @@ jobs: with: cf_username: ${{ secrets[env.CF_USERNAME] }} cf_password: ${{ secrets[env.CF_PASSWORD] }} - cf_org: cisa-getgov-prototyping + cf_org: cisa-dotgov cf_space: ${{ github.event.inputs.environment }} full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate" diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index fea4f19e2..0bf1af2d9 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -25,6 +25,7 @@ on: - ab - bl - rjm + - dk jobs: reset-db: @@ -38,7 +39,7 @@ jobs: with: cf_username: ${{ secrets[env.CF_USERNAME] }} cf_password: ${{ secrets[env.CF_PASSWORD] }} - cf_org: cisa-getgov-prototyping + cf_org: cisa-dotgov cf_space: ${{ github.event.inputs.environment }} full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py flush --no-input' --name flush" @@ -47,7 +48,7 @@ jobs: with: cf_username: ${{ secrets[env.CF_USERNAME] }} cf_password: ${{ secrets[env.CF_PASSWORD] }} - cf_org: cisa-getgov-prototyping + cf_org: cisa-dotgov cf_space: ${{ github.event.inputs.environment }} full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate" @@ -56,6 +57,6 @@ jobs: with: cf_username: ${{ secrets[env.CF_USERNAME] }} cf_password: ${{ secrets[env.CF_PASSWORD] }} - cf_org: cisa-getgov-prototyping + cf_org: cisa-dotgov cf_space: ${{ github.event.inputs.environment }} full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py load' --name loaddata" diff --git a/ops/manifests/manifest-dk.yaml b/ops/manifests/manifest-dk.yaml new file mode 100644 index 000000000..87de8a496 --- /dev/null +++ b/ops/manifests/manifest-dk.yaml @@ -0,0 +1,29 @@ +--- +applications: +- name: getgov-dk + buildpacks: + - python_buildpack + path: ../../src + instances: 1 + memory: 512M + stack: cflinuxfs4 + timeout: 180 + command: ./run.sh + health-check-type: http + health-check-http-endpoint: /health + env: + # Send stdout and stderr straight to the terminal without buffering + PYTHONUNBUFFERED: yup + # Tell Django where to find its configuration + DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: https://getgov-dk.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO + # Public site base URL + GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/ + routes: + - route: getgov-dk.app.cloud.gov + services: + - getgov-credentials + - getgov-dk-database diff --git a/ops/scripts/create_dev_sandbox.sh b/ops/scripts/create_dev_sandbox.sh index f180ada8d..5eeed9c10 100755 --- a/ops/scripts/create_dev_sandbox.sh +++ b/ops/scripts/create_dev_sandbox.sh @@ -21,9 +21,9 @@ then git checkout -b new-dev-sandbox-$1 fi -cf target -o cisa-getgov-prototyping +cf target -o cisa-dotgov -read -p "Are you logged in to the cisa-getgov-prototyping CF org above? (y/n) " -n 1 -r +read -p "Are you logged in to the cisa-dotgov CF org above? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]] then @@ -49,9 +49,9 @@ sed -i '' '/getgov-staging.app.cloud.gov/ {a\ echo "Creating new cloud.gov space for $1..." cf create-space $1 -cf target -o "cisa-getgov-prototyping" -s $1 -cf bind-security-group public_networks_egress cisa-getgov-prototyping --space $1 -cf bind-security-group trusted_local_networks_egress cisa-getgov-prototyping --space $1 +cf target -o "cisa-dotgov" -s $1 +cf bind-security-group public_networks_egress cisa-dotgov --space $1 +cf bind-security-group trusted_local_networks_egress cisa-dotgov --space $1 echo "Creating new cloud.gov DB for $1. This usually takes about 5 minutes..." cf create-service aws-rds micro-psql getgov-$1-database @@ -91,7 +91,7 @@ cd .. cf push getgov-$1 -f ops/manifests/manifest-$1.yaml read -p "Please provide the email of the space developer: " -r -cf set-space-role $REPLY cisa-getgov-prototyping $1 SpaceDeveloper +cf set-space-role $REPLY cisa-dotgov $1 SpaceDeveloper read -p "Should we run migrations? (y/n) " -n 1 -r echo diff --git a/ops/scripts/deploy.sh b/ops/scripts/deploy.sh index d8dd45fbc..50f0f5bfa 100755 --- a/ops/scripts/deploy.sh +++ b/ops/scripts/deploy.sh @@ -4,7 +4,7 @@ ../ops/scripts/build.sh # Deploy to sandbox -cf target -o cisa-getgov-prototyping -s $1 +cf target -o cisa-dotgov -s $1 cf push getgov-$1 -f ../ops/manifests/manifest-$1.yaml # migrations need to be run manually. Developers can use this command diff --git a/ops/scripts/destroy_dev_sandbox.sh b/ops/scripts/destroy_dev_sandbox.sh index 47a7f26d8..9e233b2f1 100755 --- a/ops/scripts/destroy_dev_sandbox.sh +++ b/ops/scripts/destroy_dev_sandbox.sh @@ -20,9 +20,9 @@ then git checkout -b remove-dev-sandbox-$1 fi -cf target -o cisa-getgov-prototyping -s $1 +cf target -o cisa-dotgov -s $1 -read -p "Are you logged in to the cisa-getgov-prototyping CF org above? (y/n) " -n 1 -r +read -p "Are you logged in to the cisa-dotgov CF org above? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]] then diff --git a/ops/scripts/rotate_cloud_secrets.sh b/ops/scripts/rotate_cloud_secrets.sh index aa77c39a8..23e4aa590 100755 --- a/ops/scripts/rotate_cloud_secrets.sh +++ b/ops/scripts/rotate_cloud_secrets.sh @@ -9,8 +9,8 @@ if [ -z "$1" ]; then exit 1 fi -cf target -o cisa-getgov-prototyping -s $1 -read -p "Are you logged in to the cisa-getgov-prototyping CF org above and targeting the correct space? (y/n) " -n 1 -r +cf target -o cisa-dotgov -s $1 +read -p "Are you logged in to the cisa-dotgov CF org above and targeting the correct space? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]] then diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index f6873b226..8b53fa82a 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -580,6 +580,7 @@ ALLOWED_HOSTS = [ "getgov-ab.app.cloud.gov", "getgov-bl.app.cloud.gov", "getgov-rjm.app.cloud.gov", + "getgov-dk.app.cloud.gov", "get.gov", ] diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index 2c94a1eb4..3ef9640fe 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -72,6 +72,11 @@ class UserFixture: "first_name": "Rebecca", "last_name": "Hsieh", }, + { + "username": "fa69c8e8-da83-4798-a4f2-263c9ce93f52", + "first_name": "Dave", + "last_name": "Kennedy", + }, ] STAFF = [ From 5a8c4c072b9367f7048efb8c9959ec22a6b00ac6 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Mon, 21 Aug 2023 16:53:12 -0400 Subject: [PATCH 161/164] Linting and remove Dave from fixtures so he can do it himself --- src/registrar/fixtures.py | 5 ----- src/registrar/views/index.py | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index 3ef9640fe..2c94a1eb4 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -72,11 +72,6 @@ class UserFixture: "first_name": "Rebecca", "last_name": "Hsieh", }, - { - "username": "fa69c8e8-da83-4798-a4f2-263c9ce93f52", - "first_name": "Dave", - "last_name": "Kennedy", - }, ] STAFF = [ diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py index 273ba8cb0..186535aa3 100644 --- a/src/registrar/views/index.py +++ b/src/registrar/views/index.py @@ -10,7 +10,7 @@ def index(request): if request.user.is_authenticated: applications = DomainApplication.objects.filter(creator=request.user) # Let's exclude the approved applications since our - # domain_applications context will be used to populate + # domain_applications context will be used to populate # the active applications table context["domain_applications"] = applications.exclude(status="approved") From d8ca745172168ba99ceb61e9b911406288c726d3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 22 Aug 2023 08:31:26 -0600 Subject: [PATCH 162/164] Comment changes for common.py --- src/registrar/tests/common.py | 144 ++++++++++++++++++++++++++-------- 1 file changed, 113 insertions(+), 31 deletions(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index ea22303ea..fd99b56ea 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -111,7 +111,7 @@ class AuditedAdminMockData: Two variables are used across multiple functions: - *item_name* - Used in patterning. Will be appended en masse to multiple string fields, + *item_name* - Used in patterning. Will be appended en masse to multiple str fields, like first_name. For example, item_name 'egg' will return a user object of: first_name: 'egg first_name:user', @@ -121,7 +121,7 @@ class AuditedAdminMockData: where 'user' is the short_hand *short_hand* - Used in patterning. Certain fields will have ':{shorthand}' appended to it, - as a way to optionally include metadata in the string itself. Can be further expanded on. + as a way to optionally include metadata in the str itself. Can be further expanded on. Came from a bug where different querysets used in testing would effectively be 'anonymized', wherein it would only display a list of types, but not include the variable name. """ # noqa @@ -152,23 +152,64 @@ class AuditedAdminMockData: )[0] return contact - def dummy_draft_domain(self, item_name): - """Creates a dummy draft domain object""" - return DraftDomain.objects.get_or_create(name="city{}.gov".format(item_name))[0] + def dummy_draft_domain(self, item_name, prebuilt=False): + """ + Creates a dummy DraftDomain object + Args: + item_name (str): Value for 'name' in a DraftDomain object. + prebuilt (boolean): Determines return type. + Returns: + DraftDomain: Where name = 'item_name'. If prebuilt = True, then + name will be "city{}.gov".format(item_name). + """ + if prebuilt: + item_name = "city{}.gov".format(item_name) + return DraftDomain.objects.get_or_create(name=item_name)[0] - def dummy_domain(self, item_name): - """Creates a dummy domain object""" - return Domain.objects.get_or_create(name="city{}.gov".format(item_name))[0] + def dummy_domain(self, item_name, prebuilt=False): + """ + Creates a dummy domain object + Args: + item_name (str): Value for 'name' in a Domain object. + prebuilt (boolean): Determines return type. + Returns: + Domain: Where name = 'item_name'. If prebuilt = True, then + domain name will be "city{}.gov".format(item_name). + """ + if prebuilt: + item_name = "city{}.gov".format(item_name) + return Domain.objects.get_or_create(name=item_name)[0] + + def dummy_website(self, item_name): + """ + Creates a dummy website object + Args: + item_name (str): Value for 'website' in a Website object. + Returns: + Website: Where website = 'item_name'. + """ + return Website.objects.get_or_create(website=item_name)[0] def dummy_alt(self, item_name): - """Creates a dummy website object for alternates""" - return Website.objects.get_or_create(website="cityalt{}.gov".format(item_name))[ - 0 - ] + """ + Creates a dummy website object for alternates + Args: + item_name (str): Value for 'website' in a Website object. + Returns: + Website: Where website = "cityalt{}.gov".format(item_name). + """ + return self.dummy_website(item_name="cityalt{}.gov".format(item_name)) def dummy_current(self, item_name): - """Creates a dummy website object for current""" - return Website.objects.get_or_create(website="city{}.com".format(item_name))[0] + """ + Creates a dummy website object for current + Args: + item_name (str): Value for 'website' in a Website object. + prebuilt (boolean): Determines return type. + Returns: + Website: Where website = "city{}.gov".format(item_name) + """ + return self.dummy_website(item_name="city{}.com".format(item_name)) def get_common_domain_arg_dictionary( self, @@ -177,7 +218,36 @@ class AuditedAdminMockData: federal_type="executive", purpose="Purpose of the site", ): - """Generates a generic argument list for most domains""" + """ + Generates a generic argument dict for most domains + Args: + item_name (str): A shared str value appended to first_name, last_name, + organization_name, address_line1, address_line2, + title, email, and username. + + org_type (str - optional): Sets a domains org_type + + federal_type (str - optional): Sets a domains federal_type + + purpose (str - optional): Sets a domains purpose + Returns: + Dictionary: { + organization_type: str, + federal_type: str, + purpose: str, + organization_name: str = "{} organization".format(item_name), + address_line1: str = "{} address_line1".format(item_name), + address_line2: str = "{} address_line2".format(item_name), + is_policy_acknowledged: boolean = True, + state_territory: str = "NY", + zipcode: str = "10002", + type_of_work: str = "e-Government", + anything_else: str = "There is more", + authorizing_official: Contact = self.dummy_contact(item_name, "authorizing_official"), + submitter: Contact = self.dummy_contact(item_name, "submitter"), + creator: User = self.dummy_user(item_name, "creator"), + } + """ # noqa common_args = dict( organization_type=org_type, federal_type=federal_type, @@ -200,30 +270,42 @@ class AuditedAdminMockData: self, domain_type, item_name, - status, + status=DomainApplication.STARTED, org_type="federal", federal_type="executive", purpose="Purpose of the site", ): """ - A helper function that returns premade kwargs for easily creating different domain object types. - There is a decent amount of boilerplate associated with - creating new domain objects (such as domain_application, or domain_information), - so for test case purposes, we can make some assumptions and utilize that to simplify - the object creation process. + Returns a prebuilt kwarg dictionary for DomainApplication, + DomainInformation, or DomainInvitation. + Args: + domain_type (str): is either 'application', 'information', + or 'invitation'. - *domain_type* uses constants. Used to identify what kind of 'Domain' object you'd like to make. + item_name (str): A shared str value appended to first_name, last_name, + organization_name, address_line1, address_line2, + title, email, and username. - In more detail: domain_type specifies what kind of domain object you'd like to create, i.e. - domain_application (APPLICATION), or domain_information (INFORMATION). + status (str - optional): Defines the status for DomainApplication, + e.g. DomainApplication.STARTED + + org_type (str - optional): Sets a domains org_type + + federal_type (str - optional): Sets a domains federal_type + + purpose (str - optional): Sets a domains purpose + Returns: + dict: Returns a dictionary structurally consistent with the expected input + of either DomainApplication, DomainInvitation, or DomainInformation + based on the 'domain_type' field. """ # noqa common_args = self.get_common_domain_arg_dictionary( item_name, org_type, federal_type, purpose ) - full_arg_list = None + full_arg_dict = None match domain_type: case self.APPLICATION: - full_arg_list = dict( + full_arg_dict = dict( **common_args, requested_domain=self.dummy_draft_domain(item_name), investigator=self.dummy_user(item_name, "investigator"), @@ -231,18 +313,18 @@ class AuditedAdminMockData: ) case self.INFORMATION: domain_app = self.create_full_dummy_domain_application(item_name) - full_arg_list = dict( + full_arg_dict = dict( **common_args, - domain=self.dummy_domain(item_name), + domain=self.dummy_domain(item_name, True), domain_application=domain_app, ) case self.INVITATION: - full_arg_list = dict( + full_arg_dict = dict( email="test_mail@mail.com", - domain=self.dummy_domain(item_name), + domain=self.dummy_domain(item_name, True), status=DomainInvitation.INVITED, ) - return full_arg_list + return full_arg_dict def create_full_dummy_domain_application( self, item_name, status=DomainApplication.STARTED From 54fe5384be6fcaa1beb7c6355b4f5bf73419b1b8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 22 Aug 2023 08:34:38 -0600 Subject: [PATCH 163/164] Linter --- src/registrar/tests/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index fd99b56ea..c4a2772b0 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -247,7 +247,7 @@ class AuditedAdminMockData: submitter: Contact = self.dummy_contact(item_name, "submitter"), creator: User = self.dummy_user(item_name, "creator"), } - """ # noqa + """ # noqa common_args = dict( organization_type=org_type, federal_type=federal_type, From f4ce27a4ab8b195c571fc54f68956e602c730a7c Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Tue, 22 Aug 2023 11:58:23 -0400 Subject: [PATCH 164/164] Clean up branch from unrelated work --- src/registrar/tests/test_views.py | 16 ---------------- src/registrar/views/index.py | 5 +---- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index b2d6290f6..feb553bf7 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1500,19 +1500,3 @@ class TestApplicationStatus(TestWithUser, WebTest): reverse(url_name, kwargs={"pk": application.pk}) ) self.assertEqual(page.status_code, 403) - - -def test_approved_application_not_in_active_requests(self): - """An approved application is not shown in the Active - Requests table on home.html.""" - application = completed_application( - status=DomainApplication.APPROVED, user=self.user - ) - application.save() - - home_page = self.app.get("/") - # This works in our test environemnt because creating - # an approved application here does not generate a - # domain object, so we do not expect to see 'city.gov' - # in either the Domains or Requests tables. - self.assertNotContains(home_page, "city.gov") diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py index 186535aa3..35a67bceb 100644 --- a/src/registrar/views/index.py +++ b/src/registrar/views/index.py @@ -9,10 +9,7 @@ def index(request): context = {} if request.user.is_authenticated: applications = DomainApplication.objects.filter(creator=request.user) - # Let's exclude the approved applications since our - # domain_applications context will be used to populate - # the active applications table - context["domain_applications"] = applications.exclude(status="approved") + context["domain_applications"] = applications domains = request.user.permissions.values( "role",