merge main

This commit is contained in:
Rachid Mrad 2024-05-28 13:29:22 -04:00
commit 2a6f1720be
No known key found for this signature in database
21 changed files with 694 additions and 6165 deletions

View file

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

View file

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

View file

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

View file

@ -23,16 +23,9 @@ jobs:
- name: Compile USWDS assets - name: Compile USWDS assets
working-directory: ./src working-directory: ./src
run: | run: |
docker compose run node bash -c "\ docker compose run node npm install &&
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \ docker compose run node npx gulp copyAssets &&
export NVM_DIR=\"\$HOME/.nvm\" && \ docker compose run node npx gulp compile
[ -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"
- name: Collect static assets - name: Collect static assets
working-directory: ./src working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input 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.compile = uswds.compile;
exports.watch = uswds.watch; exports.watch = uswds.watch;
exports.copyAssets = uswds.copyAssets exports.copyAssets = uswds.copyAssets
exports.updateUswds = uswds.updateUswds

View file

@ -1,5 +1,4 @@
FROM docker.io/cimg/node:current-browsers FROM docker.io/cimg/node:current-browsers
FROM node:21.7.3
WORKDIR /app WORKDIR /app
# Install app dependencies # Install app dependencies
@ -7,6 +6,4 @@ WORKDIR /app
# where available (npm@5+) # where available (npm@5+)
COPY --chown=circleci:circleci package*.json ./ COPY --chown=circleci:circleci package*.json ./
RUN npm install -g npm@10.5.0
RUN npm install 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", "version": "1.0.0",
"description": "========================", "description": "========================",
"main": "index.js", "main": "index.js",
"engines": {
"node": "21.7.3",
"npm": "10.5.0"
},
"engineStrict": true,
"scripts": { "scripts": {
"pa11y-ci": "pa11y-ci", "pa11y-ci": "pa11y-ci",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
@ -15,7 +10,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@uswds/uswds": "^3.3.0", "@uswds/uswds": "^3.8.0",
"pa11y-ci": "^3.0.1", "pa11y-ci": "^3.0.1",
"sass": "^1.54.8" "sass": "^1.54.8"
}, },

View file

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

View file

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

View file

@ -43,7 +43,11 @@ class UserProfileForm(forms.ModelForm):
self.fields[field_name].required = True self.fields[field_name].required = True
# Set custom form label # Set custom form label
self.fields["first_name"].label = "First name / given name"
self.fields["middle_name"].label = "Middle name (optional)" self.fields["middle_name"].label = "Middle name (optional)"
self.fields["last_name"].label = "Last name / family name"
self.fields["title"].label = "Title or role in your organization"
self.fields["email"].label = "Organizational email"
# Set custom error messages # Set custom error messages
self.fields["first_name"].error_messages = {"required": "Enter your first name / given name."} self.fields["first_name"].error_messages = {"required": "Enter your first name / given name."}

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

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

View file

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

View file

@ -93,7 +93,7 @@
</li> </li>
<li class="usa-identifier__required-links-item"> <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" <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>
<li class="usa-identifier__required-links-item"> <li class="usa-identifier__required-links-item">

View file

@ -32,7 +32,7 @@ Edit your User Profile |
{% include "includes/form_errors.html" with form=form %} {% include "includes/form_errors.html" with form=form %}
<h1>Your profile</h1> <h1>Your profile</h1>
<p>We <a href="https://get.gov/domains/requirements/#what-.gov-domain-registrants-must-do" target="_blank">require</a> that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and wont be made public.</p> <p>We <a href="{% public_site_url 'domains/requirements/#what-.gov-domain-registrants-must-do' %}" target="_blank">require</a> that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and wont be made public.</p>
<h2>Contact information</h2> <h2>Contact information</h2>
<p>Review the details below and update any required information. Note that editing this information wont affect your Login.gov account information.</p> <p>Review the details below and update any required information. Note that editing this information wont affect your Login.gov account information.</p>
@ -41,7 +41,6 @@ Edit your User Profile |
<form class="usa-form usa-form--large" method="post" novalidate> <form class="usa-form usa-form--large" method="post" novalidate>
{% csrf_token %} {% csrf_token %}
<fieldset class="usa-fieldset">
{% input_with_errors form.first_name %} {% input_with_errors form.first_name %}
@ -69,7 +68,6 @@ Edit your User Profile |
{% input_with_errors form.phone %} {% input_with_errors form.phone %}
{% endwith %} {% endwith %}
</fieldset>
<button type="submit" class="usa-button">Save</button> <button type="submit" class="usa-button">Save</button>
</form> </form>
</main> </main>

View file

@ -2230,8 +2230,8 @@ class TestDomainRequestAdmin(MockEppLib):
"other_contacts", "other_contacts",
"current_websites", "current_websites",
"alternative_domains", "alternative_domains",
"generic_org_type",
"is_election_board", "is_election_board",
"federal_agency",
"id", "id",
"created_at", "created_at",
"updated_at", "updated_at",
@ -2284,8 +2284,8 @@ class TestDomainRequestAdmin(MockEppLib):
"other_contacts", "other_contacts",
"current_websites", "current_websites",
"alternative_domains", "alternative_domains",
"generic_org_type",
"is_election_board", "is_election_board",
"federal_agency",
"creator", "creator",
"about_your_organization", "about_your_organization",
"requested_domain", "requested_domain",
@ -2312,8 +2312,8 @@ class TestDomainRequestAdmin(MockEppLib):
"other_contacts", "other_contacts",
"current_websites", "current_websites",
"alternative_domains", "alternative_domains",
"generic_org_type",
"is_election_board", "is_election_board",
"federal_agency",
] ]
self.assertEqual(readonly_fields, expected_fields) self.assertEqual(readonly_fields, expected_fields)
@ -3170,8 +3170,8 @@ class TestDomainInformationAdmin(TestCase):
expected_fields = [ expected_fields = [
"other_contacts", "other_contacts",
"generic_org_type",
"is_election_board", "is_election_board",
"federal_agency",
"creator", "creator",
"type_of_work", "type_of_work",
"more_organization_information", "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")}), ("Permissions", {"fields": ("is_active", "groups")}),
("Important dates", {"fields": ("last_login", "date_joined")}), ("Important dates", {"fields": ("last_login", "date_joined")}),
) )

