Merge branch 'main' into za/1988-investigate-indexes

This commit is contained in:
zandercymatics 2024-05-28 08:37:20 -06:00
commit 6a393aad51
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
21 changed files with 734 additions and 6142 deletions

View file

@ -22,16 +22,9 @@ jobs:
- name: Compile USWDS assets
working-directory: ./src
run: |
docker compose run node bash -c "\
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
export NVM_DIR=\"\$HOME/.nvm\" && \
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && \
[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" && \
nvm install 21.7.3 && \
nvm use 21.7.3 && \
npm install && \
npx gulp copyAssets && \
npx gulp compile"
docker compose run node npm install &&
docker compose run node npx gulp copyAssets &&
docker compose run node npx gulp compile
- name: Collect static assets
working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input

View file

@ -43,16 +43,9 @@ jobs:
- name: Compile USWDS assets
working-directory: ./src
run: |
docker compose run node bash -c "\
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
export NVM_DIR=\"\$HOME/.nvm\" && \
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && \
[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" && \
nvm install 21.7.3 && \
nvm use 21.7.3 && \
npm install && \
npx gulp copyAssets && \
npx gulp compile"
docker compose run node npm install &&
docker compose run node npx gulp copyAssets &&
docker compose run node npx gulp compile
- name: Collect static assets
working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input

View file

@ -23,16 +23,9 @@ jobs:
- name: Compile USWDS assets
working-directory: ./src
run: |
docker compose run node bash -c "\
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
export NVM_DIR=\"\$HOME/.nvm\" && \
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && \
[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" && \
nvm install 21.7.3 && \
nvm use 21.7.3 && \
npm install && \
npx gulp copyAssets && \
npx gulp compile"
docker compose run node npm install &&
docker compose run node npx gulp copyAssets &&
docker compose run node npx gulp compile
- name: Collect static assets
working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input

View file

@ -23,16 +23,9 @@ jobs:
- name: Compile USWDS assets
working-directory: ./src
run: |
docker compose run node bash -c "\
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
export NVM_DIR=\"\$HOME/.nvm\" && \
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && \
[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" && \
nvm install 21.7.3 && \
nvm use 21.7.3 && \
npm install && \
npx gulp copyAssets && \
npx gulp compile"
docker compose run node npm install &&
docker compose run node npx gulp copyAssets &&
docker compose run node npx gulp compile
- name: Collect static assets
working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input

View file

@ -33,4 +33,5 @@ exports.init = uswds.init;
exports.compile = uswds.compile;
exports.watch = uswds.watch;
exports.copyAssets = uswds.copyAssets
exports.updateUswds = uswds.updateUswds

View file

@ -1,5 +1,4 @@
FROM docker.io/cimg/node:current-browsers
FROM node:21.7.3
WORKDIR /app
# Install app dependencies
@ -7,6 +6,4 @@ WORKDIR /app
# where available (npm@5+)
COPY --chown=circleci:circleci package*.json ./
RUN npm install -g npm@10.5.0
RUN npm install

6614
src/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,11 +3,6 @@
"version": "1.0.0",
"description": "========================",
"main": "index.js",
"engines": {
"node": "21.7.3",
"npm": "10.5.0"
},
"engineStrict": true,
"scripts": {
"pa11y-ci": "pa11y-ci",
"test": "echo \"Error: no test specified\" && exit 1"
@ -15,7 +10,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@uswds/uswds": "^3.3.0",
"@uswds/uswds": "^3.8.0",
"pa11y-ci": "^3.0.1",
"sass": "^1.54.8"
},

View file

