mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-13 16:17:01 +02:00
Merge remote-tracking branch 'origin' into ab/645-implement-test-cases-registry-integration
This commit is contained in:
commit
c9cadd3401
57 changed files with 2238 additions and 218 deletions
23
.github/ISSUE_TEMPLATE/developer-onboarding.md
vendored
23
.github/ISSUE_TEMPLATE/developer-onboarding.md
vendored
|
@ -3,7 +3,7 @@ name: Developer Onboarding
|
||||||
about: Onboarding steps for developers.
|
about: Onboarding steps for developers.
|
||||||
title: 'Developer Onboarding: GH_HANDLE'
|
title: 'Developer Onboarding: GH_HANDLE'
|
||||||
labels: dev, onboarding
|
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.
|
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
|
- [ ] [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/)
|
- [ ] Install the [Github CLI](https://cli.github.com/)
|
||||||
|
|
||||||
## Access
|
## Access
|
||||||
|
@ -24,27 +24,27 @@ 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.
|
||||||
|
|
||||||
- [ ] 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.
|
- [ ] 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
|
### 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)
|
- [ ] 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)
|
||||||
|
|
||||||
|
@ -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)
|
GPG_TTY=$(tty)
|
||||||
export GPG_TTY
|
export GPG_TTY
|
||||||
```
|
```
|
||||||
|
and then
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source ~/.bashrc
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```bash
|
||||||
|
source ~/.zshrc
|
||||||
|
```
|
||||||
|
|
||||||
## Setting up developer sandbox
|
## Setting up developer sandbox
|
||||||
|
|
||||||
|
|
139
.github/pull_request_template.md
vendored
139
.github/pull_request_template.md
vendored
|
@ -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._
|
||||||
|
--->
|
||||||
|
|
6
.github/workflows/deploy-sandbox.yaml
vendored
6
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -15,6 +15,10 @@ jobs:
|
||||||
|| startsWith(github.head_ref, 'rb/')
|
|| startsWith(github.head_ref, 'rb/')
|
||||||
|| startsWith(github.head_ref, 'ko/')
|
|| startsWith(github.head_ref, 'ko/')
|
||||||
|| startsWith(github.head_ref, 'gd/')
|
|| startsWith(github.head_ref, 'gd/')
|
||||||
|
|| startsWith(github.head_ref, 'za/')
|
||||||
|
|| startsWith(github.head_ref, 'rh/')
|
||||||
|
|| startsWith(github.head_ref, 'nl/')
|
||||||
|
|| startsWith(github.head_ref, 'dk/')
|
||||||
outputs:
|
outputs:
|
||||||
environment: ${{ steps.var.outputs.environment}}
|
environment: ${{ steps.var.outputs.environment}}
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
|
@ -49,7 +53,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||||
cf_org: cisa-getgov-prototyping
|
cf_org: cisa-dotgov
|
||||||
cf_space: ${{ env.ENVIRONMENT }}
|
cf_space: ${{ env.ENVIRONMENT }}
|
||||||
push_arguments: "-f ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml"
|
push_arguments: "-f ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml"
|
||||||
comment:
|
comment:
|
||||||
|
|
2
.github/workflows/deploy-stable.yaml
vendored
2
.github/workflows/deploy-stable.yaml
vendored
|
@ -36,6 +36,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets.CF_STABLE_USERNAME }}
|
cf_username: ${{ secrets.CF_STABLE_USERNAME }}
|
||||||
cf_password: ${{ secrets.CF_STABLE_PASSWORD }}
|
cf_password: ${{ secrets.CF_STABLE_PASSWORD }}
|
||||||
cf_org: cisa-getgov-prototyping
|
cf_org: cisa-dotgov
|
||||||
cf_space: stable
|
cf_space: stable
|
||||||
push_arguments: "-f ops/manifests/manifest-stable.yaml"
|
push_arguments: "-f ops/manifests/manifest-stable.yaml"
|
||||||
|
|
2
.github/workflows/deploy-staging.yaml
vendored
2
.github/workflows/deploy-staging.yaml
vendored
|
@ -36,6 +36,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets.CF_STAGING_USERNAME }}
|
cf_username: ${{ secrets.CF_STAGING_USERNAME }}
|
||||||
cf_password: ${{ secrets.CF_STAGING_PASSWORD }}
|
cf_password: ${{ secrets.CF_STAGING_PASSWORD }}
|
||||||
cf_org: cisa-getgov-prototyping
|
cf_org: cisa-dotgov
|
||||||
cf_space: staging
|
cf_space: staging
|
||||||
push_arguments: "-f ops/manifests/manifest-staging.yaml"
|
push_arguments: "-f ops/manifests/manifest-staging.yaml"
|
||||||
|
|
6
.github/workflows/migrate.yaml
vendored
6
.github/workflows/migrate.yaml
vendored
|
@ -15,12 +15,16 @@ on:
|
||||||
options:
|
options:
|
||||||
- stable
|
- stable
|
||||||
- staging
|
- staging
|
||||||
|
- nl
|
||||||
|
- rh
|
||||||
|
- za
|
||||||
- gd
|
- gd
|
||||||
- rb
|
- rb
|
||||||
- ko
|
- ko
|
||||||
- ab
|
- ab
|
||||||
- bl
|
- bl
|
||||||
- rjm
|
- rjm
|
||||||
|
- dk
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
migrate:
|
migrate:
|
||||||
|
@ -34,6 +38,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||||
cf_org: cisa-getgov-prototyping
|
cf_org: cisa-dotgov
|
||||||
cf_space: ${{ github.event.inputs.environment }}
|
cf_space: ${{ github.event.inputs.environment }}
|
||||||
full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate"
|
full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate"
|
||||||
|
|
10
.github/workflows/reset-db.yaml
vendored
10
.github/workflows/reset-db.yaml
vendored
|
@ -16,12 +16,16 @@ on:
|
||||||
options:
|
options:
|
||||||
- stable
|
- stable
|
||||||
- staging
|
- staging
|
||||||
|
- nl
|
||||||
|
- rh
|
||||||
|
- za
|
||||||
- gd
|
- gd
|
||||||
- rb
|
- rb
|
||||||
- ko
|
- ko
|
||||||
- ab
|
- ab
|
||||||
- bl
|
- bl
|
||||||
- rjm
|
- rjm
|
||||||
|
- dk
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
reset-db:
|
reset-db:
|
||||||
|
@ -35,7 +39,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||||
cf_org: cisa-getgov-prototyping
|
cf_org: cisa-dotgov
|
||||||
cf_space: ${{ github.event.inputs.environment }}
|
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"
|
full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py flush --no-input' --name flush"
|
||||||
|
|
||||||
|
@ -44,7 +48,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||||
cf_org: cisa-getgov-prototyping
|
cf_org: cisa-dotgov
|
||||||
cf_space: ${{ github.event.inputs.environment }}
|
cf_space: ${{ github.event.inputs.environment }}
|
||||||
full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate"
|
full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate"
|
||||||
|
|
||||||
|
@ -53,6 +57,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||||
cf_org: cisa-getgov-prototyping
|
cf_org: cisa-dotgov
|
||||||
cf_space: ${{ github.event.inputs.environment }}
|
cf_space: ${{ github.event.inputs.environment }}
|
||||||
full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py load' --name loaddata"
|
full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py load' --name loaddata"
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -8,7 +8,7 @@ Accepted
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
We need a place to run our application for the registrar. Cloud.gov is a FIMSA Moderate Fedramped solution that supports our language and framework selections.
|
We need a place to run our application for the registrar. Cloud.gov is a FISMA Moderate FedRAMP'd solution that supports our language and framework selections.
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
|
@ -16,10 +16,10 @@ To use cloud.gov to host our application(s).
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
Choosing cloud.gov for our solution means we are locked into its opinionated choices for our infrastructure. It forces us to run 12-factor applications. It doesn't support brokering for services we may need like email notifications.
|
* Choosing Cloud.gov for our solution means we are assisted by its opinionated choices for our infrastructure. For example, it forces us to run 12-factor applications.
|
||||||
|
* It doesn't support brokering for services we may need like email notifications.
|
||||||
It also means the compliance lift is much lighter. We do not need to prove we are compliance for the majority of our infrastructure and our runtime enviornment.
|
* The compliance lift is lighter. We can inherit Cloud.gov's controls for the majority of our infrastructure and our runtime enviornment.
|
||||||
|
|
||||||
## Alternatives Considered
|
## Alternatives Considered
|
||||||
|
|
||||||
Run our application on in either CISA's Azure or AWS environment with a continerized deployment.
|
Run our application on in either CISA's Azure or AWS environment with a containerized deployment.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# 22. Submit Domain Request User Flow
|
||||||
|
|
||||||
|
Date: 2023-07-18
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Historically, the .gov vendor managed initial identity verification and organizational affiliation for users that request a .gov domain. With the new registrar, _any user with a valid Login.gov account_ will be able to make a request. As a primary layer of abuse prevention (i.e., DDoSing the registry program with illegitimate requests), we need a way to stop new users from submitting multiple domain requests before they are known to the .gov registry. In this case, "known" means they have at least one approved domain application or existing domain.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Option 1:** Users will not be able to submit any new applications if they have 0 prior approved applications OR prior registered .gov domains. We would add a page alert informing the user that they cannot submit their application because they have an application in one of these "3" statuses (Submitted, In Review or Action Needed). They would still be able to create and edit new applications, just not submit them. The benefits of this option are that it would allow users to have multiple applications essentially in "draft mode" that are queued up and ready for submission after they are permitted to submit.
|
||||||
|
|
||||||
|
**Option 2:** Users will not be able to submit any new applications if they have 0 prior approved applications OR prior registered .gov domains. Additionally, we would remove the ability to edit any application with the started/withdrawn/rejected status, or start a new application. The benefit of this option is that a user would not be able to begin an action (submitting an application) that they are not allowed to complete.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
We have decided to go with option 1. New users of the registrar will need to have at least one approved application OR prior registered .gov domain in order to submit another application. We chose this option because we would like to allow users be able to work on applications, even if they are unable to submit them.
|
||||||
|
|
||||||
|
A [user flow diagram](https://miro.com/app/board/uXjVM3jz3Bs=/?share_link_id=875307531981) demonstrates our decision.
|
|
@ -18,6 +18,20 @@ 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).
|
||||||
|
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
* 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
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
@ -66,7 +80,7 @@ The endpoint /admin can be used to view and manage site content, including but n
|
||||||
1. Login via login.gov
|
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
|
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
|
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 = [
|
ADMINS = [
|
||||||
|
@ -79,8 +93,32 @@ The endpoint /admin can be used to view and manage site content, including but n
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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:
|
||||||
|
|
||||||
|
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": "<UUID here>",
|
||||||
|
"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.
|
||||||
|
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)
|
## 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.
|
The CODEOWNERS file sets the tagged individuals as default reviewers on any Pull Request that changes files that they are marked as owners of.
|
||||||
|
@ -166,6 +204,17 @@ from .common import less_console_noise
|
||||||
# <test code goes here>
|
# <test code goes here>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Accessibility Testing in the browser
|
||||||
|
|
||||||
|
We use the [ANDI](https://www.ssa.gov/accessibility/andi/help/install.html) browser extension
|
||||||
|
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.
|
||||||
|
|
||||||
|
Note - refresh after enabling the extension on a page but before clicking ANDI.
|
||||||
|
|
||||||
### Accessibility Scanning
|
### Accessibility Scanning
|
||||||
|
|
||||||
The tool `pa11y-ci` is used to scan pages for compliance with a set of
|
The tool `pa11y-ci` is used to scan pages for compliance with a set of
|
||||||
|
@ -203,7 +252,7 @@ Assets are stored in `registrar/assets` during development and served from `regi
|
||||||
|
|
||||||
We utilize the [uswds-compile tool](https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/) from USWDS to compile and package USWDS assets.
|
We utilize the [uswds-compile tool](https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/) from USWDS to compile and package USWDS assets.
|
||||||
|
|
||||||
## Making and view style changes
|
## Making and viewing style changes
|
||||||
|
|
||||||
When you run `docker-compose up` the `node` service in the container will begin to watch for changes in the `registrar/assets` folder, and will recompile once any changes are made.
|
When you run `docker-compose up` the `node` service in the container will begin to watch for changes in the `registrar/assets` folder, and will recompile once any changes are made.
|
||||||
|
|
||||||
|
|
|
@ -18,3 +18,4 @@ auditlog | log entry | can view log entry
|
||||||
registrar | contact | can view contact
|
registrar | contact | can view contact
|
||||||
registrar | domain application | can change domain application
|
registrar | domain application | can change domain application
|
||||||
registrar | domain | can view domain
|
registrar | domain | can view domain
|
||||||
|
registrar | user | can view user
|
29
ops/manifests/manifest-dk.yaml
Normal file
29
ops/manifests/manifest-dk.yaml
Normal file
|
@ -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
|
29
ops/manifests/manifest-nl.yaml
Normal file
29
ops/manifests/manifest-nl.yaml
Normal file
|
@ -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
|
29
ops/manifests/manifest-rh.yaml
Normal file
29
ops/manifests/manifest-rh.yaml
Normal file
|
@ -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
|
29
ops/manifests/manifest-za.yaml
Normal file
29
ops/manifests/manifest-za.yaml
Normal file
|
@ -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
|
|
@ -21,9 +21,9 @@ then
|
||||||
git checkout -b new-dev-sandbox-$1
|
git checkout -b new-dev-sandbox-$1
|
||||||
fi
|
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
|
echo
|
||||||
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
||||||
then
|
then
|
||||||
|
@ -49,9 +49,9 @@ sed -i '' '/getgov-staging.app.cloud.gov/ {a\
|
||||||
|
|
||||||
echo "Creating new cloud.gov space for $1..."
|
echo "Creating new cloud.gov space for $1..."
|
||||||
cf create-space $1
|
cf create-space $1
|
||||||
cf target -o "cisa-getgov-prototyping" -s $1
|
cf target -o "cisa-dotgov" -s $1
|
||||||
cf bind-security-group public_networks_egress cisa-getgov-prototyping --space $1
|
cf bind-security-group public_networks_egress cisa-dotgov --space $1
|
||||||
cf bind-security-group trusted_local_networks_egress cisa-getgov-prototyping --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..."
|
echo "Creating new cloud.gov DB for $1. This usually takes about 5 minutes..."
|
||||||
cf create-service aws-rds micro-psql getgov-$1-database
|
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
|
cf push getgov-$1 -f ops/manifests/manifest-$1.yaml
|
||||||
|
|
||||||
read -p "Please provide the email of the space developer: " -r
|
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
|
read -p "Should we run migrations? (y/n) " -n 1 -r
|
||||||
echo
|
echo
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
../ops/scripts/build.sh
|
../ops/scripts/build.sh
|
||||||
|
|
||||||
# Deploy to sandbox
|
# 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
|
cf push getgov-$1 -f ../ops/manifests/manifest-$1.yaml
|
||||||
|
|
||||||
# migrations need to be run manually. Developers can use this command
|
# migrations need to be run manually. Developers can use this command
|
||||||
|
|
|
@ -20,9 +20,9 @@ then
|
||||||
git checkout -b remove-dev-sandbox-$1
|
git checkout -b remove-dev-sandbox-$1
|
||||||
fi
|
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
|
echo
|
||||||
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
||||||
then
|
then
|
||||||
|
|
|
@ -9,8 +9,8 @@ if [ -z "$1" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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 and targeting the correct space? (y/n) " -n 1 -r
|
read -p "Are you logged in to the cisa-dotgov CF org above and targeting the correct space? (y/n) " -n 1 -r
|
||||||
echo
|
echo
|
||||||
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
||||||
then
|
then
|
||||||
|
|
12
src/package-lock.json
generated
12
src/package-lock.json
generated
|
@ -4086,9 +4086,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/normalize-package-data/node_modules/semver": {
|
"node_modules/normalize-package-data/node_modules/semver": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver"
|
"semver": "bin/semver"
|
||||||
|
@ -10148,9 +10148,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import logging
|
import logging
|
||||||
from django.contrib import admin, messages
|
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.contrib.contenttypes.models import ContentType
|
||||||
from django.http.response import HttpResponseRedirect
|
from django.http.response import HttpResponseRedirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from registrar.models.utility.admin_sort_fields import AdminSortFields
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AuditedAdmin(admin.ModelAdmin):
|
class AuditedAdmin(admin.ModelAdmin, AdminSortFields):
|
||||||
|
|
||||||
"""Custom admin to make auditing easier."""
|
"""Custom admin to make auditing easier."""
|
||||||
|
|
||||||
def history_view(self, request, object_id, extra_context=None):
|
def history_view(self, request, object_id, extra_context=None):
|
||||||
|
@ -23,9 +23,13 @@ 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 self.form_field_order_helper(form_field, db_field)
|
||||||
|
|
||||||
|
|
||||||
class ListHeaderAdmin(AuditedAdmin):
|
class ListHeaderAdmin(AuditedAdmin):
|
||||||
|
|
||||||
"""Custom admin to add a descriptive subheader to list views."""
|
"""Custom admin to add a descriptive subheader to list views."""
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
@ -93,12 +97,29 @@ class UserContactInline(admin.StackedInline):
|
||||||
model = models.Contact
|
model = models.Contact
|
||||||
|
|
||||||
|
|
||||||
class MyUserAdmin(UserAdmin):
|
class MyUserAdmin(BaseUserAdmin):
|
||||||
|
|
||||||
"""Custom user admin class to use our inlines."""
|
"""Custom user admin class to use our inlines."""
|
||||||
|
|
||||||
inlines = [UserContactInline]
|
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")
|
||||||
|
|
||||||
|
# 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):
|
class HostIPInline(admin.StackedInline):
|
||||||
|
|
||||||
|
@ -224,7 +245,6 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
|
|
||||||
class ContactAdmin(ListHeaderAdmin):
|
class ContactAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
"""Custom contact admin class to add search."""
|
"""Custom contact admin class to add search."""
|
||||||
|
|
||||||
search_fields = ["email", "first_name", "last_name"]
|
search_fields = ["email", "first_name", "last_name"]
|
||||||
|
@ -336,18 +356,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.INVESTIGATING:
|
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:
|
||||||
|
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:
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
172
src/registrar/assets/sass/_theme/_admin.scss
Normal file
172
src/registrar/assets/sass/_theme/_admin.scss
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
@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,
|
||||||
|
html[data-theme="light"] {
|
||||||
|
--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-lightest};
|
||||||
|
// --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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
.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
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
.change-list .usa-table,
|
||||||
|
.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,
|
||||||
|
body.dashboard,
|
||||||
|
body.change-list,
|
||||||
|
body.change-form {
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#branding h1 a:link, #branding h1 a:visited {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'Delete button' layout bug
|
||||||
|
.submit-row a.deletelink {
|
||||||
|
height: auto!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep th from collapsing
|
||||||
|
.min-width-25 {
|
||||||
|
min-width: 25px;
|
||||||
|
}
|
||||||
|
.min-width-81 {
|
||||||
|
min-width: 81px;
|
||||||
|
}
|
|
@ -9,3 +9,7 @@
|
||||||
/*--------------------------------------------------
|
/*--------------------------------------------------
|
||||||
--- Custom Styles ---------------------------------*/
|
--- Custom Styles ---------------------------------*/
|
||||||
@forward "uswds-theme-custom-styles";
|
@forward "uswds-theme-custom-styles";
|
||||||
|
|
||||||
|
/*--------------------------------------------------
|
||||||
|
--- Admin ---------------------------------*/
|
||||||
|
@forward "admin";
|
||||||
|
|
|
@ -571,12 +571,16 @@ SECURE_SSL_REDIRECT = True
|
||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = [
|
||||||
"getgov-stable.app.cloud.gov",
|
"getgov-stable.app.cloud.gov",
|
||||||
"getgov-staging.app.cloud.gov",
|
"getgov-staging.app.cloud.gov",
|
||||||
|
"getgov-nl.app.cloud.gov",
|
||||||
|
"getgov-rh.app.cloud.gov",
|
||||||
|
"getgov-za.app.cloud.gov",
|
||||||
"getgov-gd.app.cloud.gov",
|
"getgov-gd.app.cloud.gov",
|
||||||
"getgov-rb.app.cloud.gov",
|
"getgov-rb.app.cloud.gov",
|
||||||
"getgov-ko.app.cloud.gov",
|
"getgov-ko.app.cloud.gov",
|
||||||
"getgov-ab.app.cloud.gov",
|
"getgov-ab.app.cloud.gov",
|
||||||
"getgov-bl.app.cloud.gov",
|
"getgov-bl.app.cloud.gov",
|
||||||
"getgov-rjm.app.cloud.gov",
|
"getgov-rjm.app.cloud.gov",
|
||||||
|
"getgov-dk.app.cloud.gov",
|
||||||
"get.gov",
|
"get.gov",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -57,6 +57,21 @@ class UserFixture:
|
||||||
"first_name": "Ryan",
|
"first_name": "Ryan",
|
||||||
"last_name": "Brooks",
|
"last_name": "Brooks",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "30001ee7-0467-4df2-8db2-786e79606060",
|
||||||
|
"first_name": "Zander",
|
||||||
|
"last_name": "Adkinson",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "bb21f687-c773-4df3-9243-111cfd4c0be4",
|
||||||
|
"first_name": "Paul",
|
||||||
|
"last_name": "Kuykendall",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "2a88a97b-be96-4aad-b99e-0b605b492c78",
|
||||||
|
"first_name": "Rebecca",
|
||||||
|
"last_name": "Hsieh",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
STAFF = [
|
STAFF = [
|
||||||
|
@ -64,12 +79,28 @@ 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",
|
||||||
"first_name": "Alysia-Analyst",
|
"first_name": "Alysia-Analyst",
|
||||||
"last_name": "Alysia-Analyst",
|
"last_name": "Alysia-Analyst",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "2cc0cde8-8313-4a50-99d8-5882e71443e8",
|
||||||
|
"first_name": "Zander-Analyst",
|
||||||
|
"last_name": "Adkinson-Analyst",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "57ab5847-7789-49fe-a2f9-21d38076d699",
|
||||||
|
"first_name": "Paul-Analyst",
|
||||||
|
"last_name": "Kuykendall-Analyst",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "e474e7a9-71ca-449d-833c-8a6e094dd117",
|
||||||
|
"first_name": "Rebecca-Analyst",
|
||||||
|
"last_name": "Hsieh-Analyst",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
STAFF_PERMISSIONS = [
|
STAFF_PERMISSIONS = [
|
||||||
|
@ -85,6 +116,7 @@ class UserFixture:
|
||||||
"permissions": ["change_domainapplication"],
|
"permissions": ["change_domainapplication"],
|
||||||
},
|
},
|
||||||
{"app_label": "registrar", "model": "domain", "permissions": ["view_domain"]},
|
{"app_label": "registrar", "model": "domain", "permissions": ["view_domain"]},
|
||||||
|
{"app_label": "registrar", "model": "user", "permissions": ["view_user"]},
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -98,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()
|
||||||
|
@ -115,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
|
||||||
|
|
||||||
|
@ -201,11 +237,11 @@ class DomainApplicationFixture:
|
||||||
"organization_name": "Example - Submitted but pending Investigation",
|
"organization_name": "Example - Submitted but pending Investigation",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"status": "investigating",
|
"status": "in review",
|
||||||
"organization_name": "Example - In Investigation",
|
"organization_name": "Example - In Investigation",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"status": "investigating",
|
"status": "in review",
|
||||||
"organization_name": "Example - Approved",
|
"organization_name": "Example - Approved",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -377,9 +413,9 @@ class DomainFixture(DomainApplicationFixture):
|
||||||
return
|
return
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
# approve one of each users investigating status domains
|
# approve one of each users in review status domains
|
||||||
application = DomainApplication.objects.filter(
|
application = DomainApplication.objects.filter(
|
||||||
creator=user, status=DomainApplication.INVESTIGATING
|
creator=user, status=DomainApplication.IN_REVIEW
|
||||||
).last()
|
).last()
|
||||||
logger.debug(f"Approving {application} for {user}")
|
logger.debug(f"Approving {application} for {user}")
|
||||||
application.approve()
|
application.approve()
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 4.2.2 on 2023-07-12 21:31
|
||||||
|
# Generated by Django 4.2.2 on 2023-07-13 17:56
|
||||||
|
# hand merged
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import django_fsm
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0027_alter_domaininformation_address_line1_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="status",
|
||||||
|
field=django_fsm.FSMField(
|
||||||
|
choices=[
|
||||||
|
("started", "started"),
|
||||||
|
("submitted", "submitted"),
|
||||||
|
("in review", "in review"),
|
||||||
|
("action needed", "action needed"),
|
||||||
|
("approved", "approved"),
|
||||||
|
("withdrawn", "withdrawn"),
|
||||||
|
("rejected", "rejected"),
|
||||||
|
],
|
||||||
|
default="started",
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -18,18 +18,22 @@ class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
"""A registrant's application for a new domain."""
|
"""A registrant's application for a new domain."""
|
||||||
|
|
||||||
# #### Contants for choice fields ####
|
# #### Constants for choice fields ####
|
||||||
STARTED = "started"
|
STARTED = "started"
|
||||||
SUBMITTED = "submitted"
|
SUBMITTED = "submitted"
|
||||||
INVESTIGATING = "investigating"
|
IN_REVIEW = "in review"
|
||||||
|
ACTION_NEEDED = "action needed"
|
||||||
APPROVED = "approved"
|
APPROVED = "approved"
|
||||||
WITHDRAWN = "withdrawn"
|
WITHDRAWN = "withdrawn"
|
||||||
|
REJECTED = "rejected"
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
(STARTED, STARTED),
|
(STARTED, STARTED),
|
||||||
(SUBMITTED, SUBMITTED),
|
(SUBMITTED, SUBMITTED),
|
||||||
(INVESTIGATING, INVESTIGATING),
|
(IN_REVIEW, IN_REVIEW),
|
||||||
|
(ACTION_NEEDED, ACTION_NEEDED),
|
||||||
(APPROVED, APPROVED),
|
(APPROVED, APPROVED),
|
||||||
(WITHDRAWN, WITHDRAWN),
|
(WITHDRAWN, WITHDRAWN),
|
||||||
|
(REJECTED, REJECTED),
|
||||||
]
|
]
|
||||||
|
|
||||||
class StateTerritoryChoices(models.TextChoices):
|
class StateTerritoryChoices(models.TextChoices):
|
||||||
|
@ -497,16 +501,13 @@ class DomainApplication(TimeStampedModel):
|
||||||
except EmailSendingError:
|
except EmailSendingError:
|
||||||
logger.warning("Failed to send confirmation email", exc_info=True)
|
logger.warning("Failed to send confirmation email", exc_info=True)
|
||||||
|
|
||||||
@transition(field="status", source=[STARTED, WITHDRAWN], target=SUBMITTED)
|
@transition(
|
||||||
def submit(self, updated_domain_application=None):
|
field="status", source=[STARTED, ACTION_NEEDED, WITHDRAWN], target=SUBMITTED
|
||||||
|
)
|
||||||
|
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
|
||||||
|
@ -522,53 +523,46 @@ 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:
|
|
||||||
# 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(
|
self._send_status_update_email(
|
||||||
"submission confirmation",
|
"submission confirmation",
|
||||||
"emails/submission_confirmation.txt",
|
"emails/submission_confirmation.txt",
|
||||||
"emails/submission_confirmation_subject.txt",
|
"emails/submission_confirmation_subject.txt",
|
||||||
)
|
)
|
||||||
|
|
||||||
@transition(field="status", source=SUBMITTED, target=INVESTIGATING)
|
@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=[SUBMITTED, INVESTIGATING], target=APPROVED)
|
@transition(field="status", source=[IN_REVIEW, REJECTED], target=ACTION_NEEDED)
|
||||||
def approve(self, updated_domain_application=None):
|
def action_needed(self):
|
||||||
|
"""Send back an application that is under investigation or rejected.
|
||||||
|
|
||||||
|
As a side effect, an email notification is sent."""
|
||||||
|
|
||||||
|
self._send_status_update_email(
|
||||||
|
"action needed",
|
||||||
|
"emails/status_change_action_needed.txt",
|
||||||
|
"emails/status_change_action_needed_subject.txt",
|
||||||
|
)
|
||||||
|
|
||||||
|
@transition(
|
||||||
|
field="status", source=[SUBMITTED, IN_REVIEW, REJECTED], target=APPROVED
|
||||||
|
)
|
||||||
|
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")
|
||||||
|
@ -587,24 +581,28 @@ 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:
|
|
||||||
# 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(
|
self._send_status_update_email(
|
||||||
"application approved",
|
"application approved",
|
||||||
"emails/status_change_approved.txt",
|
"emails/status_change_approved.txt",
|
||||||
"emails/status_change_approved_subject.txt",
|
"emails/status_change_approved_subject.txt",
|
||||||
)
|
)
|
||||||
|
|
||||||
@transition(field="status", source=[SUBMITTED, INVESTIGATING], 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)
|
||||||
|
def reject(self):
|
||||||
|
"""Reject an application that has been submitted.
|
||||||
|
|
||||||
|
As a side effect, an email notification is sent, similar to in_review"""
|
||||||
|
|
||||||
|
self._send_status_update_email(
|
||||||
|
"action needed",
|
||||||
|
"emails/status_change_rejected.txt",
|
||||||
|
"emails/status_change_rejected_subject.txt",
|
||||||
|
)
|
||||||
|
|
||||||
# ## Form policies ###
|
# ## Form policies ###
|
||||||
#
|
#
|
||||||
# These methods control what questions need to be answered by applicants
|
# These methods control what questions need to be answered by applicants
|
||||||
|
|
63
src/registrar/models/utility/admin_form_order_helper.py
Normal file
63
src/registrar/models/utility/admin_form_order_helper.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import logging
|
||||||
|
from typing import Dict
|
||||||
|
from django.forms import ModelChoiceField
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SortingDict:
|
||||||
|
"""Stores a sorting dictionary object"""
|
||||||
|
|
||||||
|
_sorting_dict: Dict[type, type] = {}
|
||||||
|
|
||||||
|
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"""
|
||||||
|
# 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_list: list[SortingDict] = []
|
||||||
|
|
||||||
|
def __init__(self, sort: list[SortingDict]):
|
||||||
|
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_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
|
||||||
|
# Exit loop when order_by_list is found
|
||||||
|
break
|
||||||
|
|
||||||
|
# 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
|
||||||
|
form_field.queryset = form_field.queryset.order_by(*_order_by_list)
|
||||||
|
|
||||||
|
return form_field
|
27
src/registrar/models/utility/admin_sort_fields.py
Normal file
27
src/registrar/models/utility/admin_sort_fields.py
Normal file
|
@ -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)
|
56
src/registrar/templates/admin/app_list.html
Normal file
56
src/registrar/templates/admin/app_list.html
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% if app_list %}
|
||||||
|
{% for app in app_list %}
|
||||||
|
<div class="app-{{ app.app_label }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %}">
|
||||||
|
<table>
|
||||||
|
<caption>
|
||||||
|
<a href="{{ app.app_url }}" class="section" title="{% blocktranslate with name=app.name %}Models in the {{ name }} application{% endblocktranslate %}">{{ app.name }}</a>
|
||||||
|
</caption>
|
||||||
|
|
||||||
|
{# .gov override #}
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Model</th>
|
||||||
|
<th><span class="display-inline-block min-width-25">Add</span></th>
|
||||||
|
{% if show_changelinks %}
|
||||||
|
<th>
|
||||||
|
<span class="display-inline-block min-width-81">
|
||||||
|
{% translate 'View/Change' %}</th>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{# end .gov override #}
|
||||||
|
|
||||||
|
{% for model in app.models %}
|
||||||
|
<tr class="model-{{ model.object_name|lower }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
||||||
|
{% if model.admin_url %}
|
||||||
|
<th scope="row"><a href="{{ model.admin_url }}"{% if model.admin_url in request.path|urlencode %} aria-current="page"{% endif %}>{{ model.name }}</a></th>
|
||||||
|
{% else %}
|
||||||
|
<th scope="row">{{ model.name }}</th>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if model.add_url %}
|
||||||
|
<td><a href="{{ model.add_url }}" class="addlink">{% translate 'Add' %}</a></td>
|
||||||
|
{% else %}
|
||||||
|
<td></td>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if model.admin_url and show_changelinks %}
|
||||||
|
{% if model.view_only %}
|
||||||
|
<td><a href="{{ model.admin_url }}" class="viewlink">{% translate 'View' %}</a></td>
|
||||||
|
{% else %}
|
||||||
|
<td><a href="{{ model.admin_url }}" class="changelink">{% translate 'Change' %}</a></td>
|
||||||
|
{% endif %}
|
||||||
|
{% elif show_changelinks %}
|
||||||
|
<td></td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
||||||
|
{% endif %}
|
38
src/registrar/templates/admin/base_site.html
Normal file
38
src/registrar/templates/admin/base_site.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{% extends "admin/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||||
|
|
||||||
|
{% block extrastyle %}{{ block.super }}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "css/styles.css" %}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block branding %}
|
||||||
|
<h1 id="site-name"><a href="{% url 'admin:index' %}">.gov admin</a></h1>
|
||||||
|
{% if user.is_anonymous %}
|
||||||
|
{% 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 %}
|
||||||
|
<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 %}
|
112
src/registrar/templates/admin/change_list_results.html
Normal file
112
src/registrar/templates/admin/change_list_results.html
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
{% 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 class="hiddenfields">{# DIV for HTML validation #}
|
||||||
|
{% for item in result_hidden_fields %}{{ item }}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if results %}
|
||||||
|
<div class="results override-change_list_results">
|
||||||
|
<table id="result_list" class="usa-table usa-table--borderless usa-table--striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
{% if results.0.form %}
|
||||||
|
{# .gov - hardcode the select all checkbox #}
|
||||||
|
<th scope="col" class="action-checkbox-column" title="Toggle all">
|
||||||
|
<div class="text">
|
||||||
|
<span>
|
||||||
|
<input type="checkbox" name="_selected_action" id="action-toggle">
|
||||||
|
<label for="action-toggle" class="usa-sr-only">Toggle all</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="clear"></div>
|
||||||
|
</th>
|
||||||
|
{# .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 %}
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
{% 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 %}
|
||||||
|
<tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
{% with result_value=result.0|extract_value %}
|
||||||
|
{% with result_label=result.1|extract_a_text %}
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="_selected_action" value="{{ result_value|default:'value' }}" id="{{ result_label|default:result_value }}" class="action-select">
|
||||||
|
<label class="usa-sr-only" for="{{ result_label|default:result_value }}">{{ result_label|default:'label' }}</label>
|
||||||
|
</td>
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% for item in result|slice:"1:" %}
|
||||||
|
{{ item }}
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% 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>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
|
@ -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 don’t 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 %}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,10 @@
|
||||||
Is your organization an election office? <abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
Is your organization an election office? <abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>Answer “yes” if the primary purpose of your organization is to manage elections.</p>
|
<p>An election office is a government entity whose <em>primary</em> responsibility is overseeing elections and/or conducting voter registration.</p>
|
||||||
|
|
||||||
|
<p>Answer “yes” only if the <em>main purpose</em> of your organization is to serve as an election office.</p>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form_required_fields_help_text %}
|
{% block form_required_fields_help_text %}
|
||||||
|
|
|
@ -2,7 +2,13 @@
|
||||||
{% load static field_helpers %}
|
{% load static field_helpers %}
|
||||||
|
|
||||||
{% block form_instructions %}
|
{% block form_instructions %}
|
||||||
<p>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. <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 don’t 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 %}
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,9 @@
|
||||||
Status:
|
Status:
|
||||||
</span>
|
</span>
|
||||||
{% if domainapplication.status == 'approved' %} Approved
|
{% if domainapplication.status == 'approved' %} Approved
|
||||||
{% elif domainapplication.status == 'investigating' %} In Review
|
{% elif domainapplication.status == 'in review' %} In Review
|
||||||
{% elif domainapplication.status == 'submitted' %} Received
|
{% elif domainapplication.status == 'rejected' %} Rejected
|
||||||
|
{% elif domainapplication.status == 'submitted' %} Submitted
|
||||||
{% else %}ERROR Please contact technical support/dev
|
{% else %}ERROR Please contact technical support/dev
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -3,7 +3,17 @@
|
||||||
|
|
||||||
|
|
||||||
{% block form_fields %}
|
{% block form_fields %}
|
||||||
|
|
||||||
|
{% 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://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 %}
|
{% input_with_errors forms.0.tribe_name %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
<fieldset class="usa-fieldset">
|
<fieldset class="usa-fieldset">
|
||||||
<legend class="usa-legend">
|
<legend class="usa-legend">
|
||||||
<p>Is your organization a federally-recognized tribe or a state-recognized tribe? Check all that apply.
|
<p>Is your organization a federally-recognized tribe or a state-recognized tribe? Check all that apply.
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
|
Hi {{ application.submitter.first_name }}.
|
||||||
|
|
||||||
|
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 }}
|
||||||
|
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.
|
||||||
|
<https://get.gov/help/domain-requests/#withdraw-your-domain-request>.
|
||||||
|
|
||||||
|
|
||||||
|
NEXT STEPS
|
||||||
|
|
||||||
|
- 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 within one business day, please contact us:
|
||||||
|
<https://forms.office.com/pages/responsepage.aspx?id=bOfNPG2UEkq7evydCEI1SqHke9Gh6wJEl3kQ5EjWUKlUQzRJWDlBNTBCQUxTTzBaNlhTWURSSTBLTC4u>
|
||||||
|
|
||||||
|
|
||||||
|
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: <https://get.gov/contact/>
|
||||||
|
Visit <https://get.gov>
|
||||||
|
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
||||||
|
Action needed for your .gov domain request
|
32
src/registrar/templates/emails/status_change_rejected.txt
Normal file
32
src/registrar/templates/emails/status_change_rejected.txt
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
|
Hi {{ application.submitter.first_name }}.
|
||||||
|
|
||||||
|
Your .gov domain request has been rejected.
|
||||||
|
|
||||||
|
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||||
|
REQUEST RECEIVED ON: {{ application.updated_at|date }}
|
||||||
|
REQUEST #: {{ application.id }}
|
||||||
|
STATUS: Rejected
|
||||||
|
|
||||||
|
|
||||||
|
YOU CAN SUBMIT A NEW REQUEST
|
||||||
|
|
||||||
|
The details of your request are included below. If your organization is eligible for a .gov
|
||||||
|
domain and you meet our other requirements, you can submit a new request. Learn
|
||||||
|
more about .gov domains <https://get.gov/help/domains/>.
|
||||||
|
|
||||||
|
|
||||||
|
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: <https://get.gov/contact/>
|
||||||
|
Visit <https://get.gov>
|
||||||
|
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
||||||
|
Your .gov domain request has been rejected
|
|
@ -33,6 +33,8 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for domain in domains %}
|
{% for domain in domains %}
|
||||||
|
{% comment %} ticket 796
|
||||||
|
{% if domain.application_status == "approved" or (domain.application does not exist) %} {% endcomment %}
|
||||||
<tr>
|
<tr>
|
||||||
<th th scope="row" role="rowheader" data-label="Domain name">
|
<th th scope="row" role="rowheader" data-label="Domain name">
|
||||||
{{ domain.name }}
|
{{ domain.name }}
|
||||||
|
@ -88,7 +90,7 @@
|
||||||
<td data-sort-value="{{ application.created_at|date:"U" }}" data-label="Date created">{{ application.created_at|date }}</td>
|
<td data-sort-value="{{ application.created_at|date:"U" }}" data-label="Date created">{{ application.created_at|date }}</td>
|
||||||
<td data-label="Status">{{ application.status|title }}</td>
|
<td data-label="Status">{{ application.status|title }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if application.status == "started" or application.status == "withdrawn" %}
|
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}
|
||||||
<a href="{% url 'edit-application' application.pk %}">
|
<a href="{% url 'edit-application' application.pk %}">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#edit"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#edit"></use>
|
||||||
|
@ -123,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>
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ Template include for form fields with classes and their corresponding
|
||||||
error messages, if necessary.
|
error messages, if necessary.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load custom_filters %}
|
||||||
|
|
||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
{% if widget.attrs.maxlength %}
|
{% if widget.attrs.maxlength %}
|
||||||
|
@ -29,7 +31,18 @@ error messages, if necessary.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if sublabel_text %}
|
{% if sublabel_text %}
|
||||||
<p id="{{ widget.attrs.id }}__sublabel" class="text-base margin-top-2px margin-bottom-1">{{ sublabel_text }}</p>
|
<p id="{{ widget.attrs.id }}__sublabel" class="text-base margin-top-2px margin-bottom-1">
|
||||||
|
{% 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 }}
|
||||||
|
{% 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 %}
|
||||||
|
<a {% if target_blank == "true" %}target="_blank" {% endif %}href="{{ link_href }}">{{ link_text }}</a>{% with sublabel_part_after=sublabel_text|slice_after:link_text %}{{ sublabel_part_after }}{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
{{ sublabel_text }}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
|
|
42
src/registrar/templatetags/custom_filters.py
Normal file
42
src/registrar/templatetags/custom_filters.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
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 <a> tag
|
||||||
|
pattern = r"<a\b[^>]*>(.*?)</a>"
|
||||||
|
match = re.search(pattern, value)
|
||||||
|
if match:
|
||||||
|
extracted_text = match.group(1)
|
||||||
|
else:
|
||||||
|
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
|
|
@ -2,13 +2,26 @@ import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
import random
|
||||||
|
from string import ascii_uppercase
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model, login
|
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,
|
||||||
|
DomainInformation,
|
||||||
|
Domain,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_handlers():
|
def get_handlers():
|
||||||
|
@ -88,6 +101,339 @@ class MockSESClient(Mock):
|
||||||
self.EMAILS_SENT.append({"args": args, "kwargs": kwargs})
|
self.EMAILS_SENT.append({"args": args, "kwargs": kwargs})
|
||||||
|
|
||||||
|
|
||||||
|
class AuditedAdminMockData:
|
||||||
|
"""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 str 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 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
|
||||||
|
|
||||||
|
# Constants for different domain object types
|
||||||
|
INFORMATION = "information"
|
||||||
|
APPLICATION = "application"
|
||||||
|
INVITATION = "invitation"
|
||||||
|
|
||||||
|
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_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, short_hand):
|
||||||
|
"""Creates a dummy contact object"""
|
||||||
|
contact = Contact.objects.get_or_create(
|
||||||
|
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]
|
||||||
|
return contact
|
||||||
|
|
||||||
|
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, 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
|
||||||
|
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
|
||||||
|
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,
|
||||||
|
item_name,
|
||||||
|
org_type="federal",
|
||||||
|
federal_type="executive",
|
||||||
|
purpose="Purpose of the site",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
purpose=purpose,
|
||||||
|
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",
|
||||||
|
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
|
||||||
|
|
||||||
|
def dummy_kwarg_boilerplate(
|
||||||
|
self,
|
||||||
|
domain_type,
|
||||||
|
item_name,
|
||||||
|
status=DomainApplication.STARTED,
|
||||||
|
org_type="federal",
|
||||||
|
federal_type="executive",
|
||||||
|
purpose="Purpose of the site",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Returns a prebuilt kwarg dictionary for DomainApplication,
|
||||||
|
DomainInformation, or DomainInvitation.
|
||||||
|
Args:
|
||||||
|
domain_type (str): is either 'application', 'information',
|
||||||
|
or 'invitation'.
|
||||||
|
|
||||||
|
item_name (str): A shared str value appended to first_name, last_name,
|
||||||
|
organization_name, address_line1, address_line2,
|
||||||
|
title, email, and username.
|
||||||
|
|
||||||
|
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_dict = None
|
||||||
|
match domain_type:
|
||||||
|
case self.APPLICATION:
|
||||||
|
full_arg_dict = dict(
|
||||||
|
**common_args,
|
||||||
|
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_dict = dict(
|
||||||
|
**common_args,
|
||||||
|
domain=self.dummy_domain(item_name, True),
|
||||||
|
domain_application=domain_app,
|
||||||
|
)
|
||||||
|
case self.INVITATION:
|
||||||
|
full_arg_dict = dict(
|
||||||
|
email="test_mail@mail.com",
|
||||||
|
domain=self.dummy_domain(item_name, True),
|
||||||
|
status=DomainInvitation.INVITED,
|
||||||
|
)
|
||||||
|
return full_arg_dict
|
||||||
|
|
||||||
|
def create_full_dummy_domain_application(
|
||||||
|
self, item_name, status=DomainApplication.STARTED
|
||||||
|
):
|
||||||
|
"""Creates a dummy domain application object"""
|
||||||
|
domain_application_kwargs = self.dummy_kwarg_boilerplate(
|
||||||
|
self.APPLICATION, item_name, status
|
||||||
|
)
|
||||||
|
application = DomainApplication.objects.get_or_create(
|
||||||
|
**domain_application_kwargs
|
||||||
|
)[0]
|
||||||
|
return application
|
||||||
|
|
||||||
|
def create_full_dummy_domain_information(
|
||||||
|
self, item_name, status=DomainApplication.STARTED
|
||||||
|
):
|
||||||
|
"""Creates a dummy domain information object"""
|
||||||
|
domain_application_kwargs = self.dummy_kwarg_boilerplate(
|
||||||
|
self.INFORMATION, item_name, status
|
||||||
|
)
|
||||||
|
application = DomainInformation.objects.get_or_create(
|
||||||
|
**domain_application_kwargs
|
||||||
|
)[0]
|
||||||
|
return application
|
||||||
|
|
||||||
|
def create_full_dummy_domain_invitation(
|
||||||
|
self, item_name, status=DomainApplication.STARTED
|
||||||
|
):
|
||||||
|
"""Creates a dummy domain invitation object"""
|
||||||
|
domain_application_kwargs = self.dummy_kwarg_boilerplate(
|
||||||
|
self.INVITATION, item_name, status
|
||||||
|
)
|
||||||
|
application = DomainInvitation.objects.get_or_create(
|
||||||
|
**domain_application_kwargs
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
return application
|
||||||
|
|
||||||
|
def create_full_dummy_domain_object(
|
||||||
|
self,
|
||||||
|
domain_type,
|
||||||
|
item_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(
|
||||||
|
item_name, status
|
||||||
|
)
|
||||||
|
case self.INVITATION:
|
||||||
|
application = self.create_full_dummy_domain_invitation(
|
||||||
|
item_name, status
|
||||||
|
)
|
||||||
|
case self.INFORMATION:
|
||||||
|
application = self.create_full_dummy_domain_information(
|
||||||
|
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(item_name, "other")
|
||||||
|
application.other_contacts.add(other)
|
||||||
|
if has_current_website and domain_type == self.APPLICATION:
|
||||||
|
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(item_name)
|
||||||
|
application.alternative_domains.add(alt)
|
||||||
|
|
||||||
|
return application
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
||||||
|
User = get_user_model()
|
||||||
|
p = "adminpass"
|
||||||
|
return User.objects.create_superuser(
|
||||||
|
username="superuser",
|
||||||
|
email="admin@example.com",
|
||||||
|
password=p,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_user():
|
||||||
|
User = get_user_model()
|
||||||
|
p = "userpass"
|
||||||
|
return User.objects.create_user(
|
||||||
|
username="staffuser",
|
||||||
|
email="user@example.com",
|
||||||
|
password=p,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def completed_application(
|
def completed_application(
|
||||||
has_other_contacts=True,
|
has_other_contacts=True,
|
||||||
has_current_website=True,
|
has_current_website=True,
|
||||||
|
@ -111,15 +457,15 @@ def completed_application(
|
||||||
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
||||||
current, _ = Website.objects.get_or_create(website="city.com")
|
current, _ = Website.objects.get_or_create(website="city.com")
|
||||||
you, _ = Contact.objects.get_or_create(
|
you, _ = Contact.objects.get_or_create(
|
||||||
first_name="Testy you",
|
first_name="Testy2",
|
||||||
last_name="Tester you",
|
last_name="Tester2",
|
||||||
title="Admin Tester",
|
title="Admin Tester",
|
||||||
email="mayor@igorville.gov",
|
email="mayor@igorville.gov",
|
||||||
phone="(555) 555 5556",
|
phone="(555) 555 5556",
|
||||||
)
|
)
|
||||||
other, _ = Contact.objects.get_or_create(
|
other, _ = Contact.objects.get_or_create(
|
||||||
first_name="Testy2",
|
first_name="Testy",
|
||||||
last_name="Tester2",
|
last_name="Tester",
|
||||||
title="Another Tester",
|
title="Another Tester",
|
||||||
email="testy2@town.com",
|
email="testy2@town.com",
|
||||||
phone="(555) 555 5557",
|
phone="(555) 555 5557",
|
||||||
|
@ -159,14 +505,16 @@ def completed_application(
|
||||||
return application
|
return application
|
||||||
|
|
||||||
|
|
||||||
def mock_user():
|
def multiple_unalphabetical_domain_objects(
|
||||||
"""A simple user."""
|
domain_type=AuditedAdminMockData.APPLICATION,
|
||||||
user_kwargs = dict(
|
):
|
||||||
id=4,
|
"""Returns a list of generic domain objects for testing purposes"""
|
||||||
first_name="Rachid",
|
applications = []
|
||||||
last_name="Mrad",
|
list_of_letters = list(ascii_uppercase)
|
||||||
)
|
random.shuffle(list_of_letters)
|
||||||
|
|
||||||
user, _ = User.objects.get_or_create(**user_kwargs)
|
mock = AuditedAdminMockData()
|
||||||
|
for object_name in list_of_letters:
|
||||||
return user
|
application = mock.create_full_dummy_domain_object(domain_type, object_name)
|
||||||
|
applications.append(application)
|
||||||
|
return applications
|
||||||
|
|
|
@ -1,34 +1,40 @@
|
||||||
from django.test import TestCase, RequestFactory, Client
|
from django.test import TestCase, RequestFactory, Client
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin
|
|
||||||
from registrar.models import DomainApplication, DomainInformation, User
|
from registrar.admin import (
|
||||||
from .common import completed_application, mock_user
|
DomainApplicationAdmin,
|
||||||
|
ListHeaderAdmin,
|
||||||
|
MyUserAdmin,
|
||||||
|
AuditedAdmin,
|
||||||
|
)
|
||||||
|
from registrar.models import (
|
||||||
|
DomainApplication,
|
||||||
|
DomainInformation,
|
||||||
|
User,
|
||||||
|
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 django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
import boto3_mocking # type: ignore
|
import boto3_mocking # type: ignore
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TestDomainApplicationAdmin(TestCase):
|
class TestDomainApplicationAdmin(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.factory = RequestFactory()
|
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_save_model_sends_submitted_email(self):
|
def test_save_model_sends_submitted_email(self):
|
||||||
|
@ -76,9 +82,6 @@ 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()
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
application.delete()
|
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_save_model_sends_in_review_email(self):
|
def test_save_model_sends_in_review_email(self):
|
||||||
# make sure there is no user with this email
|
# make sure there is no user with this email
|
||||||
|
@ -101,7 +104,7 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
||||||
|
|
||||||
# Modify the application's property
|
# Modify the application's property
|
||||||
application.status = DomainApplication.INVESTIGATING
|
application.status = DomainApplication.IN_REVIEW
|
||||||
|
|
||||||
# Use the model admin's save_model method
|
# Use the model admin's save_model method
|
||||||
model_admin.save_model(request, application, form=None, change=True)
|
model_admin.save_model(request, application, form=None, change=True)
|
||||||
|
@ -125,9 +128,6 @@ 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()
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
application.delete()
|
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_save_model_sends_approved_email(self):
|
def test_save_model_sends_approved_email(self):
|
||||||
# make sure there is no user with this email
|
# make sure there is no user with this email
|
||||||
|
@ -139,7 +139,7 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
|
|
||||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
# Create a sample application
|
# Create a sample application
|
||||||
application = completed_application(status=DomainApplication.INVESTIGATING)
|
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||||
|
|
||||||
# Create a mock request
|
# Create a mock request
|
||||||
request = self.factory.post(
|
request = self.factory.post(
|
||||||
|
@ -174,15 +174,146 @@ 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()
|
||||||
|
|
||||||
# Cleanup
|
def test_save_model_sets_approved_domain(self):
|
||||||
if DomainInformation.objects.get(id=application.pk) is not None:
|
# make sure there is no user with this email
|
||||||
DomainInformation.objects.get(id=application.pk).delete()
|
EMAIL = "mayor@igorville.gov"
|
||||||
application.delete()
|
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):
|
||||||
|
# make sure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client_instance = mock_client.return_value
|
||||||
|
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
# 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.ACTION_NEEDED
|
||||||
|
|
||||||
|
# Use the model admin's save_model method
|
||||||
|
model_admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
|
# Access the arguments passed to send_email
|
||||||
|
call_args = mock_client_instance.send_email.call_args
|
||||||
|
args, kwargs = call_args
|
||||||
|
|
||||||
|
# Retrieve the email details from the arguments
|
||||||
|
from_email = kwargs.get("FromEmailAddress")
|
||||||
|
to_email = kwargs["Destination"]["ToAddresses"][0]
|
||||||
|
email_content = kwargs["Content"]
|
||||||
|
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
|
||||||
|
|
||||||
|
# Assert or perform other checks on the email details
|
||||||
|
expected_string = (
|
||||||
|
"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)
|
||||||
|
self.assertIn(expected_string, email_body)
|
||||||
|
|
||||||
|
# Perform assertions on the mock call itself
|
||||||
|
mock_client_instance.send_email.assert_called_once()
|
||||||
|
|
||||||
|
@boto3_mocking.patching
|
||||||
|
def test_save_model_sends_rejected_email(self):
|
||||||
|
# make sure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client_instance = mock_client.return_value
|
||||||
|
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
|
# 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.REJECTED
|
||||||
|
|
||||||
|
# Use the model admin's save_model method
|
||||||
|
model_admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
|
# Access the arguments passed to send_email
|
||||||
|
call_args = mock_client_instance.send_email.call_args
|
||||||
|
args, kwargs = call_args
|
||||||
|
|
||||||
|
# Retrieve the email details from the arguments
|
||||||
|
from_email = kwargs.get("FromEmailAddress")
|
||||||
|
to_email = kwargs["Destination"]["ToAddresses"][0]
|
||||||
|
email_content = kwargs["Content"]
|
||||||
|
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
|
||||||
|
|
||||||
|
# Assert or perform other checks on the email details
|
||||||
|
expected_string = "Your .gov domain request has been rejected."
|
||||||
|
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
|
||||||
|
self.assertEqual(to_email, EMAIL)
|
||||||
|
self.assertIn(expected_string, email_body)
|
||||||
|
|
||||||
|
# Perform assertions on the mock call itself
|
||||||
|
mock_client_instance.send_email.assert_called_once()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
|
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()
|
||||||
|
|
||||||
def test_changelist_view(self):
|
def test_changelist_view(self):
|
||||||
# Have to get creative to get past linter
|
# Have to get creative to get past linter
|
||||||
p = "adminpassword"
|
p = "adminpass"
|
||||||
self.client.login(username="admin", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
# Mock a user
|
# Mock a user
|
||||||
user = mock_user()
|
user = mock_user()
|
||||||
|
@ -241,6 +372,267 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
# delete any applications too
|
# delete any applications too
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
DomainApplication.objects.all().delete()
|
DomainApplication.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
self.superuser.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()
|
||||||
|
|
||||||
|
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()
|
||||||
|
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()
|
||||||
|
fieldsets = self.admin.get_fieldsets(request)
|
||||||
|
expected_fieldsets = ((None, {"fields": []}),)
|
||||||
|
self.assertEqual(fieldsets, expected_fieldsets)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
ordered_list = list(
|
||||||
|
obj_to_sort.get_queryset(request)
|
||||||
|
.order_by(*formatted_sort_fields)
|
||||||
|
.values_list(*formatted_sort_fields)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ordered_list
|
||||||
|
|
||||||
|
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.requested_domain.field,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Creates multiple domain applications - review status does not matter
|
||||||
|
applications = multiple_unalphabetical_domain_objects("application")
|
||||||
|
|
||||||
|
# Create a mock request
|
||||||
|
request = self.factory.post(
|
||||||
|
"/admin/registrar/domainapplication/{}/change/".format(applications[0].pk)
|
||||||
|
)
|
||||||
|
|
||||||
|
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.
|
||||||
|
# For test case purposes, this should be performant.
|
||||||
|
for field in tested_fields:
|
||||||
|
isNamefield: bool = field == DomainApplication.requested_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 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),
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
||||||
|
last = None
|
||||||
|
if not isOtherOrderfield:
|
||||||
|
first = obj.first_name
|
||||||
|
last = obj.last_name
|
||||||
|
elif field_obj == DomainInformation.domain.field:
|
||||||
|
first = obj.name
|
||||||
|
elif field_obj == DomainInformation.domain_application.field:
|
||||||
|
first = obj.requested_domain.name
|
||||||
|
|
||||||
|
name_tuple = self.coerced_fk_field_helper(
|
||||||
|
first, last, field_obj.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_obj.name),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_alphabetically_sorted_fk_fields_domain_invitation(self):
|
||||||
|
tested_fields = [DomainInvitation.domain.field]
|
||||||
|
|
||||||
|
# Creates multiple domain applications - 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 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),
|
||||||
|
)
|
||||||
|
|
||||||
|
def coerced_fk_field_helper(
|
||||||
|
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")
|
||||||
|
|
||||||
|
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:
|
||||||
|
return returned_tuple
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
|
DomainApplication.objects.all().delete()
|
||||||
|
DomainInvitation.objects.all().delete()
|
||||||
|
|
|
@ -144,11 +144,11 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.submit()
|
application.submit()
|
||||||
|
|
||||||
def test_transition_not_allowed_investigating_submitted(self):
|
def test_transition_not_allowed_in_review_submitted(self):
|
||||||
"""Create an application with status investigating and call submit
|
"""Create an application with status in review and call submit
|
||||||
against transition rules"""
|
against transition rules"""
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.INVESTIGATING)
|
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.submit()
|
application.submit()
|
||||||
|
@ -162,7 +162,16 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.submit()
|
application.submit()
|
||||||
|
|
||||||
def test_transition_not_allowed_started_investigating(self):
|
def test_transition_not_allowed_rejected_submitted(self):
|
||||||
|
"""Create an application with status rejected and call submit
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.REJECTED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.submit()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_started_in_review(self):
|
||||||
"""Create an application with status started and call in_review
|
"""Create an application with status started and call in_review
|
||||||
against transition rules"""
|
against transition rules"""
|
||||||
|
|
||||||
|
@ -171,16 +180,16 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.in_review()
|
application.in_review()
|
||||||
|
|
||||||
def test_transition_not_allowed_investigating_investigating(self):
|
def test_transition_not_allowed_in_review_in_review(self):
|
||||||
"""Create an application with status investigating and call in_review
|
"""Create an application with status in review and call in_review
|
||||||
against transition rules"""
|
against transition rules"""
|
||||||
|
|
||||||
application = completed_application(status=DomainApplication.INVESTIGATING)
|
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||||
|
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.in_review()
|
application.in_review()
|
||||||
|
|
||||||
def test_transition_not_allowed_approved_investigating(self):
|
def test_transition_not_allowed_approved_in_review(self):
|
||||||
"""Create an application with status approved and call in_review
|
"""Create an application with status approved and call in_review
|
||||||
against transition rules"""
|
against transition rules"""
|
||||||
|
|
||||||
|
@ -189,7 +198,25 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.in_review()
|
application.in_review()
|
||||||
|
|
||||||
def test_transition_not_allowed_withdrawn_investigating(self):
|
def test_transition_not_allowed_action_needed_in_review(self):
|
||||||
|
"""Create an application with status action needed and call in_review
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.in_review()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_rejected_in_review(self):
|
||||||
|
"""Create an application with status rejected and call in_review
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.REJECTED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.in_review()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_withdrawn_in_review(self):
|
||||||
"""Create an application with status withdrawn and call in_review
|
"""Create an application with status withdrawn and call in_review
|
||||||
against transition rules"""
|
against transition rules"""
|
||||||
|
|
||||||
|
@ -198,6 +225,51 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.in_review()
|
application.in_review()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_started_action_needed(self):
|
||||||
|
"""Create an application with status started and call action_needed
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.STARTED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.action_needed()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_submitted_action_needed(self):
|
||||||
|
"""Create an application with status submitted and call action_needed
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.SUBMITTED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.action_needed()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_action_needed_action_needed(self):
|
||||||
|
"""Create an application with status action needed and call action_needed
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.action_needed()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_approved_action_needed(self):
|
||||||
|
"""Create an application with status approved and call action_needed
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.APPROVED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.action_needed()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_withdrawn_action_needed(self):
|
||||||
|
"""Create an application with status withdrawn and call action_needed
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.WITHDRAWN)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.action_needed()
|
||||||
|
|
||||||
def test_transition_not_allowed_started_approved(self):
|
def test_transition_not_allowed_started_approved(self):
|
||||||
"""Create an application with status started and call approve
|
"""Create an application with status started and call approve
|
||||||
against transition rules"""
|
against transition rules"""
|
||||||
|
@ -216,6 +288,15 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.approve()
|
application.approve()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_action_needed_approved(self):
|
||||||
|
"""Create an application with status action needed and call approve
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.approve()
|
||||||
|
|
||||||
def test_transition_not_allowed_withdrawn_approved(self):
|
def test_transition_not_allowed_withdrawn_approved(self):
|
||||||
"""Create an application with status withdrawn and call approve
|
"""Create an application with status withdrawn and call approve
|
||||||
against transition rules"""
|
against transition rules"""
|
||||||
|
@ -243,6 +324,24 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.withdraw()
|
application.withdraw()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_action_needed_withdrawn(self):
|
||||||
|
"""Create an application with status action needed and call withdraw
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.withdraw()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_rejected_withdrawn(self):
|
||||||
|
"""Create an application with status rejected and call withdraw
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.REJECTED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.withdraw()
|
||||||
|
|
||||||
def test_transition_not_allowed_withdrawn_withdrawn(self):
|
def test_transition_not_allowed_withdrawn_withdrawn(self):
|
||||||
"""Create an application with status withdrawn and call withdraw
|
"""Create an application with status withdrawn and call withdraw
|
||||||
against transition rules"""
|
against transition rules"""
|
||||||
|
@ -252,6 +351,51 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.withdraw()
|
application.withdraw()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_started_rejected(self):
|
||||||
|
"""Create an application with status started and call reject
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.STARTED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.reject()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_submitted_rejected(self):
|
||||||
|
"""Create an application with status submitted and call reject
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.SUBMITTED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.reject()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_action_needed_rejected(self):
|
||||||
|
"""Create an application with status action needed and call reject
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.reject()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_withdrawn_rejected(self):
|
||||||
|
"""Create an application with status withdrawn and call reject
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.WITHDRAWN)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.reject()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_rejected_rejected(self):
|
||||||
|
"""Create an application with status rejected and call reject
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.REJECTED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.reject()
|
||||||
|
|
||||||
|
|
||||||
class TestPermissions(TestCase):
|
class TestPermissions(TestCase):
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,12 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
|
from registrar.templatetags.custom_filters import (
|
||||||
|
extract_value,
|
||||||
|
extract_a_text,
|
||||||
|
find_index,
|
||||||
|
slice_after,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestTemplateTags(TestCase):
|
class TestTemplateTags(TestCase):
|
||||||
|
@ -29,3 +35,51 @@ class TestTemplateTags(TestCase):
|
||||||
self.assertTrue(result.startswith(settings.GETGOV_PUBLIC_SITE_URL))
|
self.assertTrue(result.startswith(settings.GETGOV_PUBLIC_SITE_URL))
|
||||||
# slash-slash host slash directory slash page
|
# slash-slash host slash directory slash page
|
||||||
self.assertEqual(result.count("/"), 4)
|
self.assertEqual(result.count("/"), 4)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFiltersTestCase(TestCase):
|
||||||
|
def test_extract_value_filter(self):
|
||||||
|
html_input = (
|
||||||
|
'<input type="checkbox" name="_selected_action" value="123" '
|
||||||
|
'id="label_123" class="action-select">'
|
||||||
|
)
|
||||||
|
result = extract_value(html_input)
|
||||||
|
self.assertEqual(result, "123")
|
||||||
|
|
||||||
|
html_input = (
|
||||||
|
'<input type="checkbox" name="_selected_action" value="abc" '
|
||||||
|
'id="label_123" class="action-select">'
|
||||||
|
)
|
||||||
|
result = extract_value(html_input)
|
||||||
|
self.assertEqual(result, "abc")
|
||||||
|
|
||||||
|
def test_extract_a_text_filter(self):
|
||||||
|
input_text = '<a href="#">Link Text</a>'
|
||||||
|
result = extract_a_text(input_text)
|
||||||
|
self.assertEqual(result, "Link Text")
|
||||||
|
|
||||||
|
input_text = '<a href="/example">Another Link</a>'
|
||||||
|
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
|
||||||
|
|
|
@ -145,7 +145,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
# ---- TYPE PAGE ----
|
# ---- TYPE PAGE ----
|
||||||
type_form = type_page.form
|
type_form = type_page.form
|
||||||
type_form["organization_type-organization_type"] = "federal"
|
type_form["organization_type-organization_type"] = "federal"
|
||||||
|
|
||||||
# test next button and validate data
|
# test next button and validate data
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
type_result = type_page.form.submit()
|
type_result = type_page.form.submit()
|
||||||
|
@ -161,6 +160,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
# ---- FEDERAL BRANCH PAGE ----
|
# ---- FEDERAL BRANCH PAGE ----
|
||||||
# Follow the redirect to the next form page
|
# Follow the redirect to the next form page
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
federal_page = type_result.follow()
|
federal_page = type_result.follow()
|
||||||
federal_form = federal_page.form
|
federal_form = federal_page.form
|
||||||
federal_form["organization_federal-federal_type"] = "executive"
|
federal_form["organization_federal-federal_type"] = "executive"
|
||||||
|
|
|
@ -67,7 +67,7 @@ class ApplicationWizard(TemplateView):
|
||||||
URL_NAMESPACE = "application"
|
URL_NAMESPACE = "application"
|
||||||
# name for accessing /application/<id>/edit
|
# name for accessing /application/<id>/edit
|
||||||
EDIT_URL_NAME = "edit-application"
|
EDIT_URL_NAME = "edit-application"
|
||||||
|
NEW_URL_NAME = "/register/"
|
||||||
# We need to pass our human-readable step titles as context to the templates.
|
# We need to pass our human-readable step titles as context to the templates.
|
||||||
TITLES = {
|
TITLES = {
|
||||||
Step.ORGANIZATION_TYPE: _("Type of organization"),
|
Step.ORGANIZATION_TYPE: _("Type of organization"),
|
||||||
|
@ -144,6 +144,7 @@ class ApplicationWizard(TemplateView):
|
||||||
self._application = DomainApplication.objects.create(
|
self._application = DomainApplication.objects.create(
|
||||||
creator=self.request.user, # type: ignore
|
creator=self.request.user, # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
self.storage["application_id"] = self._application.id
|
self.storage["application_id"] = self._application.id
|
||||||
return self._application
|
return self._application
|
||||||
|
|
||||||
|
@ -195,7 +196,6 @@ class ApplicationWizard(TemplateView):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""This method handles GET requests."""
|
"""This method handles GET requests."""
|
||||||
|
|
||||||
current_url = resolve(request.path_info).url_name
|
current_url = resolve(request.path_info).url_name
|
||||||
|
|
||||||
# if user visited via an "edit" url, associate the id of the
|
# if user visited via an "edit" url, associate the id of the
|
||||||
|
@ -213,12 +213,15 @@ class ApplicationWizard(TemplateView):
|
||||||
# send users "to the application wizard" without needing to
|
# send users "to the application wizard" without needing to
|
||||||
# know which view is first in the list of steps.
|
# know which view is first in the list of steps.
|
||||||
if self.__class__ == ApplicationWizard:
|
if self.__class__ == ApplicationWizard:
|
||||||
|
# if starting a new application, clear the storage
|
||||||
|
if request.path_info == self.NEW_URL_NAME:
|
||||||
|
del self.storage
|
||||||
|
|
||||||
return self.goto(self.steps.first)
|
return self.goto(self.steps.first)
|
||||||
|
|
||||||
self.steps.current = current_url
|
self.steps.current = current_url
|
||||||
context = self.get_context_data()
|
context = self.get_context_data()
|
||||||
context["forms"] = self.get_forms()
|
context["forms"] = self.get_forms()
|
||||||
|
|
||||||
return render(request, self.template_name, context)
|
return render(request, self.template_name, context)
|
||||||
|
|
||||||
def get_all_forms(self, **kwargs) -> list:
|
def get_all_forms(self, **kwargs) -> list:
|
||||||
|
@ -242,7 +245,6 @@ class ApplicationWizard(TemplateView):
|
||||||
and from the database if `use_db` is True (provided that record exists).
|
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.
|
An empty form will be provided if neither of those are true.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"files": files,
|
"files": files,
|
||||||
"prefix": self.steps.current,
|
"prefix": self.steps.current,
|
||||||
|
|
|
@ -24,6 +24,12 @@ class DomainPermission(PermissionsLoginMixin):
|
||||||
The user is in self.request.user and the domain needs to be looked
|
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"]
|
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:
|
if not self.request.user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -33,6 +39,10 @@ class DomainPermission(PermissionsLoginMixin):
|
||||||
).exists():
|
).exists():
|
||||||
return False
|
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.
|
# if we need to check more about the nature of role, do it here.
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,15 @@ asgiref==3.7.2 ; python_version >= '3.7'
|
||||||
boto3==1.26.145
|
boto3==1.26.145
|
||||||
botocore==1.29.145 ; python_version >= '3.7'
|
botocore==1.29.145 ; python_version >= '3.7'
|
||||||
cachetools==5.3.1
|
cachetools==5.3.1
|
||||||
certifi==2023.5.7 ; python_version >= '3.6'
|
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.1 ; 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
|
||||||
django==4.2.1
|
django==4.2.3
|
||||||
django-allow-cidr==0.6.0
|
django-allow-cidr==0.6.0
|
||||||
django-auditlog==2.3.0
|
django-auditlog==2.3.0
|
||||||
django-cache-url==3.4.4
|
django-cache-url==3.4.4
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue