Merge branch 'main' into za/813-sort-dropdowns-alphabetically

This commit is contained in:
zandercymatics 2023-08-22 10:54:20 -06:00
commit 251b8291f9
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
19 changed files with 375 additions and 155 deletions

View file

@ -24,13 +24,13 @@ There are several tools we use locally that you will need to have.
### Steps for the onboardee ### Steps for the onboardee
- [ ] Setup [commit signing in Github](#setting-up-commit-signing) and with git locally. - [ ] 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/) - [ ] [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 - [ ] Ensure you can login to your cloud.gov account via the CLI
```bash ```bash
cf login -a api.fr.cloud.gov --sso 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 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. **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 ### Steps for the onboarder
- [ ] Add the onboardee to cloud.gov org (cisa-getgov-prototyping) - [ ] Add the onboardee to cloud.gov org (cisa-getgov-prototyping)
- [ ] Setup a [developer specific space for the new developer](#setting-up-developer-sandbox) - [ ] 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 ## 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) - [ ] [Architecture Decision Records](https://github.com/cisagov/dotgov/tree/main/docs/architecture/decisions)
- [ ] [Contributing Policy](https://github.com/cisagov/dotgov/tree/main/CONTRIBUTING.md) - [ ] [Contributing Policy](https://github.com/cisagov/dotgov/tree/main/CONTRIBUTING.md)

View file

@ -1,13 +1,134 @@
# <!-- Use the title to describe PR changes in the imperative mood --> # ## Ticket
## 🗣 Description ## Resolves #00
<!--Reminder, 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.
<!-- Describe the "what" of your changes in detail. --> All other changes require just a single approving review.-->
<!-- Please link to any relevant issues. -->
## 💭 Motivation and context ## ## Changes
<!-- Why is this change required? --> <!-- What was added, updated, or removed in this PR. -->
<!-- What problem does this change solve? How did you solve it? --> - Change 1
<!-- Mention any related issue(s) here using appropriate keywords such --> - Change 2
<!-- as "closes" or "resolves" to auto-close them on merge. -->
<!--
Please add/remove/edit any of the template below to fit the needs
of this specific PR.
--->
## Context for reviewers
<!--Background context, more in-depth details of the implementation, and anything else you'd like to call out or ask reviewers. -->
## Setup
<!-- Add any steps or code to run in this section to help others run your code.
Example 1:
```sh
echo "Code goes here"
```
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 <insert location>
-notice user is redirected to <proper end location>
-->
## 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
- [ ] 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)
- [ ] 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
### As a code reviewer, 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
- [ ] 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)
- [ ] All new functions and methods are commented using plain language
- [ ] 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
- [ ] 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)
- [ ] 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) Tested as both an analyst and applicant user
**Note:** Multiple code reviewers can share the checklists above, a second reviewers should not make a duplicate checklist
### As a designer reviewer, I have
#### Verified that the changes match the design intention
- [ ] Checked that 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)
- [ ] Tested with multiple browsers (check off which ones were used)
- [ ] Chrome
- [ ] Microsoft Edge
- [ ] FireFox
- [ ] Safari
- [ ] (Rarely needed) Tested as both an analyst and applicant user
## Screenshots
<!-- If this PR makes visible interface changes, an image of the finished interface can help reviewers
and casual observers understand the context of the changes.
A before image is optional and can be included at the submitter's discretion.
Consider using an animated image to show an entire workflow.
You may want to use [GIPHY Capture](https://giphy.com/apps/giphycapture) for this! 📸
_Please frame images to show useful context but also highlight the affected regions._
--->

View file

@ -9,6 +9,22 @@ There are a handful of things we do not commit to the repository:
- Compliance documentation that includes IP addresses - Compliance documentation that includes IP addresses
- Secrets of any kind - 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 ## 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. 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.

View file

@ -34,6 +34,13 @@ 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 involve _building up_, leveraging Django Admin will require carefully _pairing back_ the functionalities available to
users such as analysts. users such as analysts.
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)
is easy and worthwhile, while overwriting Django's templates with USWDS is hard and provides little return on investment
([research PR](https://github.com/cisagov/getgov/pull/831)).
While we anticipate that Django Admin will meet (or even exceed) the user needs that we are aware of today, it is still While we anticipate that Django Admin will meet (or even exceed) the user needs that we are aware of today, it is still
an open question whether Django Admin will be the long-term administrator tool of choice. A pivot away from Django Admin an open question whether Django Admin will be the long-term administrator tool of choice. A pivot away from Django Admin
in the future would of course mean starting from scratch at a later date, and potentially juggling two separate admin in the future would of course mean starting from scratch at a later date, and potentially juggling two separate admin

View file

@ -18,9 +18,19 @@ 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). 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.
### Troubleshooting
* 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` * 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) * 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)
* 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 involving `puppeteer`, try adding this line below into your `.bashrc` or `.zshrc`.
```
export DOCKER_DEFAULT_PLATFORM=linux/amd64
```
When completed, don't forget to rerun `docker-compose up`!
## Branch Conventions ## Branch Conventions
@ -84,6 +94,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. 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 ### 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: 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 +116,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. 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` 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) ## Adding to CODEOWNERS (optional)