@ -594,7 +594,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
None,
{"fields": ("username", "password", "status", "verification_type")},
),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}),
(
"Permissions",
{
@ -625,7 +625,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
)
},
),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}),
(
"Permissions",
{
@ -651,7 +651,9 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
analyst_readonly_fields = [
"Personal Info",
"first_name",
"middle_name",
"last_name",
"title",
"email",
"Permissions",
"is_active",
@ -1124,7 +1126,6 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"Type of organization",
{
"fields": [
"generic_org_type",
"is_election_board",
"organization_type",
]
@ -1171,7 +1172,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
]
# Readonly fields for analysts and superusers
readonly_fields = ("other_contacts", "generic_org_type", "is_election_board")
readonly_fields = ("other_contacts", "is_election_board", "federal_agency")
# Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = [
@ -1385,7 +1386,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"Type of organization",
{
"fields": [
"generic_org_type",
"is_election_board",
"organization_type",
]
@ -1436,8 +1436,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"other_contacts",
"current_websites",
"alternative_domains",
"generic_org_type",
"is_election_board",
"federal_agency",
)
# Read only that we'll leverage for CISA Analysts
@ -1879,7 +1879,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
search_fields = ["name"]
search_help_text = "Search by domain name."
change_form_template = "django/admin/domain_change_form.html"
readonly_fields = ["state", "expiration_date", "first_ready", "deleted"]
readonly_fields = ("state", "expiration_date", "first_ready", "deleted", "federal_agency")
# Table ordering
ordering = ["name"]

View file

@ -385,7 +385,6 @@ class DomainOrgNameAddressForm(forms.ModelForm):
# because for this fields we are creating an individual
# instance of the Select. For the other fields we use the for loop to set
# the class's required attribute to true.
"federal_agency": forms.TextInput,
"organization_name": forms.TextInput,
"address_line1": forms.TextInput,
"address_line2": forms.TextInput,

View file

@ -0,0 +1,37 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
# It is dependent on 0079 (which populates federal agencies)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# [NOT RECOMMENDED]
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
# step 3: fake run the latest migration in the migrations list
# [RECOMMENDED]
# Alternatively:
# step 1: duplicate the migration that loads data
# step 2: docker-compose exec app ./manage.py migrate
from django.db import migrations
from registrar.models import UserGroup
from typing import Any
# For linting: RunPython expects a function reference,
# so let's give it one
def create_groups(apps, schema_editor) -> Any:
UserGroup.create_cisa_analyst_group(apps, schema_editor)
UserGroup.create_full_access_group(apps, schema_editor)
class Migration(migrations.Migration):
dependencies = [
("registrar", "0093_alter_publiccontact_unique_together"),
]
operations = [
migrations.RunPython(
create_groups,
reverse_code=migrations.RunPython.noop,
atomic=True,
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 4.2.10 on 2024-05-22 14:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0094_create_groups_v12"),
]
operations = [
migrations.AddField(
model_name="user",
name="middle_name",
field=models.CharField(blank=True, null=True),
),
migrations.AddField(
model_name="user",
name="title",
field=models.CharField(blank=True, null=True, verbose_name="title / role"),
),
]

View file

@ -9,6 +9,7 @@ from django.db import models
from django_fsm import FSMField, transition # type: ignore
from django.utils import timezone
from registrar.models.domain import Domain
from registrar.models.federal_agency import FederalAgency
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
@ -758,6 +759,10 @@ class DomainRequest(TimeStampedModel):
domain request into an admin on that domain. It also triggers an email
notification."""
if self.federal_agency is None:
self.federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first()
self.save()
# create the domain
Domain = apps.get_model("registrar.Domain")

View file

@ -90,6 +90,17 @@ class User(AbstractUser):
help_text="Phone",
)
middle_name = models.CharField(
null=True,
blank=True,
)
title = models.CharField(
null=True,
blank=True,
verbose_name="title / role",
)
verification_type = models.CharField(
choices=VerificationTypeChoices.choices,
null=True,

View file

@ -81,7 +81,7 @@
<div class="grid-col-auto">
<img class="usa-banner__header-flag" src="{% static 'img/us_flag_small.png' %}" alt="U.S. flag" />
</div>
<div class="grid-col-fill tablet:grid-col-auto">
<div class="grid-col-fill tablet:grid-col-auto" aria-hidden="true">
<p class="usa-banner__header-text">
An official website of the United States government
</p>

View file

@ -93,7 +93,7 @@
</li>
<li class="usa-identifier__required-links-item">
<a rel="noopener noreferrer" target="_blank" href="https://www.dhs.gov/accessibility" class="usa-identifier__required-link usa-link usa-link--external"
>Accessibility</a
>Accessibility statement</a
>
</li>
<li class="usa-identifier__required-links-item">

View file

@ -646,7 +646,7 @@ class TestDomainAdmin(MockEppLib, WebTest):
response = self.client.get("/admin/registrar/domain/")
# There are 4 template references to Federal (4) plus four references in the table
# for our actual domain_request
self.assertContains(response, "Federal", count=54)
self.assertContains(response, "Federal", count=56)
# This may be a bit more robust
self.assertContains(response, '<td class="field-generic_org_type">Federal</td>', count=1)
# Now let's make sure the long description does not exist
@ -2230,8 +2230,8 @@ class TestDomainRequestAdmin(MockEppLib):
"other_contacts",
"current_websites",
"alternative_domains",
"generic_org_type",
"is_election_board",
"federal_agency",
"id",
"created_at",
"updated_at",
@ -2284,8 +2284,8 @@ class TestDomainRequestAdmin(MockEppLib):
"other_contacts",
"current_websites",
"alternative_domains",
"generic_org_type",
"is_election_board",
"federal_agency",
"creator",
"about_your_organization",
"requested_domain",
@ -2312,8 +2312,8 @@ class TestDomainRequestAdmin(MockEppLib):
"other_contacts",
"current_websites",
"alternative_domains",
"generic_org_type",
"is_election_board",
"federal_agency",
]
self.assertEqual(readonly_fields, expected_fields)
@ -3170,8 +3170,8 @@ class TestDomainInformationAdmin(TestCase):
expected_fields = [
"other_contacts",
"generic_org_type",
"is_election_board",
"federal_agency",
"creator",
"type_of_work",
"more_organization_information",
@ -3534,7 +3534,7 @@ class TestMyUserAdmin(TestCase):
)
},
),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}),
("Permissions", {"fields": ("is_active", "groups")}),
("Important dates", {"fields": ("last_login", "date_joined")}),
)

View file

@ -12,6 +12,7 @@ from registrar.models import (
DraftDomain,
DomainInvitation,
UserDomainRole,
FederalAgency,
)
import boto3_mocking
@ -75,6 +76,26 @@ class TestDomainRequest(TestCase):
with less_console_noise():
return self.assertRaises(Exception, None, exception_type)
def test_federal_agency_set_to_non_federal_on_approve(self):
"""Ensures that when the federal_agency field is 'none' when .approve() is called,
the field is set to the 'Non-Federal Agency' record"""
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
name="city2.gov",
federal_agency=None,
)
# Ensure that the federal agency is None
self.assertEqual(domain_request.federal_agency, None)
# Approve the request
domain_request.approve()
self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.APPROVED)
# After approval, it should be "Non-Federal agency"
expected_federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first()
self.assertEqual(domain_request.federal_agency, expected_federal_agency)
def test_empty_create_fails(self):
"""Can't create a completely empty domain request.
NOTE: something about theexception this test raises messes up with the
@ -942,6 +963,7 @@ class TestDomainInformation(TestCase):
domain=domain,
notes="test notes",
domain_request=domain_request,
federal_agency=FederalAgency.objects.get(agency="Non-Federal Agency"),
).__dict__
# Test the two records for consistency

View file

@ -1447,7 +1447,7 @@ class TestDomainOrganization(TestDomainOverview):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
org_name_page.form["federal_agency"] = "Department of State"
org_name_page.form["federal_agency"] = FederalAgency.objects.filter(agency="Department of State").get().id
org_name_page.form["city"] = "Faketown"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
@ -1456,9 +1456,8 @@ class TestDomainOrganization(TestDomainOverview):
success_result_page = org_name_page.form.submit()
self.assertEqual(success_result_page.status_code, 200)
# Check for the old and new value
self.assertContains(success_result_page, federal_agency.id)
self.assertNotContains(success_result_page, "Department of State")
# Check that the agency has not changed
self.assertEqual(self.domain_information.federal_agency.agency, "AMTRAK")
# Do another check on the form itself
form = success_result_page.forms[0]

View file

@ -32,8 +32,9 @@ def send_templated_email(
template_name and subject_template_name are relative to the same template
context as Django's HTML templates. context gives additional information
that the template may use.
Raises EmailSendingError if SES client could not be accessed
"""
logger.info(f"An email was sent! Template name: {template_name} to {to_address}")
template = get_template(template_name)
email_body = template.render(context=context)
@ -48,7 +49,9 @@ def send_templated_email(
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=settings.BOTO_CONFIG,
)
logger.info(f"An email was sent! Template name: {template_name} to {to_address}")
except Exception as exc:
logger.debug("E-mail unable to send! Could not access the SES client.")
raise EmailSendingError("Could not access the SES client.") from exc
destination = {"ToAddresses": [to_address]}

View file

@ -734,7 +734,10 @@ class DomainAddUserView(DomainFormBaseView):
does not make a domain information object
email: string- email to send to
add_success: bool- default True indicates:
adding a success message to the view if the email sending succeeds"""
adding a success message to the view if the email sending succeeds
raises EmailSendingError
"""
# Set a default email address to send to for staff
requestor_email = settings.DEFAULT_FROM_EMAIL
@ -762,33 +765,43 @@ class DomainAddUserView(DomainFormBaseView):
"requestor_email": requestor_email,
},
)
except EmailSendingError:
messages.warning(self.request, "Could not send email invitation.")
except EmailSendingError as exc:
logger.warn(
"Could not sent email invitation to %s for domain %s",
email,
self.object,
exc_info=True,
)
raise EmailSendingError("Could not send email invitation.") from exc
else:
if add_success:
messages.success(self.request, f"{email} has been invited to this domain.")
def _make_invitation(self, email_address: str, requestor: User):
"""Make a Domain invitation for this email and redirect with a message."""
invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
if not created:
# Check to see if an invite has already been sent (NOTE: we do not want to create an invite just yet.)
try:
invite = DomainInvitation.objects.get(email=email_address, domain=self.object)
# that invitation already existed
if invite is not None:
messages.warning(
self.request,
f"{email_address} has already been invited to this domain.",
)
else:
except DomainInvitation.DoesNotExist:
# Try to send the invitation. If it succeeds, add it to the DomainInvitation table.
try:
self._send_domain_invitation_email(email=email_address, requestor=requestor)
except EmailSendingError:
messages.warning(self.request, "Could not send email invitation.")
else:
# (NOTE: only create a domainInvitation if the e-mail sends correctly)
DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
return redirect(self.get_success_url())
def form_valid(self, form):
"""Add the specified user on this domain."""
"""Add the specified user on this domain.
Throws EmailSendingError."""
requested_email = form.cleaned_data["email"]
requestor = self.request.user
# look up a user with that email
@ -799,7 +812,22 @@ class DomainAddUserView(DomainFormBaseView):
return self._make_invitation(requested_email, requestor)
else:
# if user already exists then just send an email
try:
self._send_domain_invitation_email(requested_email, requestor, add_success=False)
except EmailSendingError:
logger.warn(
"Could not send email invitation (EmailSendingError)",
self.object,
exc_info=True,
)
messages.warning(self.request, "Could not send email invitation.")
except Exception:
logger.warn(
"Could not send email invitation (Other Exception)",
self.object,
exc_info=True,
)
messages.warning(self.request, "Could not send email invitation.")
try:
UserDomainRole.objects.create(