View file

@ -528,6 +528,8 @@ class UserProfileTests(TestWithUser, WebTest):
self.role.delete() self.role.delete()
self.domain.delete() self.domain.delete()
Contact.objects.all().delete() Contact.objects.all().delete()
DraftDomain.objects.all().delete()
DomainRequest.objects.all().delete()
@less_console_noise_decorator @less_console_noise_decorator
def error_500_main_nav_with_profile_feature_turned_on(self): def error_500_main_nav_with_profile_feature_turned_on(self):
@ -635,9 +637,6 @@ class UserProfileTests(TestWithUser, WebTest):
self.assertContains(response, "Your profile") self.assertContains(response, "Your profile")
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw") response = self.client.get(f"/domain-request/{domain_request.id}/withdraw")
self.assertContains(response, "Your profile") self.assertContains(response, "Your profile")
# cleanup
domain_request.delete()
site.delete()
@less_console_noise_decorator @less_console_noise_decorator
def test_request_when_profile_feature_off(self): def test_request_when_profile_feature_off(self):

View file

@ -1447,7 +1447,7 @@ class TestDomainOrganization(TestDomainOverview):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] 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" org_name_page.form["city"] = "Faketown"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) 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() success_result_page = org_name_page.form.submit()
self.assertEqual(success_result_page.status_code, 200) self.assertEqual(success_result_page.status_code, 200)
# Check for the old and new value # Check that the agency has not changed
self.assertContains(success_result_page, federal_agency.id) self.assertEqual(self.domain_information.federal_agency.agency, "AMTRAK")
self.assertNotContains(success_result_page, "Department of State")
# Do another check on the form itself # Do another check on the form itself
form = success_result_page.forms[0] 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 template_name and subject_template_name are relative to the same template
context as Django's HTML templates. context gives additional information context as Django's HTML templates. context gives additional information
that the template may use. 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) template = get_template(template_name)
email_body = template.render(context=context) email_body = template.render(context=context)
@ -48,7 +49,9 @@ def send_templated_email(
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=settings.BOTO_CONFIG, config=settings.BOTO_CONFIG,
) )
logger.info(f"An email was sent! Template name: {template_name} to {to_address}")
except Exception as exc: 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 raise EmailSendingError("Could not access the SES client.") from exc
destination = {"ToAddresses": [to_address]} destination = {"ToAddresses": [to_address]}

View file

@ -745,7 +745,10 @@ class DomainAddUserView(DomainFormBaseView):
does not make a domain information object does not make a domain information object
email: string- email to send to email: string- email to send to
add_success: bool- default True indicates: 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 # Set a default email address to send to for staff
requestor_email = settings.DEFAULT_FROM_EMAIL requestor_email = settings.DEFAULT_FROM_EMAIL
@ -773,33 +776,43 @@ class DomainAddUserView(DomainFormBaseView):
"requestor_email": requestor_email, "requestor_email": requestor_email,
}, },
) )
except EmailSendingError: except EmailSendingError as exc:
messages.warning(self.request, "Could not send email invitation.")
logger.warn( logger.warn(
"Could not sent email invitation to %s for domain %s", "Could not sent email invitation to %s for domain %s",
email, email,
self.object, self.object,
exc_info=True, exc_info=True,
) )
raise EmailSendingError("Could not send email invitation.") from exc
else: else:
if add_success: if add_success:
messages.success(self.request, f"{email} has been invited to this domain.") messages.success(self.request, f"{email} has been invited to this domain.")
def _make_invitation(self, email_address: str, requestor: User): def _make_invitation(self, email_address: str, requestor: User):
"""Make a Domain invitation for this email and redirect with a message.""" """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) # Check to see if an invite has already been sent (NOTE: we do not want to create an invite just yet.)
if not created: try:
invite = DomainInvitation.objects.get(email=email_address, domain=self.object)
# that invitation already existed # that invitation already existed
if invite is not None:
messages.warning( messages.warning(
self.request, self.request,
f"{email_address} has already been invited to this domain.", 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) 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()) return redirect(self.get_success_url())
def form_valid(self, form): 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"] requested_email = form.cleaned_data["email"]
requestor = self.request.user requestor = self.request.user
# look up a user with that email # look up a user with that email
@ -810,7 +823,22 @@ class DomainAddUserView(DomainFormBaseView):
return self._make_invitation(requested_email, requestor) return self._make_invitation(requested_email, requestor)
else: else:
# if user already exists then just send an email # if user already exists then just send an email
try:
self._send_domain_invitation_email(requested_email, requestor, add_success=False) 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: try:
UserDomainRole.objects.create( UserDomainRole.objects.create(