View file

@ -277,22 +277,27 @@ class DomainApplicationAdmin(ListHeaderAdmin):
pass pass
elif obj.status == models.DomainApplication.SUBMITTED: elif obj.status == models.DomainApplication.SUBMITTED:
# This is an fsm in model which will throw an error if the # This is an fsm in model which will throw an error if the
# transition condition is violated, so we call it on the # transition condition is violated, so we roll back the
# original object which has the right status value, and pass # status to what it was before the admin user changed it and
# the updated object which contains the up-to-date data # let the fsm method set it. Same comment applies to
# for the side effects (like an email send). Same # transition method calls below.
# comment applies to original_obj method calls below. obj.status = original_obj.status
original_obj.submit(updated_domain_application=obj) obj.submit()
elif obj.status == models.DomainApplication.IN_REVIEW: 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: 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: 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: elif obj.status == models.DomainApplication.WITHDRAWN:
original_obj.withdraw() obj.status = original_obj.status
obj.withdraw()
elif obj.status == models.DomainApplication.REJECTED: elif obj.status == models.DomainApplication.REJECTED:
original_obj.reject(updated_domain_application=obj) obj.status = original_obj.status
obj.reject()
else: else:
logger.warning("Unknown status selected in django admin") logger.warning("Unknown status selected in django admin")

View file

@ -105,19 +105,29 @@ html[data-theme="light"] {
} }
// Dark mode django (bug due to scss cascade) and USWDS tables // Dark mode django (bug due to scss cascade) and USWDS tables
body,
.change-list .usa-table, .change-list .usa-table,
.change-list .usa-table--striped tbody tr:nth-child(odd) td { .change-list .usa-table--striped tbody tr:nth-child(odd) td,
color: var(--body-fg)!important; .change-list .usa-table--borderless thead th,
.change-list .usa-table thead td,
.change-list .usa-table thead th,
body.dashboard,
body.change-list,
body.change-form {
color: var(--body-fg);
} }
} }
// Firefox needs this to be specifically set // Firefox needs this to be specifically set
html[data-theme="dark"] { html[data-theme="dark"] {
body,
.change-list .usa-table, .change-list .usa-table,
.change-list .usa-table--striped tbody tr:nth-child(odd) td { .change-list .usa-table--striped tbody tr:nth-child(odd) td,
color: var(--body-fg)!important; .change-list .usa-table--borderless thead th,
.change-list .usa-table thead td,
.change-list .usa-table thead th,
body.dashboard,
body.change-list,
body.change-form {
color: var(--body-fg);
} }
} }

View file

@ -4,7 +4,6 @@ For more information see:
https://docs.djangoproject.com/en/4.0/topics/http/urls/ https://docs.djangoproject.com/en/4.0/topics/http/urls/
""" """
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.generic import RedirectView from django.views.generic import RedirectView
@ -45,6 +44,10 @@ for step, view in [
urlpatterns = [ urlpatterns = [
path("", views.index, name="home"), path("", views.index, name="home"),
path(
"admin/logout/",
RedirectView.as_view(pattern_name="logout", permanent=False),
),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path( path(
"application/<id>/edit/", "application/<id>/edit/",
@ -114,20 +117,6 @@ urlpatterns = [
), ),
] ]
if not settings.DEBUG:
urlpatterns += [
# redirect to login.gov
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 # 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 # 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 # was actually True. Instead, let's add these URLs any time we are able to

View file

@ -67,6 +67,11 @@ class UserFixture:
"first_name": "Paul", "first_name": "Paul",
"last_name": "Kuykendall", "last_name": "Kuykendall",
}, },
{
"username": "2a88a97b-be96-4aad-b99e-0b605b492c78",
"first_name": "Rebecca",
"last_name": "Hsieh",
},
] ]
STAFF = [ STAFF = [
@ -74,6 +79,7 @@ class UserFixture:
"username": "319c490d-453b-43d9-bc4d-7d6cd8ff6844", "username": "319c490d-453b-43d9-bc4d-7d6cd8ff6844",
"first_name": "Rachid-Analyst", "first_name": "Rachid-Analyst",
"last_name": "Mrad-Analyst", "last_name": "Mrad-Analyst",
"email": "rachid.mrad@gmail.com",
}, },
{ {
"username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd", "username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd",
@ -90,6 +96,11 @@ class UserFixture:
"first_name": "Paul-Analyst", "first_name": "Paul-Analyst",
"last_name": "Kuykendall-Analyst", "last_name": "Kuykendall-Analyst",
}, },
{
"username": "e474e7a9-71ca-449d-833c-8a6e094dd117",
"first_name": "Rebecca-Analyst",
"last_name": "Hsieh-Analyst",
},
] ]
STAFF_PERMISSIONS = [ STAFF_PERMISSIONS = [
@ -119,6 +130,8 @@ class UserFixture:
user.is_superuser = True user.is_superuser = True
user.first_name = admin["first_name"] user.first_name = admin["first_name"]
user.last_name = admin["last_name"] user.last_name = admin["last_name"]
if "email" in admin.keys():
user.email = admin["email"]
user.is_staff = True user.is_staff = True
user.is_active = True user.is_active = True
user.save() user.save()
@ -136,6 +149,8 @@ class UserFixture:
user.is_superuser = False user.is_superuser = False
user.first_name = staff["first_name"] user.first_name = staff["first_name"]
user.last_name = staff["last_name"] user.last_name = staff["last_name"]
if "email" in admin.keys():
user.email = admin["email"]
user.is_staff = True user.is_staff = True
user.is_active = True user.is_active = True

View file

@ -504,15 +504,10 @@ class DomainApplication(TimeStampedModel):
@transition( @transition(
field="status", source=[STARTED, ACTION_NEEDED, WITHDRAWN], target=SUBMITTED 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. """Submit an application that is started.
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."""
# check our conditions here inside the `submit` method so that we # check our conditions here inside the `submit` method so that we
# can raise more informative exceptions # can raise more informative exceptions
@ -528,46 +523,31 @@ class DomainApplication(TimeStampedModel):
if not DraftDomain.string_could_be_domain(self.requested_domain.name): if not DraftDomain.string_could_be_domain(self.requested_domain.name):
raise ValueError("Requested domain is not a valid domain name.") raise ValueError("Requested domain is not a valid domain name.")
if updated_domain_application is not None: self._send_status_update_email(
# A DomainApplication is being passed to this method (ie from admin) "submission confirmation",
updated_domain_application._send_status_update_email( "emails/submission_confirmation.txt",
"submission confirmation", "emails/submission_confirmation_subject.txt",
"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",
)
@transition(field="status", source=SUBMITTED, target=IN_REVIEW) @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. """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 self._send_status_update_email(
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(
"application in review", "application in review",
"emails/status_change_in_review.txt", "emails/status_change_in_review.txt",
"emails/status_change_in_review_subject.txt", "emails/status_change_in_review_subject.txt",
) )
@transition(field="status", source=[IN_REVIEW, REJECTED], target=ACTION_NEEDED) @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. """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", "action needed",
"emails/status_change_action_needed.txt", "emails/status_change_action_needed.txt",
"emails/status_change_action_needed_subject.txt", "emails/status_change_action_needed_subject.txt",
@ -576,19 +556,13 @@ class DomainApplication(TimeStampedModel):
@transition( @transition(
field="status", source=[SUBMITTED, IN_REVIEW, REJECTED], target=APPROVED 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. """Approve an application that has been submitted.
This has substantial side-effects because it creates another database This has substantial side-effects because it creates another database
object for the approved Domain and makes the user who created the object for the approved Domain and makes the user who created the
application into an admin on that domain. It also triggers an email application into an admin on that domain. It also triggers an email
notification. 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.
"""
# create the domain # create the domain
Domain = apps.get_model("registrar.Domain") Domain = apps.get_model("registrar.Domain")
@ -607,31 +581,23 @@ class DomainApplication(TimeStampedModel):
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN
) )
if updated_domain_application is not None: self._send_status_update_email(
# A DomainApplication is being passed to this method (ie from admin) "application approved",
updated_domain_application._send_status_update_email( "emails/status_change_approved.txt",
"application approved", "emails/status_change_approved_subject.txt",
"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",
)
@transition(field="status", source=[SUBMITTED, IN_REVIEW], target=WITHDRAWN) @transition(field="status", source=[SUBMITTED, IN_REVIEW], target=WITHDRAWN)
def withdraw(self): def withdraw(self):
"""Withdraw an application that has been submitted.""" """Withdraw an application that has been submitted."""
@transition(field="status", source=[IN_REVIEW, APPROVED], target=REJECTED) @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. """Reject an application that has been submitted.
As a side effect, an email notification is sent, similar to in_review""" 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", "action needed",
"emails/status_change_rejected.txt", "emails/status_change_rejected.txt",
"emails/status_change_rejected_subject.txt", "emails/status_change_rejected_subject.txt",

View file

@ -1,5 +1,6 @@
{% extends "admin/base.html" %} {% extends "admin/base.html" %}
{% load static %} {% load static %}
{% load i18n %}
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
@ -13,5 +14,25 @@
{% include "admin/color_theme_toggle.html" %} {% include "admin/color_theme_toggle.html" %}
{% endif %} {% endif %}
{% endblock %} {% 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 %}
<a href="{{ site_url }}">{% translate 'View site' %}</a> /
{% endif %}
{% if user.is_active and user.is_staff %}
{% url 'django-admindocs-docroot' as docsroot %}
{% if docsroot %}
<a href="{{ docsroot }}">{% translate 'Documentation' %}</a> /
{% endif %}
{% endif %}
{% if user.has_usable_password %}
<a href="{% url 'admin:password_change' %}">{% translate 'Change password' %}</a> /
{% endif %}
<a href="{% url 'admin:logout' %}" id="admin-logout-button">{% translate 'Log out' %}</a>
{% include "admin/color_theme_toggle.html" %}
{% endblock %}
{% block nav-global %}{% endblock %} {% block nav-global %}{% endblock %}

View file

@ -17,53 +17,45 @@ Load our custom filters to extract info from the django generated markup.
<thead> <thead>
<tr> <tr>
{# .gov - hardcode the select all checkbox #} {% if results.0.form %}
<th scope="col" class="action-checkbox-column" title="Toggle all"> {# .gov - hardcode the select all checkbox #}
<div class="text"> <th scope="col" class="action-checkbox-column" title="Toggle all">
<span> <div class="text">
<input type="checkbox" name="_selected_action" id="action-toggle"> <span>
<label for="action-toggle" class="usa-sr-only">Toggle all</label> <input type="checkbox" name="_selected_action" id="action-toggle">
</span> <label for="action-toggle" class="usa-sr-only">Toggle all</label>
</div> </span>
<div class="clear"></div> </div>
</th> <div class="clear"></div>
{# .gov - don't let django generate the select all checkbox #} </th>
{% for header in result_headers|slice:"1:" %} {# .gov - don't let django generate the select all checkbox #}
{% for header in result_headers|slice:"1:" %}
<th scope="col"{{ header.class_attrib }}>
{% if header.sortable %}
{% if header.sort_priority > 0 %}
<div class="sortoptions">
<a class="sortremove" href="{{ header.url_remove }}" title="{% translate "Remove from sorting" %}"></a>
{% if num_sorted_fields > 1 %}<span class="sortpriority" title="{% blocktranslate with priority_number=header.sort_priority %}Sorting priority: {{ priority_number }}{% endblocktranslate %}">{{ header.sort_priority }}</span>{% endif %}
<a href="{{ header.url_toggle }}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% translate "Toggle sorting" %}"></a>
</div>
{% endif %}
{% endif %}
<div class="text">{% if header.sortable %}<a href="{{ header.url_primary }}">{{ header.text|capfirst }}</a>{% else %}<span>{{ header.text|capfirst }}</span>{% endif %}</div>
<div class="clear"></div>
</th>{% endfor %}
<th scope="col"{{ header.class_attrib }}> </tr>
{% if header.sortable %} </thead>
{% if header.sort_priority > 0 %} <tbody>
<div class="sortoptions">
<a class="sortremove" href="{{ header.url_remove }}" title="{% translate "Remove from sorting" %}"></a>
{% if num_sorted_fields > 1 %}<span class="sortpriority" title="{% blocktranslate with priority_number=header.sort_priority %}Sorting priority: {{ priority_number }}{% endblocktranslate %}">{{ header.sort_priority }}</span>{% endif %}
<a href="{{ header.url_toggle }}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% translate "Toggle sorting" %}"></a>
</div>
{% endif %}
{% endif %}
<div class="text">{% if header.sortable %}<a href="{{ header.url_primary }}">{{ header.text|capfirst }}</a>{% else %}<span>{{ header.text|capfirst }}</span>{% endif %}</div>
<div class="clear"></div>
</th>{% endfor %}
</tr> {% comment %}
</thead> .gov - hardcode the row checkboxes using the custom filters to extract
<tbody> 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 %}
{% for result in results %}
{% if result.form.non_field_errors %}
<tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
{% endif %}
<tr>{% for item in result %}{{ item }}{% endfor %}</tr>
{% 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 %} {% if result.form.non_field_errors %}
<tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr> <tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
{% endif %} {% endif %}
@ -82,7 +74,37 @@ checkboxes.
{{ item }} {{ item }}
{% endfor %} {% endfor %}
</tr> </tr>
{% endfor %} {% endfor %}
{% else %} {# results doesn't have a form as its first element #}
{% for header in result_headers %}
<th scope="col"{{ header.class_attrib }}>
{% if header.sortable %}
{% if header.sort_priority > 0 %}
<div class="sortoptions">
<a class="sortremove" href="{{ header.url_remove }}" title="{% translate "Remove from sorting" %}"></a>
{% if num_sorted_fields > 1 %}<span class="sortpriority" title="{% blocktranslate with priority_number=header.sort_priority %}Sorting priority: {{ priority_number }}{% endblocktranslate %}">{{ header.sort_priority }}</span>{% endif %}
<a href="{{ header.url_toggle }}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% translate "Toggle sorting" %}"></a>
</div>
{% endif %}
{% endif %}
<div class="text">{% if header.sortable %}<a href="{{ header.url_primary }}">{{ header.text|capfirst }}</a>{% else %}<span>{{ header.text|capfirst }}</span>{% endif %}</div>
<div class="clear"></div>
</th>{% endfor %}
</tr>
</thead>
<tbody>
{% for result in results %}
{% if result.form.non_field_errors %}
<tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
{% endif %}
<tr>{% for item in result %}{{ item }}{% endfor %}</tr>
{% endfor %}
{% endif %}
</tbody> </tbody>
</table> </table>

View file

@ -6,13 +6,13 @@
Who is the authorizing official for your organization? Who is the authorizing official for your organization?
</h2> </h2>
<p>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.</p> <p>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.</p>
<div class="ao_example"> <div class="ao_example">
{% include "includes/ao_example.html" %} {% include "includes/ao_example.html" %}
</div> </div>
<p>We might contact your authorizing official, or their office, to double check that they approve this request. Read more about <a href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p> <p>We typically dont reach out to the authorizing official, but if contact is necessary, our practice is to coordinate first with you, the requestor. Read more about <a href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p>
{% endblock %} {% endblock %}

View file

@ -2,7 +2,13 @@
{% load static field_helpers %} {% load static field_helpers %}
{% block form_instructions %} {% block form_instructions %}
<p>Wed 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. <strong>This information will help us assess your eligibility for a .gov domain.</strong> These contacts should be in addition to you and your authorizing official. They should be employees of your organization.</p> <p>To help us assess your eligibility for a .gov domain, please provide contact information for other employees from your organization.
<ul class="usa-list">
<li>They should be clearly and publicly affiliated with your organization and familiar with your domain request. </li>
<li>They don't need to be involved with the technical management of your domain (although they can be). </li>
<li>We typically dont reach out to these employees, but if contact is necessary, our practice is to coordinate first with you, the requestor. </li>
</ul>
</p>
{% endblock %} {% endblock %}

View file

@ -22,7 +22,7 @@
{% if domainapplication.status == 'approved' %} Approved {% if domainapplication.status == 'approved' %} Approved
{% elif domainapplication.status == 'in review' %} In Review {% elif domainapplication.status == 'in review' %} In Review
{% elif domainapplication.status == 'rejected' %} Rejected {% elif domainapplication.status == 'rejected' %} Rejected
{% elif domainapplication.status == 'submitted' %} Received {% elif domainapplication.status == 'submitted' %} Submitted
{% else %}ERROR Please contact technical support/dev {% else %}ERROR Please contact technical support/dev
{% endif %} {% endif %}
</p> </p>

View file

@ -125,13 +125,14 @@
<p>You don't have any archived domains</p> <p>You don't have any archived domains</p>
</section> </section>
<section class="tablet:grid-col-11 desktop:grid-col-10"> <!-- Note: Uncomment below when this is being implemented post-MVP -->
<!-- <section class="tablet:grid-col-11 desktop:grid-col-10">
<h2 class="padding-top-1 mobile-lg:padding-top-3"> Export domains</h2> <h2 class="padding-top-1 mobile-lg:padding-top-3"> Export domains</h2>
<p>Download a list of your domains and their statuses as a csv file.</p> <p>Download a list of your domains and their statuses as a csv file.</p>
<a href="{% url 'todo' %}" class="usa-button usa-button--outline"> <a href="{% url 'todo' %}" class="usa-button usa-button--outline">
Export domains as csv Export domains as csv
</a> </a>
</section> </section> -->
</div> </div>

View file

@ -174,6 +174,33 @@ class TestDomainApplicationAdmin(TestCase):
# Perform assertions on the mock call itself # Perform assertions on the mock call itself
mock_client_instance.send_email.assert_called_once() 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 @boto3_mocking.patching
def test_save_model_sends_action_needed_email(self): def test_save_model_sends_action_needed_email(self):
# make sure there is no user with this email # make sure there is no user with this email

View file

@ -7,7 +7,7 @@ certifi==2023.7.22 ; python_version >= '3.6'
cfenv==0.5.3 cfenv==0.5.3
cffi==1.15.1 cffi==1.15.1
charset-normalizer==3.1.0 ; python_full_version >= '3.7.0' 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' 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-database-url==2.0.0
dj-email-url==1.0.6 dj-email-url==1.0.6

View file

@ -31,6 +31,8 @@
10027 OUTOFSCOPE http://app:8080/public/js/uswds-init.min.js 10027 OUTOFSCOPE http://app:8080/public/js/uswds-init.min.js
# get-gov.js contains suspicious word "from" as in `Array.from()` # get-gov.js contains suspicious word "from" as in `Array.from()`
10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js 10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js
# Ignore wording of "TODO"
10027 OUTOFSCOPE http://app:8080.*$
10028 FAIL (Open Redirect - Passive/beta) 10028 FAIL (Open Redirect - Passive/beta)
10029 FAIL (Cookie Poisoning - Passive/beta) 10029 FAIL (Cookie Poisoning - Passive/beta)
10030 FAIL (User Controllable Charset - Passive/beta) 10030 FAIL (User Controllable Charset - Passive/beta)