mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-17 18:09:25 +02:00
Merge branch 'main' into za/1864-error-message-rejection-placement
This commit is contained in:
commit
fb6b8838d3
69 changed files with 3252 additions and 760 deletions
9
.github/workflows/daily-csv-upload.yaml
vendored
9
.github/workflows/daily-csv-upload.yaml
vendored
|
@ -31,3 +31,12 @@ jobs:
|
||||||
cf_space: ${{ secrets.CF_REPORT_ENV }}
|
cf_space: ${{ secrets.CF_REPORT_ENV }}
|
||||||
cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py generate_current_full_report' --name full"
|
cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py generate_current_full_report' --name full"
|
||||||
|
|
||||||
|
- name: Generate and email domain-metadata.csv
|
||||||
|
uses: cloud-gov/cg-cli-tools@main
|
||||||
|
with:
|
||||||
|
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||||
|
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||||
|
cf_org: cisa-dotgov
|
||||||
|
cf_space: ${{ secrets.CF_REPORT_ENV }}
|
||||||
|
cf_command: "run-task getgov-${{ secrets.CF_REPORT_ENV }} --command 'python manage.py email_current_metadata_report' --name metadata"
|
||||||
|
|
||||||
|
|
2
.github/workflows/deploy-sandbox.yaml
vendored
2
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -22,6 +22,8 @@ jobs:
|
||||||
|| startsWith(github.head_ref, 'es/')
|
|| startsWith(github.head_ref, 'es/')
|
||||||
|| startsWith(github.head_ref, 'ky/')
|
|| startsWith(github.head_ref, 'ky/')
|
||||||
|| startsWith(github.head_ref, 'backup/')
|
|| startsWith(github.head_ref, 'backup/')
|
||||||
|
|| startsWith(github.head_ref, 'meoward/')
|
||||||
|
|| startsWith(github.head_ref, 'bob/')
|
||||||
outputs:
|
outputs:
|
||||||
environment: ${{ steps.var.outputs.environment}}
|
environment: ${{ steps.var.outputs.environment}}
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
|
|
2
.github/workflows/migrate.yaml
vendored
2
.github/workflows/migrate.yaml
vendored
|
@ -16,6 +16,8 @@ on:
|
||||||
- stable
|
- stable
|
||||||
- staging
|
- staging
|
||||||
- development
|
- development
|
||||||
|
- bob
|
||||||
|
- meoward
|
||||||
- backup
|
- backup
|
||||||
- ky
|
- ky
|
||||||
- es
|
- es
|
||||||
|
|
2
.github/workflows/reset-db.yaml
vendored
2
.github/workflows/reset-db.yaml
vendored
|
@ -16,6 +16,8 @@ on:
|
||||||
options:
|
options:
|
||||||
- staging
|
- staging
|
||||||
- development
|
- development
|
||||||
|
- bob
|
||||||
|
- meoward
|
||||||
- backup
|
- backup
|
||||||
- ky
|
- ky
|
||||||
- es
|
- es
|
||||||
|
|
41
.github/workflows/test-deploy.yaml
vendored
41
.github/workflows/test-deploy.yaml
vendored
|
@ -1,41 +0,0 @@
|
||||||
# This workflow is to for testing a change to our deploy structure and will be deleted when testing finishes
|
|
||||||
|
|
||||||
name: Deploy Main
|
|
||||||
run-name: Run deploy for ${{ github.event.inputs.environment }}
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
environment:
|
|
||||||
type: choice
|
|
||||||
description: Which environment should we run deploy for?
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- backup
|
|
||||||
- ky
|
|
||||||
- es
|
|
||||||
- nl
|
|
||||||
- rh
|
|
||||||
- za
|
|
||||||
- gd
|
|
||||||
- rb
|
|
||||||
- ko
|
|
||||||
- ab
|
|
||||||
- rjm
|
|
||||||
- dk
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
CF_USERNAME: CF_${{ github.event.inputs.environment }}_USERNAME
|
|
||||||
CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD
|
|
||||||
steps:
|
|
||||||
- name: Deploy to cloud.gov sandbox
|
|
||||||
uses: cloud-gov/cg-cli-tools@main
|
|
||||||
with:
|
|
||||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
|
||||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
|
||||||
cf_org: cisa-dotgov
|
|
||||||
cf_space: ${{ github.event.inputs.environment }}
|
|
||||||
cf_command: "push -f ops/manifests/manifest-${{ github.event.inputs.environment }}.yaml --strategy rolling"
|
|
|
@ -330,11 +330,12 @@ To associate a S3 instance to your sandbox, follow these steps:
|
||||||
3. Click `Services` on the application nav bar
|
3. Click `Services` on the application nav bar
|
||||||
4. Add a new service (plus symbol)
|
4. Add a new service (plus symbol)
|
||||||
5. Click `Marketplace Service`
|
5. Click `Marketplace Service`
|
||||||
6. On the `Select the service` dropdown, select `s3`
|
6. For Space, put in your sandbox initials
|
||||||
7. Under the dropdown on `Select Plan`, select `basic-sandbox`
|
7. On the `Select the service` dropdown, select `s3`
|
||||||
8. Under `Service Instance` enter `getgov-s3` for the name
|
8. Under the dropdown on `Select Plan`, select `basic-sandbox`
|
||||||
|
9. Under `Service Instance` enter `getgov-s3` for the name and leave the other fields empty
|
||||||
|
|
||||||
See this [resource](https://cloud.gov/docs/services/s3/) for information on associating an S3 instance with your sandbox through the CLI.
|
See this [resource](https://cloud.gov/docs/services/s3/) for information on associating an S3 instance with your sandbox through the CLI.
|
||||||
|
|
||||||
### Testing your S3 instance locally
|
### Testing your S3 instance locally
|
||||||
To test the S3 bucket associated with your sandbox, you will need to add four additional variables to your `.env` file. These are as follows:
|
To test the S3 bucket associated with your sandbox, you will need to add four additional variables to your `.env` file. These are as follows:
|
||||||
|
|
|
@ -117,3 +117,11 @@ You'll need to give the new certificate to the registry vendor _before_ rotating
|
||||||
## REGISTRY_HOSTNAME
|
## REGISTRY_HOSTNAME
|
||||||
|
|
||||||
This is the hostname at which the registry can be found.
|
This is the hostname at which the registry can be found.
|
||||||
|
|
||||||
|
## SECRET_METADATA_KEY
|
||||||
|
|
||||||
|
This is the passphrase for the zipped and encrypted metadata email that is sent out daily. Reach out to product team members or leads with access to security passwords if the passcode is needed.
|
||||||
|
|
||||||
|
To change the password, use a password generator to generate a password, then update the user credentials per the above instructions. Be sure to update the [KBDX](https://docs.google.com/document/d/1_BbJmjYZNYLNh4jJPPnUEG9tFCzJrOc0nMrZrnSKKyw) file in Google Drive with this password change.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
========================
|
========================
|
||||||
|
|
||||||
1. Check the [Pipfile](../../../src/Pipfile) for pinned dependencies and manually adjust the version numbers
|
1. Check the [Pipfile](../../../src/Pipfile) for pinned dependencies and manually adjust the version numbers
|
||||||
|
2. Run `docker-compose stop` to spin down the current containers and images so we can start afresh
|
||||||
2. Run
|
3. Run
|
||||||
|
|
||||||
cd src
|
cd src
|
||||||
docker-compose run app bash -c "pipenv lock && pipenv requirements > requirements.txt"
|
docker-compose run app bash -c "pipenv lock && pipenv requirements > requirements.txt"
|
||||||
|
@ -13,9 +13,9 @@
|
||||||
It is necessary to use `bash -c` because `run pipenv requirements` will not recognize that it is running non-interactively and will include garbage formatting characters.
|
It is necessary to use `bash -c` because `run pipenv requirements` will not recognize that it is running non-interactively and will include garbage formatting characters.
|
||||||
|
|
||||||
The requirements.txt is used by Cloud.gov. It is needed to work around a bug in the CloudFoundry buildpack version of Pipenv that breaks on installing from a git repository.
|
The requirements.txt is used by Cloud.gov. It is needed to work around a bug in the CloudFoundry buildpack version of Pipenv that breaks on installing from a git repository.
|
||||||
3. Change geventconnpool back to what it was originally within the Pipfile.lock and requirements.txt.
|
4. Change geventconnpool back to what it was originally within the Pipfile.lock and requirements.txt.
|
||||||
This is done by either saving what it was originally or opening a PR and using that as a reference to undo changes to any mention of geventconnpool.
|
This is done by either saving what it was originally or opening a PR and using that as a reference to undo changes to any mention of geventconnpool.
|
||||||
Geventconnpool, when set as a requirement without the reference portion, is defaulting to get a commit from 2014 which then breaks the code, as we want the newest version from them.
|
Geventconnpool, when set as a requirement without the reference portion, is defaulting to get a commit from 2014 which then breaks the code, as we want the newest version from them.
|
||||||
4. (optional) Run `docker-compose stop` and `docker-compose build` to build a new image for local development with the updated dependencies.
|
5. Run `docker-compose build` to build a new image for local development with the updated dependencies.
|
||||||
|
|
||||||
The reason for de-coupling the `build` and `lock` steps is to increase consistency between builds--a run of `build` will always get exactly the dependencies listed in `Pipfile.lock`, nothing more, nothing less.
|
The reason for de-coupling the `build` and `lock` steps is to increase consistency between builds--a run of `build` will always get exactly the dependencies listed in `Pipfile.lock`, nothing more, nothing less.
|
|
@ -4,7 +4,7 @@ applications:
|
||||||
buildpacks:
|
buildpacks:
|
||||||
- python_buildpack
|
- python_buildpack
|
||||||
path: ../../src
|
path: ../../src
|
||||||
instances: 2
|
instances: 1
|
||||||
memory: 512M
|
memory: 512M
|
||||||
stack: cflinuxfs4
|
stack: cflinuxfs4
|
||||||
timeout: 180
|
timeout: 180
|
||||||
|
|
32
ops/manifests/manifest-bob.yaml
Normal file
32
ops/manifests/manifest-bob.yaml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
applications:
|
||||||
|
- name: getgov-bob
|
||||||
|
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
|
||||||
|
health-check-invocation-timeout: 40
|
||||||
|
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-bob.app.cloud.gov
|
||||||
|
# Tell Django how much stuff to log
|
||||||
|
DJANGO_LOG_LEVEL: INFO
|
||||||
|
# default public site location
|
||||||
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
|
# Flag to disable/enable features in prod environments
|
||||||
|
IS_PRODUCTION: False
|
||||||
|
routes:
|
||||||
|
- route: getgov-bob.app.cloud.gov
|
||||||
|
services:
|
||||||
|
- getgov-credentials
|
||||||
|
- getgov-bob-database
|
32
ops/manifests/manifest-meoward.yaml
Normal file
32
ops/manifests/manifest-meoward.yaml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
applications:
|
||||||
|
- name: getgov-meoward
|
||||||
|
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
|
||||||
|
health-check-invocation-timeout: 40
|
||||||
|
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-meoward.app.cloud.gov
|
||||||
|
# Tell Django how much stuff to log
|
||||||
|
DJANGO_LOG_LEVEL: INFO
|
||||||
|
# default public site location
|
||||||
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
|
# Flag to disable/enable features in prod environments
|
||||||
|
IS_PRODUCTION: False
|
||||||
|
routes:
|
||||||
|
- route: getgov-meoward.app.cloud.gov
|
||||||
|
services:
|
||||||
|
- getgov-credentials
|
||||||
|
- getgov-meoward-database
|
|
@ -29,6 +29,7 @@ django-login-required-middleware = "*"
|
||||||
greenlet = "*"
|
greenlet = "*"
|
||||||
gevent = "*"
|
gevent = "*"
|
||||||
fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"}
|
fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"}
|
||||||
|
pyzipper="*"
|
||||||
tblib = "*"
|
tblib = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
@ -44,4 +45,4 @@ django-webtest = "*"
|
||||||
types-cachetools = "*"
|
types-cachetools = "*"
|
||||||
boto3-mocking = "*"
|
boto3-mocking = "*"
|
||||||
boto3-stubs = "*"
|
boto3-stubs = "*"
|
||||||
django-model2puml = "*"
|
django-model2puml = "*"
|
65
src/Pipfile.lock
generated
65
src/Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "b5d93b1b9ccafc37019276a222957544bab3f1f46b5dab8a0f2ffc2e5c9e1678"
|
"sha256": "082a951f15bb26a28f2dca7e0840fdf61518b3d90c42d77a310f982344cbd1dc"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {},
|
"requires": {},
|
||||||
|
@ -32,20 +32,20 @@
|
||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8b3f5cc7fbedcbb22271c328039df8a6ab343001e746e0cdb24774c426cadcf8",
|
"sha256:300888f0c1b6f32f27f85a9aa876f50f46514ec619647af7e4d20db74d339714",
|
||||||
"sha256:f201b6a416f809283d554c652211eecec9fe3a52ed4063dab3f3e7aea7571d9c"
|
"sha256:b26928f9a21cf3649cea20a59061340f3294c6e7785ceb6e1a953eb8010dc3ba"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.34.54"
|
"version": "==1.34.56"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4061ff4be3efcf53547ebadf2c94d419dfc8be7beec24e9fa1819599ffd936fa",
|
"sha256:bffeb71ab21d47d4ecf947d9bdb2fbd1b0bbd0c27742cea7cf0b77b701c41d9f",
|
||||||
"sha256:bf215d93e9d5544c593962780d194e74c6ee40b883d0b885e62ef35fc0ec01e5"
|
"sha256:fff66e22a5589c2d58fba57d1d95c334ce771895e831f80365f6cff6453285ec"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.34.54"
|
"version": "==1.34.56"
|
||||||
},
|
},
|
||||||
"cachetools": {
|
"cachetools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -376,20 +376,20 @@
|
||||||
"django"
|
"django"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:cc421ddb143fa30183568164755aa113a160e555cd19e97e664c478662032c24",
|
"sha256:069727a8f73d8ba8d033d3cd95c0da231d44f38f1da773bf076cef168d312ee8",
|
||||||
"sha256:feeaf28f17fd0499f9cd7c0fcf408c6d82c308e69e335eb92d09322fc9ed8138"
|
"sha256:e0bcfd41c718c07a7db422f9109e490746450da38793fe4ee197f397b9343435"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==10.3.0"
|
"version": "==11.0.0"
|
||||||
},
|
},
|
||||||
"faker": {
|
"faker": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:117ce1a2805c1bc5ca753b3dc6f9d567732893b2294b827d3164261ee8f20267",
|
"sha256:2456d674f40bd51eb3acbf85221277027822e529a90cc826453d9a25dff932b1",
|
||||||
"sha256:458d93580de34403a8dec1e8d5e6be2fee96c4deca63b95d71df7a6a80a690de"
|
"sha256:ea6f784c40730de0f77067e49e78cdd590efb00bec3d33f577492262206c17fc"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==23.3.0"
|
"version": "==24.0.0"
|
||||||
},
|
},
|
||||||
"fred-epplib": {
|
"fred-epplib": {
|
||||||
"git": "https://github.com/cisagov/epplib.git",
|
"git": "https://github.com/cisagov/epplib.git",
|
||||||
|
@ -708,11 +708,11 @@
|
||||||
},
|
},
|
||||||
"marshmallow": {
|
"marshmallow": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:20f53be28c6e374a711a16165fb22a8dc6003e3f7cda1285e3ca777b9193885b",
|
"sha256:4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3",
|
||||||
"sha256:e7997f83571c7fd476042c2c188e4ee8a78900ca5e74bd9c8097afa56624e9bd"
|
"sha256:f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==3.21.0"
|
"version": "==3.21.1"
|
||||||
},
|
},
|
||||||
"oic": {
|
"oic": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -994,6 +994,15 @@
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.0.1"
|
"version": "==1.0.1"
|
||||||
},
|
},
|
||||||
|
"pyzipper": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0adca90a00c36a93fbe49bfa8c5add452bfe4ef85a1b8e3638739dd1c7b26bfc",
|
||||||
|
"sha256:6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.4'",
|
||||||
|
"version": "==0.3.6"
|
||||||
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
|
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
|
||||||
|
@ -1186,12 +1195,12 @@
|
||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8b3f5cc7fbedcbb22271c328039df8a6ab343001e746e0cdb24774c426cadcf8",
|
"sha256:300888f0c1b6f32f27f85a9aa876f50f46514ec619647af7e4d20db74d339714",
|
||||||
"sha256:f201b6a416f809283d554c652211eecec9fe3a52ed4063dab3f3e7aea7571d9c"
|
"sha256:b26928f9a21cf3649cea20a59061340f3294c6e7785ceb6e1a953eb8010dc3ba"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.34.54"
|
"version": "==1.34.56"
|
||||||
},
|
},
|
||||||
"boto3-mocking": {
|
"boto3-mocking": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -1204,28 +1213,28 @@
|
||||||
},
|
},
|
||||||
"boto3-stubs": {
|
"boto3-stubs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7db5194e47f76e0010cd00b6ad9725db114d6a3fd04e52ceed3ef1181fe326bc",
|
"sha256:627f8eca69d832581ee1676d39df099a2a2e3a86d6b3ebd21c81c5f11ed6a6fa",
|
||||||
"sha256:c7b2e8b99f4896cf1226df47d4badaaa8df7426008c96a428bf00205695669e9"
|
"sha256:a87e7ecbab6235ec371b4363027e57483bca349a9cd5c891f40db81dadfa273e"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.34.54"
|
"version": "==1.34.56"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4061ff4be3efcf53547ebadf2c94d419dfc8be7beec24e9fa1819599ffd936fa",
|
"sha256:bffeb71ab21d47d4ecf947d9bdb2fbd1b0bbd0c27742cea7cf0b77b701c41d9f",
|
||||||
"sha256:bf215d93e9d5544c593962780d194e74c6ee40b883d0b885e62ef35fc0ec01e5"
|
"sha256:fff66e22a5589c2d58fba57d1d95c334ce771895e831f80365f6cff6453285ec"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.34.54"
|
"version": "==1.34.56"
|
||||||
},
|
},
|
||||||
"botocore-stubs": {
|
"botocore-stubs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:958f0084322dc9e549f73151b686fa51b15858fb2b3a573b9f4367f073fff463",
|
"sha256:018e001e3add5eb1828ef444b45fb8c9faf695e08334031bf2d96853cd9af703",
|
||||||
"sha256:bcc35bfbd14d1261813681c40108f2ce85fdf082c15b0a04016d3c22dd93b73f"
|
"sha256:25468ba6983987b704b1856bb155f297f576e6d4a690b021ab0c7122889ba907"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8' and python_version < '4.0'",
|
"markers": "python_version >= '3.8' and python_version < '4.0'",
|
||||||
"version": "==1.34.54"
|
"version": "==1.34.56"
|
||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
|
@ -49,3 +49,17 @@ def less_console_noise():
|
||||||
handler.setStream(restore[handler.name])
|
handler.setStream(restore[handler.name])
|
||||||
# close the file we opened
|
# close the file we opened
|
||||||
devnull.close()
|
devnull.close()
|
||||||
|
|
||||||
|
|
||||||
|
def less_console_noise_decorator(func):
|
||||||
|
"""
|
||||||
|
Decorator to silence console logging using the less_console_noise() function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# "Wrap" the original function in the less_console_noise with clause,
|
||||||
|
# then just return this wrapper.
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
with less_console_noise():
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
|
@ -6,12 +6,13 @@ from django.conf import settings
|
||||||
from django.contrib.auth import logout as auth_logout
|
from django.contrib.auth import logout as auth_logout
|
||||||
from django.contrib.auth import authenticate, login
|
from django.contrib.auth import authenticate, login
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect
|
||||||
from urllib.parse import parse_qs, urlencode
|
from urllib.parse import parse_qs, urlencode
|
||||||
|
|
||||||
from djangooidc.oidc import Client
|
from djangooidc.oidc import Client
|
||||||
from djangooidc import exceptions as o_e
|
from djangooidc import exceptions as o_e
|
||||||
from registrar.models import User
|
from registrar.models import User
|
||||||
|
from registrar.views.utility.error_views import custom_500_error_view, custom_401_error_view
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -49,27 +50,19 @@ def error_page(request, error):
|
||||||
"""Display a sensible message and log the error."""
|
"""Display a sensible message and log the error."""
|
||||||
logger.error(error)
|
logger.error(error)
|
||||||
if isinstance(error, o_e.AuthenticationFailed):
|
if isinstance(error, o_e.AuthenticationFailed):
|
||||||
return render(
|
context = {
|
||||||
request,
|
"friendly_message": error.friendly_message,
|
||||||
"401.html",
|
"log_identifier": error.locator,
|
||||||
context={
|
}
|
||||||
"friendly_message": error.friendly_message,
|
return custom_401_error_view(request, context)
|
||||||
"log_identifier": error.locator,
|
|
||||||
},
|
|
||||||
status=401,
|
|
||||||
)
|
|
||||||
if isinstance(error, o_e.InternalError):
|
if isinstance(error, o_e.InternalError):
|
||||||
return render(
|
context = {
|
||||||
request,
|
"friendly_message": error.friendly_message,
|
||||||
"500.html",
|
"log_identifier": error.locator,
|
||||||
context={
|
}
|
||||||
"friendly_message": error.friendly_message,
|
return custom_500_error_view(request, context)
|
||||||
"log_identifier": error.locator,
|
|
||||||
},
|
|
||||||
status=500,
|
|
||||||
)
|
|
||||||
if isinstance(error, Exception):
|
if isinstance(error, Exception):
|
||||||
return render(request, "500.html", status=500)
|
return custom_500_error_view(request)
|
||||||
|
|
||||||
|
|
||||||
def openid(request):
|
def openid(request):
|
||||||
|
|
|
@ -58,6 +58,8 @@ services:
|
||||||
- AWS_S3_SECRET_ACCESS_KEY
|
- AWS_S3_SECRET_ACCESS_KEY
|
||||||
- AWS_S3_REGION
|
- AWS_S3_REGION
|
||||||
- AWS_S3_BUCKET_NAME
|
- AWS_S3_BUCKET_NAME
|
||||||
|
# File encryption credentials
|
||||||
|
- SECRET_ENCRYPT_METADATA
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
ports:
|
ports:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""Provide a wrapper around epplib to handle authentication and errors."""
|
"""Provide a wrapper around epplib to handle authentication and errors."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from gevent.lock import BoundedSemaphore
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from epplib.client import Client
|
from epplib.client import Client
|
||||||
|
@ -52,10 +53,16 @@ class EPPLibWrapper:
|
||||||
"urn:ietf:params:xml:ns:contact-1.0",
|
"urn:ietf:params:xml:ns:contact-1.0",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
# We should only ever have one active connection at a time
|
||||||
|
self.connection_lock = BoundedSemaphore(1)
|
||||||
|
|
||||||
|
self.connection_lock.acquire()
|
||||||
try:
|
try:
|
||||||
self._initialize_client()
|
self._initialize_client()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Unable to configure epplib. Registrar cannot contact registry.")
|
logger.warning("Unable to configure the connection to the registry.")
|
||||||
|
finally:
|
||||||
|
self.connection_lock.release()
|
||||||
|
|
||||||
def _initialize_client(self) -> None:
|
def _initialize_client(self) -> None:
|
||||||
"""Initialize a client, assuming _login defined. Sets _client to initialized
|
"""Initialize a client, assuming _login defined. Sets _client to initialized
|
||||||
|
@ -74,11 +81,7 @@ class EPPLibWrapper:
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
# use the _client object to connect
|
# use the _client object to connect
|
||||||
self._client.connect() # type: ignore
|
self._connect()
|
||||||
response = self._client.send(self._login) # type: ignore
|
|
||||||
if response.code >= 2000: # type: ignore
|
|
||||||
self._client.close() # type: ignore
|
|
||||||
raise LoginError(response.msg) # type: ignore
|
|
||||||
except TransportError as err:
|
except TransportError as err:
|
||||||
message = "_initialize_client failed to execute due to a connection error."
|
message = "_initialize_client failed to execute due to a connection error."
|
||||||
logger.error(f"{message} Error: {err}")
|
logger.error(f"{message} Error: {err}")
|
||||||
|
@ -90,13 +93,33 @@ class EPPLibWrapper:
|
||||||
logger.error(f"{message} Error: {err}")
|
logger.error(f"{message} Error: {err}")
|
||||||
raise RegistryError(message) from err
|
raise RegistryError(message) from err
|
||||||
|
|
||||||
|
def _connect(self) -> None:
|
||||||
|
"""Connects to EPP. Sends a login command. If an invalid response is returned,
|
||||||
|
the client will be closed and a LoginError raised."""
|
||||||
|
self._client.connect() # type: ignore
|
||||||
|
response = self._client.send(self._login) # type: ignore
|
||||||
|
if response.code >= 2000: # type: ignore
|
||||||
|
self._client.close() # type: ignore
|
||||||
|
raise LoginError(response.msg) # type: ignore
|
||||||
|
|
||||||
def _disconnect(self) -> None:
|
def _disconnect(self) -> None:
|
||||||
"""Close the connection."""
|
"""Close the connection. Sends a logout command and closes the connection."""
|
||||||
|
self._send_logout_command()
|
||||||
|
self._close_client()
|
||||||
|
|
||||||
|
def _send_logout_command(self):
|
||||||
|
"""Sends a logout command to epp"""
|
||||||
try:
|
try:
|
||||||
self._client.send(commands.Logout()) # type: ignore
|
self._client.send(commands.Logout()) # type: ignore
|
||||||
self._client.close() # type: ignore
|
except Exception as err:
|
||||||
except Exception:
|
logger.warning(f"Logout command not sent successfully: {err}")
|
||||||
logger.warning("Connection to registry was not cleanly closed.")
|
|
||||||
|
def _close_client(self):
|
||||||
|
"""Closes an active client connection"""
|
||||||
|
try:
|
||||||
|
self._client.close()
|
||||||
|
except Exception as err:
|
||||||
|
logger.warning(f"Connection to registry was not cleanly closed: {err}")
|
||||||
|
|
||||||
def _send(self, command):
|
def _send(self, command):
|
||||||
"""Helper function used by `send`."""
|
"""Helper function used by `send`."""
|
||||||
|
@ -146,6 +169,8 @@ class EPPLibWrapper:
|
||||||
cmd_type = command.__class__.__name__
|
cmd_type = command.__class__.__name__
|
||||||
if not cleaned:
|
if not cleaned:
|
||||||
raise ValueError("Please sanitize user input before sending it.")
|
raise ValueError("Please sanitize user input before sending it.")
|
||||||
|
|
||||||
|
self.connection_lock.acquire()
|
||||||
try:
|
try:
|
||||||
return self._send(command)
|
return self._send(command)
|
||||||
except RegistryError as err:
|
except RegistryError as err:
|
||||||
|
@ -161,6 +186,8 @@ class EPPLibWrapper:
|
||||||
return self._retry(command)
|
return self._retry(command)
|
||||||
else:
|
else:
|
||||||
raise err
|
raise err
|
||||||
|
finally:
|
||||||
|
self.connection_lock.release()
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
import datetime
|
||||||
|
from dateutil.tz import tzlocal # type: ignore
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
from pathlib import Path
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from gevent.exceptions import ConcurrentObjectUseError
|
||||||
from epplibwrapper.client import EPPLibWrapper
|
from epplibwrapper.client import EPPLibWrapper
|
||||||
from epplibwrapper.errors import RegistryError, LoginError
|
from epplibwrapper.errors import RegistryError, LoginError
|
||||||
from .common import less_console_noise
|
from .common import less_console_noise
|
||||||
|
@ -8,6 +12,9 @@ import logging
|
||||||
try:
|
try:
|
||||||
from epplib.exceptions import TransportError
|
from epplib.exceptions import TransportError
|
||||||
from epplib.responses import Result
|
from epplib.responses import Result
|
||||||
|
from epplib.transport import SocketTransport
|
||||||
|
from epplib import commands
|
||||||
|
from epplib.models import common, info
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -255,3 +262,116 @@ class TestClient(TestCase):
|
||||||
mock_close.assert_called_once()
|
mock_close.assert_called_once()
|
||||||
# send() is called 5 times: send(login), send(command) fail, send(logout), send(login), send(command)
|
# send() is called 5 times: send(login), send(command) fail, send(logout), send(login), send(command)
|
||||||
self.assertEquals(mock_send.call_count, 5)
|
self.assertEquals(mock_send.call_count, 5)
|
||||||
|
|
||||||
|
def fake_failure_send_concurrent_threads(self, command=None, cleaned=None):
|
||||||
|
"""
|
||||||
|
Raises a ConcurrentObjectUseError, which gevent throws when accessing
|
||||||
|
the same thread from two different locations.
|
||||||
|
"""
|
||||||
|
# This error is thrown when two threads are being used concurrently
|
||||||
|
raise ConcurrentObjectUseError("This socket is already used by another greenlet")
|
||||||
|
|
||||||
|
def do_nothing(self, command=None):
|
||||||
|
"""
|
||||||
|
A placeholder method that performs no action.
|
||||||
|
"""
|
||||||
|
pass # noqa
|
||||||
|
|
||||||
|
def fake_success_send(self, command=None, cleaned=None):
|
||||||
|
"""
|
||||||
|
Simulates receiving a success response from EPP.
|
||||||
|
"""
|
||||||
|
mock = MagicMock(
|
||||||
|
code=1000,
|
||||||
|
msg="Command completed successfully",
|
||||||
|
res_data=None,
|
||||||
|
cl_tr_id="xkw1uo#2023-10-17T15:29:09.559376",
|
||||||
|
sv_tr_id="5CcH4gxISuGkq8eqvr1UyQ==-35a",
|
||||||
|
extensions=[],
|
||||||
|
msg_q=None,
|
||||||
|
)
|
||||||
|
return mock
|
||||||
|
|
||||||
|
def fake_info_domain_received(self, command=None, cleaned=None):
|
||||||
|
"""
|
||||||
|
Simulates receiving a response by reading from a predefined XML file.
|
||||||
|
"""
|
||||||
|
location = Path(__file__).parent / "utility" / "infoDomain.xml"
|
||||||
|
xml = (location).read_bytes()
|
||||||
|
return xml
|
||||||
|
|
||||||
|
def get_fake_epp_result(self):
|
||||||
|
"""Mimics a return from EPP by returning a dictionary in the same format"""
|
||||||
|
result = {
|
||||||
|
"cl_tr_id": None,
|
||||||
|
"code": 1000,
|
||||||
|
"extensions": [],
|
||||||
|
"msg": "Command completed successfully",
|
||||||
|
"msg_q": None,
|
||||||
|
"res_data": [
|
||||||
|
info.InfoDomainResultData(
|
||||||
|
roid="DF1340360-GOV",
|
||||||
|
statuses=[
|
||||||
|
common.Status(
|
||||||
|
state="serverTransferProhibited",
|
||||||
|
description=None,
|
||||||
|
lang="en",
|
||||||
|
),
|
||||||
|
common.Status(state="inactive", description=None, lang="en"),
|
||||||
|
],
|
||||||
|
cl_id="gov2023-ote",
|
||||||
|
cr_id="gov2023-ote",
|
||||||
|
cr_date=datetime.datetime(2023, 8, 15, 23, 56, 36, tzinfo=tzlocal()),
|
||||||
|
up_id="gov2023-ote",
|
||||||
|
up_date=datetime.datetime(2023, 8, 17, 2, 3, 19, tzinfo=tzlocal()),
|
||||||
|
tr_date=None,
|
||||||
|
name="test3.gov",
|
||||||
|
registrant="TuaWnx9hnm84GCSU",
|
||||||
|
admins=[],
|
||||||
|
nsset=None,
|
||||||
|
keyset=None,
|
||||||
|
ex_date=datetime.date(2024, 8, 15),
|
||||||
|
auth_info=info.DomainAuthInfo(pw="2fooBAR123fooBaz"),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
"sv_tr_id": "wRRNVhKhQW2m6wsUHbo/lA==-29a",
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def test_send_command_close_failure_recovers(self):
|
||||||
|
"""
|
||||||
|
Validates the resilience of the connection handling mechanism
|
||||||
|
during command execution on retry.
|
||||||
|
|
||||||
|
Scenario:
|
||||||
|
- Initialization of the connection is successful.
|
||||||
|
- An attempt to send a command fails with a specific error code (ConcurrentObjectUseError)
|
||||||
|
- The client attempts to retry.
|
||||||
|
- Subsequently, the client re-initializes the connection.
|
||||||
|
- A retry of the command execution post-reinitialization succeeds.
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected_result = self.get_fake_epp_result()
|
||||||
|
wrapper = None
|
||||||
|
# Trigger a retry
|
||||||
|
# Do nothing on connect, as we aren't testing it and want to connect while
|
||||||
|
# mimicking the rest of the client as closely as possible (which is not entirely possible with MagicMock)
|
||||||
|
with patch.object(EPPLibWrapper, "_connect", self.do_nothing):
|
||||||
|
with patch.object(SocketTransport, "send", self.fake_failure_send_concurrent_threads):
|
||||||
|
wrapper = EPPLibWrapper()
|
||||||
|
tested_command = commands.InfoDomain(name="test.gov")
|
||||||
|
try:
|
||||||
|
wrapper.send(tested_command, cleaned=True)
|
||||||
|
except RegistryError as err:
|
||||||
|
expected_error = "InfoDomain failed to execute due to an unknown error."
|
||||||
|
self.assertEqual(err.args[0], expected_error)
|
||||||
|
else:
|
||||||
|
self.fail("Registry error was not thrown")
|
||||||
|
|
||||||
|
# After a retry, try sending again to see if the connection recovers
|
||||||
|
with patch.object(EPPLibWrapper, "_connect", self.do_nothing):
|
||||||
|
with patch.object(SocketTransport, "send", self.fake_success_send), patch.object(
|
||||||
|
SocketTransport, "receive", self.fake_info_domain_received
|
||||||
|
):
|
||||||
|
result = wrapper.send(tested_command, cleaned=True)
|
||||||
|
self.assertEqual(expected_result, result.__dict__)
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import logging
|
import logging
|
||||||
|
import copy
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models.functions import Concat, Coalesce
|
|
||||||
from django.db.models import Value, CharField, Q
|
from django.db.models import Value, CharField, Q
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.db.models.functions import Concat, Coalesce
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django_fsm import get_available_FIELD_transitions
|
from django_fsm import get_available_FIELD_transitions
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
|
@ -875,18 +876,21 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
||||||
search_help_text = "Search by domain."
|
search_help_text = "Search by domain."
|
||||||
|
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, {"fields": ["creator", "domain_request", "notes"]}),
|
(None, {"fields": ["creator", "submitter", "domain_request", "notes"]}),
|
||||||
|
(".gov domain", {"fields": ["domain"]}),
|
||||||
|
("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}),
|
||||||
|
("Background info", {"fields": ["anything_else"]}),
|
||||||
(
|
(
|
||||||
"Type of organization",
|
"Type of organization",
|
||||||
{
|
{
|
||||||
"fields": [
|
"fields": [
|
||||||
"organization_type",
|
"organization_type",
|
||||||
|
"is_election_board",
|
||||||
|
"federal_type",
|
||||||
|
"federal_agency",
|
||||||
|
"tribe_name",
|
||||||
"federally_recognized_tribe",
|
"federally_recognized_tribe",
|
||||||
"state_recognized_tribe",
|
"state_recognized_tribe",
|
||||||
"tribe_name",
|
|
||||||
"federal_agency",
|
|
||||||
"federal_type",
|
|
||||||
"is_election_board",
|
|
||||||
"about_your_organization",
|
"about_your_organization",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -896,28 +900,15 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
||||||
{
|
{
|
||||||
"fields": [
|
"fields": [
|
||||||
"organization_name",
|
"organization_name",
|
||||||
|
"state_territory",
|
||||||
"address_line1",
|
"address_line1",
|
||||||
"address_line2",
|
"address_line2",
|
||||||
"city",
|
"city",
|
||||||
"state_territory",
|
|
||||||
"zipcode",
|
"zipcode",
|
||||||
"urbanization",
|
"urbanization",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
("Authorizing official", {"fields": ["authorizing_official"]}),
|
|
||||||
(".gov domain", {"fields": ["domain"]}),
|
|
||||||
("Your contact information", {"fields": ["submitter"]}),
|
|
||||||
("Other employees from your organization?", {"fields": ["other_contacts"]}),
|
|
||||||
(
|
|
||||||
"No other employees from your organization?",
|
|
||||||
{"fields": ["no_other_contacts_rationale"]},
|
|
||||||
),
|
|
||||||
("Anything else?", {"fields": ["anything_else"]}),
|
|
||||||
(
|
|
||||||
"Requirements for operating a .gov domain",
|
|
||||||
{"fields": ["is_policy_acknowledged"]},
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Read only that we'll leverage for CISA Analysts
|
# Read only that we'll leverage for CISA Analysts
|
||||||
|
@ -1029,6 +1020,8 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
||||||
if self.value() == "0":
|
if self.value() == "0":
|
||||||
return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None))
|
return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None))
|
||||||
|
|
||||||
|
change_form_template = "django/admin/domain_application_change_form.html"
|
||||||
|
|
||||||
# Columns
|
# Columns
|
||||||
list_display = [
|
list_display = [
|
||||||
"requested_domain",
|
"requested_domain",
|
||||||
|
@ -1040,7 +1033,7 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
||||||
"custom_election_board",
|
"custom_election_board",
|
||||||
"city",
|
"city",
|
||||||
"state_territory",
|
"state_territory",
|
||||||
"created_at",
|
"submission_date",
|
||||||
"submitter",
|
"submitter",
|
||||||
"investigator",
|
"investigator",
|
||||||
]
|
]
|
||||||
|
@ -1077,18 +1070,34 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
||||||
search_help_text = "Search by domain or submitter."
|
search_help_text = "Search by domain or submitter."
|
||||||
|
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, {"fields": ["status", "rejection_reason", "investigator", "creator", "approved_domain", "notes"]}),
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
"status",
|
||||||
|
"rejection_reason",
|
||||||
|
"investigator",
|
||||||
|
"creator",
|
||||||
|
"submitter",
|
||||||
|
"approved_domain",
|
||||||
|
"notes",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
|
||||||
|
("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}),
|
||||||
|
("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}),
|
||||||
(
|
(
|
||||||
"Type of organization",
|
"Type of organization",
|
||||||
{
|
{
|
||||||
"fields": [
|
"fields": [
|
||||||
"organization_type",
|
"organization_type",
|
||||||
|
"is_election_board",
|
||||||
|
"federal_type",
|
||||||
|
"federal_agency",
|
||||||
|
"tribe_name",
|
||||||
"federally_recognized_tribe",
|
"federally_recognized_tribe",
|
||||||
"state_recognized_tribe",
|
"state_recognized_tribe",
|
||||||
"tribe_name",
|
|
||||||
"federal_agency",
|
|
||||||
"federal_type",
|
|
||||||
"is_election_board",
|
|
||||||
"about_your_organization",
|
"about_your_organization",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1098,30 +1107,15 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
||||||
{
|
{
|
||||||
"fields": [
|
"fields": [
|
||||||
"organization_name",
|
"organization_name",
|
||||||
|
"state_territory",
|
||||||
"address_line1",
|
"address_line1",
|
||||||
"address_line2",
|
"address_line2",
|
||||||
"city",
|
"city",
|
||||||
"state_territory",
|
|
||||||
"zipcode",
|
"zipcode",
|
||||||
"urbanization",
|
"urbanization",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
("Authorizing official", {"fields": ["authorizing_official"]}),
|
|
||||||
("Current websites", {"fields": ["current_websites"]}),
|
|
||||||
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
|
|
||||||
("Purpose of your domain", {"fields": ["purpose"]}),
|
|
||||||
("Your contact information", {"fields": ["submitter"]}),
|
|
||||||
("Other employees from your organization?", {"fields": ["other_contacts"]}),
|
|
||||||
(
|
|
||||||
"No other employees from your organization?",
|
|
||||||
{"fields": ["no_other_contacts_rationale"]},
|
|
||||||
),
|
|
||||||
("Anything else?", {"fields": ["anything_else"]}),
|
|
||||||
(
|
|
||||||
"Requirements for operating a .gov domain",
|
|
||||||
{"fields": ["is_policy_acknowledged"]},
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Read only that we'll leverage for CISA Analysts
|
# Read only that we'll leverage for CISA Analysts
|
||||||
|
@ -1353,7 +1347,13 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
|
|
||||||
model = models.DomainInformation
|
model = models.DomainInformation
|
||||||
|
|
||||||
fieldsets = DomainInformationAdmin.fieldsets
|
fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets)
|
||||||
|
# remove .gov domain from fieldset
|
||||||
|
for index, (title, f) in enumerate(fieldsets):
|
||||||
|
if title == ".gov domain":
|
||||||
|
del fieldsets[index]
|
||||||
|
break
|
||||||
|
|
||||||
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
|
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
|
||||||
# For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
|
# For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
|
||||||
# to activate the edit/delete/view buttons
|
# to activate the edit/delete/view buttons
|
||||||
|
@ -1490,12 +1490,25 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
search_help_text = "Search by domain name."
|
search_help_text = "Search by domain name."
|
||||||
change_form_template = "django/admin/domain_change_form.html"
|
change_form_template = "django/admin/domain_change_form.html"
|
||||||
change_list_template = "django/admin/domain_change_list.html"
|
|
||||||
readonly_fields = ["state", "expiration_date", "first_ready", "deleted"]
|
readonly_fields = ["state", "expiration_date", "first_ready", "deleted"]
|
||||||
|
|
||||||
# Table ordering
|
# Table ordering
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
|
# Override for the delete confirmation page on the domain table (bulk delete action)
|
||||||
|
delete_selected_confirmation_template = "django/admin/domain_delete_selected_confirmation.html"
|
||||||
|
|
||||||
|
def delete_view(self, request, object_id, extra_context=None):
|
||||||
|
"""
|
||||||
|
Custom delete_view to perform additional actions or customize the template.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Set the delete template to a custom one
|
||||||
|
self.delete_confirmation_template = "django/admin/domain_delete_confirmation.html"
|
||||||
|
response = super().delete_view(request, object_id, extra_context=extra_context)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
|
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
|
||||||
"""Custom changeform implementation to pass in context information"""
|
"""Custom changeform implementation to pass in context information"""
|
||||||
if extra_context is None:
|
if extra_context is None:
|
||||||
|
@ -1523,56 +1536,6 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
return super().changeform_view(request, object_id, form_url, extra_context)
|
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
def export_data_type(self, request):
|
|
||||||
# match the CSV example with all the fields
|
|
||||||
response = HttpResponse(content_type="text/csv")
|
|
||||||
response["Content-Disposition"] = 'attachment; filename="domains-by-type.csv"'
|
|
||||||
csv_export.export_data_type_to_csv(response)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def export_data_full(self, request):
|
|
||||||
# Smaller export based on 1
|
|
||||||
response = HttpResponse(content_type="text/csv")
|
|
||||||
response["Content-Disposition"] = 'attachment; filename="current-full.csv"'
|
|
||||||
csv_export.export_data_full_to_csv(response)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def export_data_federal(self, request):
|
|
||||||
# Federal only
|
|
||||||
response = HttpResponse(content_type="text/csv")
|
|
||||||
response["Content-Disposition"] = 'attachment; filename="current-federal.csv"'
|
|
||||||
csv_export.export_data_federal_to_csv(response)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def get_urls(self):
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
urlpatterns = super().get_urls()
|
|
||||||
|
|
||||||
# Used to extrapolate a path name, for instance
|
|
||||||
# name="{app_label}_{model_name}_export_data_type"
|
|
||||||
info = self.model._meta.app_label, self.model._meta.model_name
|
|
||||||
|
|
||||||
my_url = [
|
|
||||||
path(
|
|
||||||
"export_data_type/",
|
|
||||||
self.export_data_type,
|
|
||||||
name="%s_%s_export_data_type" % info,
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"export_data_full/",
|
|
||||||
self.export_data_full,
|
|
||||||
name="%s_%s_export_data_full" % info,
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"export_data_federal/",
|
|
||||||
self.export_data_federal,
|
|
||||||
name="%s_%s_export_data_federal" % info,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
return my_url + urlpatterns
|
|
||||||
|
|
||||||
def response_change(self, request, obj):
|
def response_change(self, request, obj):
|
||||||
# Create dictionary of action functions
|
# Create dictionary of action functions
|
||||||
ACTION_FUNCTIONS = {
|
ACTION_FUNCTIONS = {
|
||||||
|
@ -1704,9 +1667,11 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
else:
|
else:
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
"Error deleting this Domain: "
|
(
|
||||||
f"Can't switch from state '{obj.state}' to 'deleted'"
|
"Error deleting this Domain: "
|
||||||
", must be either 'dns_needed' or 'on_hold'",
|
f"Can't switch from state '{obj.state}' to 'deleted'"
|
||||||
|
", must be either 'dns_needed' or 'on_hold'"
|
||||||
|
),
|
||||||
messages.ERROR,
|
messages.ERROR,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -1718,7 +1683,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
else:
|
else:
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
("Domain %s has been deleted. Thanks!") % obj.name,
|
"Domain %s has been deleted. Thanks!" % obj.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
@ -1760,7 +1725,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
else:
|
else:
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
("%s is in client hold. This domain is no longer accessible on the public internet.") % obj.name,
|
"%s is in client hold. This domain is no longer accessible on the public internet." % obj.name,
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
|
@ -1789,7 +1754,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
else:
|
else:
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
("%s is ready. This domain is accessible on the public internet.") % obj.name,
|
"%s is ready. This domain is accessible on the public internet." % obj.name,
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
|
@ -1834,9 +1799,6 @@ class VerifiedByStaffAdmin(ListHeaderAdmin):
|
||||||
list_display = ("email", "requestor", "truncated_notes", "created_at")
|
list_display = ("email", "requestor", "truncated_notes", "created_at")
|
||||||
search_fields = ["email"]
|
search_fields = ["email"]
|
||||||
search_help_text = "Search by email."
|
search_help_text = "Search by email."
|
||||||
list_filter = [
|
|
||||||
"requestor",
|
|
||||||
]
|
|
||||||
readonly_fields = [
|
readonly_fields = [
|
||||||
"requestor",
|
"requestor",
|
||||||
]
|
]
|
||||||
|
|
|
@ -29,20 +29,26 @@ function openInNewTab(el, removeAttribute = false){
|
||||||
*/
|
*/
|
||||||
(function (){
|
(function (){
|
||||||
function createPhantomModalFormButtons(){
|
function createPhantomModalFormButtons(){
|
||||||
let submitButtons = document.querySelectorAll('.usa-modal button[type="submit"]');
|
let submitButtons = document.querySelectorAll('.usa-modal button[type="submit"].dja-form-placeholder');
|
||||||
form = document.querySelector("form")
|
form = document.querySelector("form")
|
||||||
submitButtons.forEach((button) => {
|
submitButtons.forEach((button) => {
|
||||||
|
|
||||||
let input = document.createElement("input");
|
let input = document.createElement("input");
|
||||||
input.type = "submit";
|
input.type = "submit";
|
||||||
input.name = button.name;
|
|
||||||
input.value = button.value;
|
if(button.name){
|
||||||
|
input.name = button.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(button.value){
|
||||||
|
input.value = button.value;
|
||||||
|
}
|
||||||
|
|
||||||
input.style.display = "none"
|
input.style.display = "none"
|
||||||
|
|
||||||
// Add the hidden input to the form
|
// Add the hidden input to the form
|
||||||
form.appendChild(input);
|
form.appendChild(input);
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
console.log("clicking")
|
|
||||||
input.click();
|
input.click();
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -50,6 +56,61 @@ function openInNewTab(el, removeAttribute = false){
|
||||||
|
|
||||||
createPhantomModalFormButtons();
|
createPhantomModalFormButtons();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/** An IIFE for DomainRequest to hook a modal to a dropdown option.
|
||||||
|
* This intentionally does not interact with createPhantomModalFormButtons()
|
||||||
|
*/
|
||||||
|
(function (){
|
||||||
|
function displayModalOnDropdownClick(linkClickedDisplaysModal, statusDropdown, actionButton, valueToCheck){
|
||||||
|
|
||||||
|
// If these exist all at the same time, we're on the right page
|
||||||
|
if (linkClickedDisplaysModal && statusDropdown && statusDropdown.value){
|
||||||
|
|
||||||
|
// Set the previous value in the event the user cancels.
|
||||||
|
let previousValue = statusDropdown.value;
|
||||||
|
if (actionButton){
|
||||||
|
|
||||||
|
// Otherwise, if the confirmation buttion is pressed, set it to that
|
||||||
|
actionButton.addEventListener('click', function() {
|
||||||
|
// Revert the dropdown to its previous value
|
||||||
|
statusDropdown.value = valueToCheck;
|
||||||
|
});
|
||||||
|
}else {
|
||||||
|
console.log("displayModalOnDropdownClick() -> Cancel button was null")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a change event listener to the dropdown.
|
||||||
|
statusDropdown.addEventListener('change', function() {
|
||||||
|
// Check if "Ineligible" is selected
|
||||||
|
if (this.value && this.value.toLowerCase() === valueToCheck) {
|
||||||
|
// Set the old value in the event the user cancels,
|
||||||
|
// or otherwise exists the dropdown.
|
||||||
|
statusDropdown.value = previousValue
|
||||||
|
|
||||||
|
// Display the modal.
|
||||||
|
linkClickedDisplaysModal.click()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the status dropdown is clicked and is set to "ineligible", toggle a confirmation dropdown.
|
||||||
|
function hookModalToIneligibleStatus(){
|
||||||
|
// Grab the invisible element that will hook to the modal.
|
||||||
|
// This doesn't technically need to be done with one, but this is simpler to manage.
|
||||||
|
let modalButton = document.getElementById("invisible-ineligible-modal-toggler")
|
||||||
|
let statusDropdown = document.getElementById("id_status")
|
||||||
|
|
||||||
|
// Because the modal button does not have the class "dja-form-placeholder",
|
||||||
|
// it will not be affected by the createPhantomModalFormButtons() function.
|
||||||
|
let actionButton = document.querySelector('button[name="_set_domain_request_ineligible"]');
|
||||||
|
let valueToCheck = "ineligible"
|
||||||
|
displayModalOnDropdownClick(modalButton, statusDropdown, actionButton, valueToCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
hookModalToIneligibleStatus()
|
||||||
|
})();
|
||||||
|
|
||||||
/** An IIFE for pages in DjangoAdmin which may need custom JS implementation.
|
/** An IIFE for pages in DjangoAdmin which may need custom JS implementation.
|
||||||
* Currently only appends target="_blank" to the domain_form object,
|
* Currently only appends target="_blank" to the domain_form object,
|
||||||
* but this can be expanded.
|
* but this can be expanded.
|
||||||
|
@ -307,43 +368,6 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk,
|
||||||
viewLink.setAttribute('title', viewLink.getAttribute('title-template').replace('selected item', elementText));
|
viewLink.setAttribute('title', viewLink.getAttribute('title-template').replace('selected item', elementText));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An IIFE for admin in DjangoAdmin to listen to clicks on the growth report export button,
|
|
||||||
* attach the seleted start and end dates to a url that'll trigger the view, and finally
|
|
||||||
* redirect to that url.
|
|
||||||
*/
|
|
||||||
(function (){
|
|
||||||
|
|
||||||
// Get the current date in the format YYYY-MM-DD
|
|
||||||
let currentDate = new Date().toISOString().split('T')[0];
|
|
||||||
|
|
||||||
// Default the value of the start date input field to the current date
|
|
||||||
let startDateInput =document.getElementById('start');
|
|
||||||
|
|
||||||
// Default the value of the end date input field to the current date
|
|
||||||
let endDateInput =document.getElementById('end');
|
|
||||||
|
|
||||||
let exportGrowthReportButton = document.getElementById('exportLink');
|
|
||||||
|
|
||||||
if (exportGrowthReportButton) {
|
|
||||||
startDateInput.value = currentDate;
|
|
||||||
endDateInput.value = currentDate;
|
|
||||||
|
|
||||||
exportGrowthReportButton.addEventListener('click', function() {
|
|
||||||
// Get the selected start and end dates
|
|
||||||
let startDate = startDateInput.value;
|
|
||||||
let endDate = endDateInput.value;
|
|
||||||
let exportUrl = document.getElementById('exportLink').dataset.exportUrl;
|
|
||||||
|
|
||||||
// Build the URL with parameters
|
|
||||||
exportUrl += "?start_date=" + startDate + "&end_date=" + endDate;
|
|
||||||
|
|
||||||
// Redirect to the export URL
|
|
||||||
window.location.href = exportUrl;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
|
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
|
||||||
* status select amd to show/hide the rejection reason
|
* status select amd to show/hide the rejection reason
|
||||||
*/
|
*/
|
||||||
|
|
117
src/registrar/assets/js/get-gov-reports.js
Normal file
117
src/registrar/assets/js/get-gov-reports.js
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
/** An IIFE for admin in DjangoAdmin to listen to clicks on the growth report export button,
|
||||||
|
* attach the seleted start and end dates to a url that'll trigger the view, and finally
|
||||||
|
* redirect to that url.
|
||||||
|
*
|
||||||
|
* This function also sets the start and end dates to match the url params if they exist
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
// Function to get URL parameter value by name
|
||||||
|
function getParameterByName(name, url) {
|
||||||
|
if (!url) url = window.location.href;
|
||||||
|
name = name.replace(/[\[\]]/g, '\\$&');
|
||||||
|
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
|
||||||
|
results = regex.exec(url);
|
||||||
|
if (!results) return null;
|
||||||
|
if (!results[2]) return '';
|
||||||
|
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current date in the format YYYY-MM-DD
|
||||||
|
let currentDate = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Default the value of the start date input field to the current date
|
||||||
|
let startDateInput = document.getElementById('start');
|
||||||
|
|
||||||
|
// Default the value of the end date input field to the current date
|
||||||
|
let endDateInput = document.getElementById('end');
|
||||||
|
|
||||||
|
let exportButtons = document.querySelectorAll('.exportLink');
|
||||||
|
|
||||||
|
if (exportButtons.length > 0) {
|
||||||
|
// Check if start and end dates are present in the URL
|
||||||
|
let urlStartDate = getParameterByName('start_date');
|
||||||
|
let urlEndDate = getParameterByName('end_date');
|
||||||
|
|
||||||
|
// Set input values based on URL parameters or current date
|
||||||
|
startDateInput.value = urlStartDate || currentDate;
|
||||||
|
endDateInput.value = urlEndDate || currentDate;
|
||||||
|
|
||||||
|
exportButtons.forEach((btn) => {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
// Get the selected start and end dates
|
||||||
|
let startDate = startDateInput.value;
|
||||||
|
let endDate = endDateInput.value;
|
||||||
|
let exportUrl = btn.dataset.exportUrl;
|
||||||
|
|
||||||
|
// Build the URL with parameters
|
||||||
|
exportUrl += "?start_date=" + startDate + "&end_date=" + endDate;
|
||||||
|
|
||||||
|
// Redirect to the export URL
|
||||||
|
window.location.href = exportUrl;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
createComparativeColumnChart("myChart1", "Managed domains", "Start Date", "End Date");
|
||||||
|
createComparativeColumnChart("myChart2", "Unmanaged domains", "Start Date", "End Date");
|
||||||
|
createComparativeColumnChart("myChart3", "Deleted domains", "Start Date", "End Date");
|
||||||
|
createComparativeColumnChart("myChart4", "Ready domains", "Start Date", "End Date");
|
||||||
|
createComparativeColumnChart("myChart5", "Submitted requests", "Start Date", "End Date");
|
||||||
|
createComparativeColumnChart("myChart6", "All requests", "Start Date", "End Date");
|
||||||
|
});
|
||||||
|
|
||||||
|
function createComparativeColumnChart(canvasId, title, labelOne, labelTwo) {
|
||||||
|
var canvas = document.getElementById(canvasId);
|
||||||
|
var ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
var listOne = JSON.parse(canvas.getAttribute('data-list-one'));
|
||||||
|
var listTwo = JSON.parse(canvas.getAttribute('data-list-two'));
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
labels: ["Total", "Federal", "Interstate", "State/Territory", "Tribal", "County", "City", "Special District", "School District", "Election Board"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: labelOne,
|
||||||
|
backgroundColor: "rgba(255, 99, 132, 0.2)",
|
||||||
|
borderColor: "rgba(255, 99, 132, 1)",
|
||||||
|
borderWidth: 1,
|
||||||
|
data: listOne,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: labelTwo,
|
||||||
|
backgroundColor: "rgba(75, 192, 192, 0.2)",
|
||||||
|
borderColor: "rgba(75, 192, 192, 1)",
|
||||||
|
borderWidth: 1,
|
||||||
|
data: listTwo,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: title
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: "bar",
|
||||||
|
data: data,
|
||||||
|
options: options,
|
||||||
|
});
|
||||||
|
}
|
|
@ -112,7 +112,8 @@ html[data-theme="light"] {
|
||||||
.change-list .usa-table thead th,
|
.change-list .usa-table thead th,
|
||||||
body.dashboard,
|
body.dashboard,
|
||||||
body.change-list,
|
body.change-list,
|
||||||
body.change-form {
|
body.change-form,
|
||||||
|
.analytics {
|
||||||
color: var(--body-fg);
|
color: var(--body-fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,6 +144,10 @@ h1, h2, h3,
|
||||||
font-weight: font-weight('bold');
|
font-weight: font-weight('bold');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div#content > h2 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.module h3 {
|
.module h3 {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: var(--link-fg);
|
color: var(--link-fg);
|
||||||
|
@ -299,3 +304,45 @@ input.admin-confirm-button {
|
||||||
display: contents !important;
|
display: contents !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-button-group {
|
||||||
|
margin-left: -0.25rem!important;
|
||||||
|
padding-left: 0!important;
|
||||||
|
.usa-button-group__item {
|
||||||
|
list-style-type: none;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 8px;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
.usa-icon {
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
a.button:active, a.button:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module--custom {
|
||||||
|
a {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: solid 1px var(--darkened-bg);
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-modal--django-admin .usa-prose ul > li {
|
||||||
|
list-style-type: inherit;
|
||||||
|
// Styling based off of the <p> styling in django admin
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
max-width: 68ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-summary-box__dhs-color {
|
||||||
|
color: $dhs-blue-70;
|
||||||
|
}
|
||||||
|
|
|
@ -38,3 +38,18 @@ legend.float-left-tablet + button.float-right-tablet {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom style for disabled inputs
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
.usa-input:disabled, .usa-select:disabled, .usa-textarea:disabled {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.usa-input:disabled, .usa-select:disabled, .usa-textarea:disabled {
|
||||||
|
background-color: var(--body-fg);
|
||||||
|
color: var(--close-button-hover-bg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -126,7 +126,6 @@ in the form $setting: value,
|
||||||
----------------------------*/
|
----------------------------*/
|
||||||
$theme-input-line-height: 5,
|
$theme-input-line-height: 5,
|
||||||
|
|
||||||
|
|
||||||
/*---------------------------
|
/*---------------------------
|
||||||
# Component settings
|
# Component settings
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
|
@ -74,6 +74,9 @@ secret_aws_s3_key_id = secret("access_key_id", None) or secret("AWS_S3_ACCESS_KE
|
||||||
secret_aws_s3_key = secret("secret_access_key", None) or secret("AWS_S3_SECRET_ACCESS_KEY", None)
|
secret_aws_s3_key = secret("secret_access_key", None) or secret("AWS_S3_SECRET_ACCESS_KEY", None)
|
||||||
secret_aws_s3_bucket_name = secret("bucket", None) or secret("AWS_S3_BUCKET_NAME", None)
|
secret_aws_s3_bucket_name = secret("bucket", None) or secret("AWS_S3_BUCKET_NAME", None)
|
||||||
|
|
||||||
|
# Passphrase for the encrypted metadata email
|
||||||
|
secret_encrypt_metadata = secret("SECRET_ENCRYPT_METADATA", None)
|
||||||
|
|
||||||
secret_registry_cl_id = secret("REGISTRY_CL_ID")
|
secret_registry_cl_id = secret("REGISTRY_CL_ID")
|
||||||
secret_registry_password = secret("REGISTRY_PASSWORD")
|
secret_registry_password = secret("REGISTRY_PASSWORD")
|
||||||
secret_registry_cert = b64decode(secret("REGISTRY_CERT", ""))
|
secret_registry_cert = b64decode(secret("REGISTRY_CERT", ""))
|
||||||
|
@ -94,6 +97,7 @@ DEBUG = env_debug
|
||||||
|
|
||||||
# Controls production specific feature toggles
|
# Controls production specific feature toggles
|
||||||
IS_PRODUCTION = env_is_production
|
IS_PRODUCTION = env_is_production
|
||||||
|
SECRET_ENCRYPT_METADATA = secret_encrypt_metadata
|
||||||
|
|
||||||
# Applications are modular pieces of code.
|
# Applications are modular pieces of code.
|
||||||
# They are provided by Django, by third-parties, or by yourself.
|
# They are provided by Django, by third-parties, or by yourself.
|
||||||
|
@ -326,8 +330,9 @@ CSP_FORM_ACTION = allowed_sources
|
||||||
|
|
||||||
# Google analytics requires that we relax our otherwise
|
# Google analytics requires that we relax our otherwise
|
||||||
# strict CSP by allowing scripts to run from their domain
|
# strict CSP by allowing scripts to run from their domain
|
||||||
# and inline with a nonce, as well as allowing connections back to their domain
|
# and inline with a nonce, as well as allowing connections back to their domain.
|
||||||
CSP_SCRIPT_SRC_ELEM = ["'self'", "https://www.googletagmanager.com/"]
|
# Note: If needed, we can embed chart.js instead of using the CDN
|
||||||
|
CSP_SCRIPT_SRC_ELEM = ["'self'", "https://www.googletagmanager.com/", "https://cdn.jsdelivr.net/npm/chart.js"]
|
||||||
CSP_CONNECT_SRC = ["'self'", "https://www.google-analytics.com/"]
|
CSP_CONNECT_SRC = ["'self'", "https://www.google-analytics.com/"]
|
||||||
CSP_INCLUDE_NONCE_IN = ["script-src-elem"]
|
CSP_INCLUDE_NONCE_IN = ["script-src-elem"]
|
||||||
|
|
||||||
|
@ -635,6 +640,8 @@ ALLOWED_HOSTS = [
|
||||||
"getgov-stable.app.cloud.gov",
|
"getgov-stable.app.cloud.gov",
|
||||||
"getgov-staging.app.cloud.gov",
|
"getgov-staging.app.cloud.gov",
|
||||||
"getgov-development.app.cloud.gov",
|
"getgov-development.app.cloud.gov",
|
||||||
|
"getgov-bob.app.cloud.gov",
|
||||||
|
"getgov-meoward.app.cloud.gov",
|
||||||
"getgov-backup.app.cloud.gov",
|
"getgov-backup.app.cloud.gov",
|
||||||
"getgov-ky.app.cloud.gov",
|
"getgov-ky.app.cloud.gov",
|
||||||
"getgov-es.app.cloud.gov",
|
"getgov-es.app.cloud.gov",
|
||||||
|
|
|
@ -9,9 +9,16 @@ from django.urls import include, path
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
from registrar import views
|
from registrar import views
|
||||||
|
from registrar.views.admin_views import (
|
||||||
from registrar.views.admin_views import ExportData
|
ExportDataDomainsGrowth,
|
||||||
|
ExportDataFederal,
|
||||||
|
ExportDataFull,
|
||||||
|
ExportDataManagedDomains,
|
||||||
|
ExportDataRequestsGrowth,
|
||||||
|
ExportDataType,
|
||||||
|
ExportDataUnmanagedDomains,
|
||||||
|
AnalyticsView,
|
||||||
|
)
|
||||||
|
|
||||||
from registrar.views.domain_request import Step
|
from registrar.views.domain_request import Step
|
||||||
from registrar.views.utility import always_404
|
from registrar.views.utility import always_404
|
||||||
|
@ -52,7 +59,46 @@ urlpatterns = [
|
||||||
"admin/logout/",
|
"admin/logout/",
|
||||||
RedirectView.as_view(pattern_name="logout", permanent=False),
|
RedirectView.as_view(pattern_name="logout", permanent=False),
|
||||||
),
|
),
|
||||||
path("export_data/", ExportData.as_view(), name="admin_export_data"),
|
path(
|
||||||
|
"admin/analytics/export_data_type/",
|
||||||
|
ExportDataType.as_view(),
|
||||||
|
name="export_data_type",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_data_full/",
|
||||||
|
ExportDataFull.as_view(),
|
||||||
|
name="export_data_full",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_data_federal/",
|
||||||
|
ExportDataFederal.as_view(),
|
||||||
|
name="export_data_federal",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_domains_growth/",
|
||||||
|
ExportDataDomainsGrowth.as_view(),
|
||||||
|
name="export_domains_growth",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_requests_growth/",
|
||||||
|
ExportDataRequestsGrowth.as_view(),
|
||||||
|
name="export_requests_growth",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_managed_domains/",
|
||||||
|
ExportDataManagedDomains.as_view(),
|
||||||
|
name="export_managed_domains",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_unmanaged_domains/",
|
||||||
|
ExportDataUnmanagedDomains.as_view(),
|
||||||
|
name="export_unmanaged_domains",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/",
|
||||||
|
AnalyticsView.as_view(),
|
||||||
|
name="analytics",
|
||||||
|
),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path(
|
path(
|
||||||
"domain-request/<id>/edit/",
|
"domain-request/<id>/edit/",
|
||||||
|
@ -149,6 +195,18 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Djangooidc strips out context data from that context, so we define a custom error
|
||||||
|
# view through this method.
|
||||||
|
# If Djangooidc is left to its own devices and uses reverse directly,
|
||||||
|
# then both context and session information will be obliterated due to:
|
||||||
|
|
||||||
|
# a) Djangooidc being out of scope for context_processors
|
||||||
|
# b) Potential cyclical import errors restricting what kind of data is passable.
|
||||||
|
|
||||||
|
# Rather than dealing with that, we keep everything centralized in one location.
|
||||||
|
# This way, we can share a view for djangooidc, and other pages as we see fit.
|
||||||
|
handler500 = "registrar.views.utility.error_views.custom_500_error_view"
|
||||||
|
|
||||||
# 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
|
||||||
|
|
|
@ -163,6 +163,12 @@ class UserFixture:
|
||||||
"last_name": "Chin-Analyst",
|
"last_name": "Chin-Analyst",
|
||||||
"email": "szu.chin@ecstech.com",
|
"email": "szu.chin@ecstech.com",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "d9839768-0c17-4fa2-9c8e-36291eef5c11",
|
||||||
|
"first_name": "Alex-Analyst",
|
||||||
|
"last_name": "Mcelya-Analyst",
|
||||||
|
"email": "ALEXANDER.MCELYA@cisa.dhs.gov",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
def load_users(cls, users, group_name):
|
def load_users(cls, users, group_name):
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
"""Forms for domain management."""
|
"""Forms for domain management."""
|
||||||
|
|
||||||
|
import logging
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
|
||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
|
from registrar.models import DomainRequest
|
||||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||||
|
from registrar.models.utility.domain_helper import DomainHelper
|
||||||
from registrar.utility.errors import (
|
from registrar.utility.errors import (
|
||||||
NameserverError,
|
NameserverError,
|
||||||
NameserverErrorCodes as nsErrorCodes,
|
NameserverErrorCodes as nsErrorCodes,
|
||||||
|
@ -23,6 +25,9 @@ from .common import (
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DomainAddUserForm(forms.Form):
|
class DomainAddUserForm(forms.Form):
|
||||||
"""Form for adding a user to a domain."""
|
"""Form for adding a user to a domain."""
|
||||||
|
|
||||||
|
@ -205,6 +210,13 @@ class ContactForm(forms.ModelForm):
|
||||||
"required": "Enter your email address in the required format, like name@example.com."
|
"required": "Enter your email address in the required format, like name@example.com."
|
||||||
}
|
}
|
||||||
self.fields["phone"].error_messages["required"] = "Enter your phone number."
|
self.fields["phone"].error_messages["required"] = "Enter your phone number."
|
||||||
|
self.domainInfo = None
|
||||||
|
|
||||||
|
def set_domain_info(self, domainInfo):
|
||||||
|
"""Set the domain information for the form.
|
||||||
|
The form instance is associated with the contact itself. In order to access the associated
|
||||||
|
domain information object, this needs to be set in the form by the view."""
|
||||||
|
self.domainInfo = domainInfo
|
||||||
|
|
||||||
|
|
||||||
class AuthorizingOfficialContactForm(ContactForm):
|
class AuthorizingOfficialContactForm(ContactForm):
|
||||||
|
@ -212,7 +224,7 @@ class AuthorizingOfficialContactForm(ContactForm):
|
||||||
|
|
||||||
JOIN = "authorizing_official"
|
JOIN = "authorizing_official"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, disable_fields=False, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Overriding bc phone not required in this form
|
# Overriding bc phone not required in this form
|
||||||
|
@ -232,20 +244,36 @@ class AuthorizingOfficialContactForm(ContactForm):
|
||||||
self.fields["email"].error_messages = {
|
self.fields["email"].error_messages = {
|
||||||
"required": "Enter an email address in the required format, like name@example.com."
|
"required": "Enter an email address in the required format, like name@example.com."
|
||||||
}
|
}
|
||||||
self.domainInfo = None
|
|
||||||
|
|
||||||
def set_domain_info(self, domainInfo):
|
# All fields should be disabled if the domain is federal or tribal
|
||||||
"""Set the domain information for the form.
|
if disable_fields:
|
||||||
The form instance is associated with the contact itself. In order to access the associated
|
DomainHelper.mass_disable_fields(fields=self.fields, disable_required=True, disable_maxlength=True)
|
||||||
domain information object, this needs to be set in the form by the view."""
|
|
||||||
self.domainInfo = domainInfo
|
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
"""Override the save() method of the BaseModelForm."""
|
"""
|
||||||
|
Override the save() method of the BaseModelForm.
|
||||||
|
Used to perform checks on the underlying domain_information object.
|
||||||
|
If this doesn't exist, we just save as normal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If the underlying Domain doesn't have a domainInfo object,
|
||||||
|
# just let the default super handle it.
|
||||||
|
if not self.domainInfo:
|
||||||
|
return super().save()
|
||||||
|
|
||||||
|
# Determine if the domain is federal or tribal
|
||||||
|
is_federal = self.domainInfo.organization_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||||
|
is_tribal = self.domainInfo.organization_type == DomainRequest.OrganizationChoices.TRIBAL
|
||||||
|
|
||||||
# Get the Contact object from the db for the Authorizing Official
|
# Get the Contact object from the db for the Authorizing Official
|
||||||
db_ao = Contact.objects.get(id=self.instance.id)
|
db_ao = Contact.objects.get(id=self.instance.id)
|
||||||
if self.domainInfo and db_ao.has_more_than_one_join("information_authorizing_official"):
|
|
||||||
|
if (is_federal or is_tribal) and self.has_changed():
|
||||||
|
# This action should be blocked by the UI, as the text fields are readonly.
|
||||||
|
# If they get past this point, we forbid it this way.
|
||||||
|
# This could be malicious, so lets reserve information for the backend only.
|
||||||
|
raise ValueError("Authorizing Official cannot be modified for federal or tribal domains.")
|
||||||
|
elif db_ao.has_more_than_one_join("information_authorizing_official"):
|
||||||
# Handle the case where the domain information object is available and the AO Contact
|
# Handle the case where the domain information object is available and the AO Contact
|
||||||
# has more than one joined object.
|
# has more than one joined object.
|
||||||
# In this case, create a new Contact, and update the new Contact with form data.
|
# In this case, create a new Contact, and update the new Contact with form data.
|
||||||
|
@ -254,6 +282,7 @@ class AuthorizingOfficialContactForm(ContactForm):
|
||||||
self.domainInfo.authorizing_official = Contact.objects.create(**data)
|
self.domainInfo.authorizing_official = Contact.objects.create(**data)
|
||||||
self.domainInfo.save()
|
self.domainInfo.save()
|
||||||
else:
|
else:
|
||||||
|
# If all checks pass, just save normally
|
||||||
super().save()
|
super().save()
|
||||||
|
|
||||||
|
|
||||||
|
@ -304,11 +333,11 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
# We need to set the required attributed for federal_agency and
|
# We need to set the required attributed for State/territory
|
||||||
# state/territory because for these fields we are creating an individual
|
# because for this fields we are creating an individual
|
||||||
# instance of the Select. For the other fields we use the for loop to set
|
# instance of the Select. For the other fields we use the for loop to set
|
||||||
# the class's required attribute to true.
|
# the class's required attribute to true.
|
||||||
"federal_agency": forms.Select(attrs={"required": True}, choices=DomainInformation.AGENCY_CHOICES),
|
"federal_agency": forms.TextInput,
|
||||||
"organization_name": forms.TextInput,
|
"organization_name": forms.TextInput,
|
||||||
"address_line1": forms.TextInput,
|
"address_line1": forms.TextInput,
|
||||||
"address_line2": forms.TextInput,
|
"address_line2": forms.TextInput,
|
||||||
|
@ -334,6 +363,46 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
||||||
self.fields["state_territory"].widget.attrs.pop("maxlength", None)
|
self.fields["state_territory"].widget.attrs.pop("maxlength", None)
|
||||||
self.fields["zipcode"].widget.attrs.pop("maxlength", None)
|
self.fields["zipcode"].widget.attrs.pop("maxlength", None)
|
||||||
|
|
||||||
|
self.is_federal = self.instance.organization_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||||
|
self.is_tribal = self.instance.organization_type == DomainRequest.OrganizationChoices.TRIBAL
|
||||||
|
|
||||||
|
field_to_disable = None
|
||||||
|
if self.is_federal:
|
||||||
|
field_to_disable = "federal_agency"
|
||||||
|
elif self.is_tribal:
|
||||||
|
field_to_disable = "organization_name"
|
||||||
|
|
||||||
|
# Disable any field that should be disabled, if applicable
|
||||||
|
if field_to_disable is not None:
|
||||||
|
DomainHelper.disable_field(self.fields[field_to_disable], disable_required=True)
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
"""Override the save() method of the BaseModelForm."""
|
||||||
|
if self.has_changed():
|
||||||
|
|
||||||
|
# This action should be blocked by the UI, as the text fields are readonly.
|
||||||
|
# If they get past this point, we forbid it this way.
|
||||||
|
# This could be malicious, so lets reserve information for the backend only.
|
||||||
|
if self.is_federal and not self._field_unchanged("federal_agency"):
|
||||||
|
raise ValueError("federal_agency cannot be modified when the organization_type is federal")
|
||||||
|
elif self.is_tribal and not self._field_unchanged("organization_name"):
|
||||||
|
raise ValueError("organization_name cannot be modified when the organization_type is tribal")
|
||||||
|
|
||||||
|
else:
|
||||||
|
super().save()
|
||||||
|
|
||||||
|
def _field_unchanged(self, field_name) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if a specified field has not changed between the old value
|
||||||
|
and the new value.
|
||||||
|
|
||||||
|
The old value is grabbed from self.initial.
|
||||||
|
The new value is grabbed from self.cleaned_data.
|
||||||
|
"""
|
||||||
|
old_value = self.initial.get(field_name, None)
|
||||||
|
new_value = self.cleaned_data.get(field_name, None)
|
||||||
|
return old_value == new_value
|
||||||
|
|
||||||
|
|
||||||
class DomainDnssecForm(forms.Form):
|
class DomainDnssecForm(forms.Form):
|
||||||
"""Form for enabling and disabling dnssec"""
|
"""Form for enabling and disabling dnssec"""
|
||||||
|
|
|
@ -319,8 +319,8 @@ class AboutYourOrganizationForm(RegistrarForm):
|
||||||
widget=forms.Textarea(),
|
widget=forms.Textarea(),
|
||||||
validators=[
|
validators=[
|
||||||
MaxLengthValidator(
|
MaxLengthValidator(
|
||||||
1000,
|
2000,
|
||||||
message="Response must be less than 1000 characters.",
|
message="Response must be less than 2000 characters.",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
error_messages={"required": ("Enter more information about your organization.")},
|
error_messages={"required": ("Enter more information about your organization.")},
|
||||||
|
@ -515,8 +515,8 @@ class PurposeForm(RegistrarForm):
|
||||||
widget=forms.Textarea(),
|
widget=forms.Textarea(),
|
||||||
validators=[
|
validators=[
|
||||||
MaxLengthValidator(
|
MaxLengthValidator(
|
||||||
1000,
|
2000,
|
||||||
message="Response must be less than 1000 characters.",
|
message="Response must be less than 2000 characters.",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
error_messages={"required": "Describe how you’ll use the .gov domain you’re requesting."},
|
error_messages={"required": "Describe how you’ll use the .gov domain you’re requesting."},
|
||||||
|
@ -830,8 +830,8 @@ class AnythingElseForm(RegistrarForm):
|
||||||
widget=forms.Textarea(),
|
widget=forms.Textarea(),
|
||||||
validators=[
|
validators=[
|
||||||
MaxLengthValidator(
|
MaxLengthValidator(
|
||||||
1000,
|
2000,
|
||||||
message="Response must be less than 1000 characters.",
|
message="Response must be less than 2000 characters.",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
"""Generates current-metadata.csv then uploads to S3 + sends email"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pyzipper
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
from registrar.utility import csv_export
|
||||||
|
from registrar.utility.s3_bucket import S3ClientHelper
|
||||||
|
from ...utility.email import send_templated_email
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = (
|
||||||
|
"Generates and uploads a domain-metadata.csv file to our S3 bucket "
|
||||||
|
"which is based off of all existing Domains."
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""Add our two filename arguments."""
|
||||||
|
parser.add_argument("--directory", default="migrationdata", help="Desired directory")
|
||||||
|
parser.add_argument(
|
||||||
|
"--checkpath",
|
||||||
|
default=True,
|
||||||
|
help="Flag that determines if we do a check for os.path.exists. Used for test cases",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, **options):
|
||||||
|
"""Grabs the directory then creates domain-metadata.csv in that directory"""
|
||||||
|
file_name = "domain-metadata.csv"
|
||||||
|
# Ensures a slash is added
|
||||||
|
directory = os.path.join(options.get("directory"), "")
|
||||||
|
check_path = options.get("checkpath")
|
||||||
|
|
||||||
|
logger.info("Generating report...")
|
||||||
|
try:
|
||||||
|
self.email_current_metadata_report(directory, file_name, check_path)
|
||||||
|
except Exception as err:
|
||||||
|
# TODO - #1317: Notify operations when auto report generation fails
|
||||||
|
raise err
|
||||||
|
else:
|
||||||
|
logger.info(f"Success! Created {file_name} and successfully sent out an email!")
|
||||||
|
|
||||||
|
def email_current_metadata_report(self, directory, file_name, check_path):
|
||||||
|
"""Creates a current-metadata.csv file under the specified directory,
|
||||||
|
then uploads it to a AWS S3 bucket. This is done for resiliency
|
||||||
|
reasons in the event our application goes down and/or the email
|
||||||
|
cannot send -- we'll still be able to grab info from the S3
|
||||||
|
instance"""
|
||||||
|
s3_client = S3ClientHelper()
|
||||||
|
file_path = os.path.join(directory, file_name)
|
||||||
|
|
||||||
|
# Generate a file locally for upload
|
||||||
|
with open(file_path, "w") as file:
|
||||||
|
csv_export.export_data_type_to_csv(file)
|
||||||
|
|
||||||
|
if check_path and not os.path.exists(file_path):
|
||||||
|
raise FileNotFoundError(f"Could not find newly created file at '{file_path}'")
|
||||||
|
|
||||||
|
s3_client.upload_file(file_path, file_name)
|
||||||
|
|
||||||
|
# Set zip file name
|
||||||
|
current_date = datetime.now().strftime("%m%d%Y")
|
||||||
|
current_filename = f"domain-metadata-{current_date}.zip"
|
||||||
|
|
||||||
|
# Pre-set zip file name
|
||||||
|
encrypted_metadata_output = current_filename
|
||||||
|
|
||||||
|
# Set context for the subject
|
||||||
|
current_date_str = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# Encrypt the metadata
|
||||||
|
encrypted_metadata_in_bytes = self._encrypt_metadata(
|
||||||
|
s3_client.get_file(file_name), encrypted_metadata_output, str.encode(settings.SECRET_ENCRYPT_METADATA)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send the metadata file that is zipped
|
||||||
|
send_templated_email(
|
||||||
|
template_name="emails/metadata_body.txt",
|
||||||
|
subject_template_name="emails/metadata_subject.txt",
|
||||||
|
to_address=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
context={"current_date_str": current_date_str},
|
||||||
|
attachment_file=encrypted_metadata_in_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _encrypt_metadata(self, input_file, output_file, password):
|
||||||
|
"""Helper function for encrypting the attachment file"""
|
||||||
|
current_date = datetime.now().strftime("%m%d%Y")
|
||||||
|
current_filename = f"domain-metadata-{current_date}.csv"
|
||||||
|
# Using ZIP_DEFLATED bc it's a more common compression method supported by most zip utilities and faster
|
||||||
|
# We could also use compression=pyzipper.ZIP_LZMA if we are looking for smaller file size
|
||||||
|
with pyzipper.AESZipFile(
|
||||||
|
output_file, "w", compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES
|
||||||
|
) as f_out:
|
||||||
|
f_out.setpassword(password)
|
||||||
|
f_out.writestr(current_filename, input_file)
|
||||||
|
with open(output_file, "rb") as file_data:
|
||||||
|
attachment_in_bytes = file_data.read()
|
||||||
|
return attachment_in_bytes
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-03-13 21:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0075_create_groups_v08"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="current_websites",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, related_name="current+", to="registrar.website", verbose_name="Current websites"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="other_contacts",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="contact_domain_requests",
|
||||||
|
to="registrar.contact",
|
||||||
|
verbose_name="Other employees",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="other_contacts",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="contact_domain_requests_information",
|
||||||
|
to="registrar.contact",
|
||||||
|
verbose_name="Other employees",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -198,6 +198,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
is called in the validate function on the request/domain page
|
is called in the validate function on the request/domain page
|
||||||
|
|
||||||
throws- RegistryError or InvalidDomainError"""
|
throws- RegistryError or InvalidDomainError"""
|
||||||
|
|
||||||
if not cls.string_could_be_domain(domain):
|
if not cls.string_could_be_domain(domain):
|
||||||
logger.warning("Not a valid domain: %s" % str(domain))
|
logger.warning("Not a valid domain: %s" % str(domain))
|
||||||
# throw invalid domain error so that it can be caught in
|
# throw invalid domain error so that it can be caught in
|
||||||
|
|
|
@ -183,7 +183,7 @@ class DomainInformation(TimeStampedModel):
|
||||||
"registrar.Contact",
|
"registrar.Contact",
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="contact_domain_requests_information",
|
related_name="contact_domain_requests_information",
|
||||||
verbose_name="contacts",
|
verbose_name="Other employees",
|
||||||
)
|
)
|
||||||
|
|
||||||
no_other_contacts_rationale = models.TextField(
|
no_other_contacts_rationale = models.TextField(
|
||||||
|
|
|
@ -505,7 +505,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
"registrar.Website",
|
"registrar.Website",
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="current+",
|
related_name="current+",
|
||||||
verbose_name="websites",
|
verbose_name="Current websites",
|
||||||
)
|
)
|
||||||
|
|
||||||
approved_domain = models.OneToOneField(
|
approved_domain = models.OneToOneField(
|
||||||
|
@ -551,7 +551,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
"registrar.Contact",
|
"registrar.Contact",
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="contact_domain_requests",
|
related_name="contact_domain_requests",
|
||||||
verbose_name="contacts",
|
verbose_name="Other employees",
|
||||||
)
|
)
|
||||||
|
|
||||||
no_other_contacts_rationale = models.TextField(
|
no_other_contacts_rationale = models.TextField(
|
||||||
|
|
|
@ -188,3 +188,33 @@ class DomainHelper:
|
||||||
common_fields = model_1_fields & model_2_fields
|
common_fields = model_1_fields & model_2_fields
|
||||||
|
|
||||||
return common_fields
|
return common_fields
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mass_disable_fields(fields, disable_required=False, disable_maxlength=False):
|
||||||
|
"""
|
||||||
|
Given some fields, invoke .disabled = True on them.
|
||||||
|
disable_required: bool -> invokes .required = False on each field.
|
||||||
|
disable_maxlength: bool -> pops "maxlength" from each field.
|
||||||
|
"""
|
||||||
|
for field in fields.values():
|
||||||
|
field = DomainHelper.disable_field(field, disable_required, disable_maxlength)
|
||||||
|
return fields
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def disable_field(field, disable_required=False, disable_maxlength=False):
|
||||||
|
"""
|
||||||
|
Given a fields, invoke .disabled = True on it.
|
||||||
|
disable_required: bool -> invokes .required = False for the field.
|
||||||
|
disable_maxlength: bool -> pops "maxlength" for the field.
|
||||||
|
"""
|
||||||
|
field.disabled = True
|
||||||
|
|
||||||
|
if disable_required:
|
||||||
|
# if a field is disabled, it can't be required
|
||||||
|
field.required = False
|
||||||
|
|
||||||
|
if disable_maxlength:
|
||||||
|
# Remove the maxlength dialog
|
||||||
|
if "maxlength" in field.widget.attrs:
|
||||||
|
field.widget.attrs.pop("maxlength", None)
|
||||||
|
return field
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
<svg width="404" height="409" viewBox="0 0 404 409" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M291.707 328.743C240.024 358.583 133.444 374.87 78.8899 280.379C14.3648 168.618 78.2559 99.3488 140.956 63.1491C203.655 26.9495 296.801 80.4848 337.226 150.503C377.652 220.522 343.391 298.903 291.707 328.743Z" fill="#F5F8FA"/>
|
|
||||||
<circle cx="276.88" cy="130.594" r="8" transform="rotate(135 276.88 130.594)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="288.196" cy="119.279" r="8" transform="rotate(135 288.196 119.279)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="231.626" cy="175.849" r="8" transform="rotate(135 231.626 175.849)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="186.371" cy="221.104" r="8" transform="rotate(135 186.371 221.104)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="242.939" cy="164.535" r="8" transform="rotate(135 242.939 164.535)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="197.686" cy="209.788" r="8" transform="rotate(135 197.686 209.788)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="220.312" cy="187.163" r="8" transform="rotate(135 220.312 187.163)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="175.057" cy="232.417" r="8" transform="rotate(135 175.057 232.417)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="163.743" cy="243.731" r="8" transform="rotate(135 163.743 243.731)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="152.43" cy="255.045" r="8" transform="rotate(135 152.43 255.045)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="141.116" cy="266.358" r="8" transform="rotate(135 141.116 266.358)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="129.802" cy="277.672" r="8" transform="rotate(135 129.802 277.672)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="118.489" cy="288.986" r="8" transform="rotate(135 118.489 288.986)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="254.253" cy="153.221" r="8" transform="rotate(135 254.253 153.221)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="265.566" cy="141.908" r="8" transform="rotate(135 265.566 141.908)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="208.998" cy="198.476" r="8" transform="rotate(135 208.998 198.476)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="203.342" cy="203.999" r="120.001" stroke="#7AA5C1" stroke-width="16"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.9 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 53 KiB |
|
@ -1,59 +0,0 @@
|
||||||
<svg width="409" height="214" viewBox="0 0 409 214" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M366.004 90.4612C372.017 135.603 322.608 199.102 196.168 205.902C-32.9139 218.22 19.0655 18.8457 205.511 8.81994C299.204 3.78172 359.99 45.3195 366.004 90.4612Z" fill="#F5F8FA"/>
|
|
||||||
<circle cx="213.873" cy="37.4943" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="212.214" cy="58.4272" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="231.089" cy="66.2297" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="235.451" cy="85.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="252.535" cy="99.7787" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="273.981" cy="115.121" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="372.072" cy="182.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="402.423" cy="189.642" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="220.441" cy="99.6379" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="231.089" cy="118.336" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="262.722" cy="143.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="248.722" cy="125.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="288.537" cy="138.04" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="300.051" cy="160.304" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="338.722" cy="170.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="349.914" cy="189.575" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="330.21" cy="189.575" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="316.722" cy="173.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="305.722" cy="190.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="287.722" cy="184.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="269.894" cy="183.007" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="250.19" cy="189.575" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="220.722" cy="192.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="233.722" cy="173.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="252.722" cy="165.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="180.203" cy="189.575" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="161.329" cy="196.143" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="140.795" cy="189.575" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<path d="M122.733 189.575C122.733 193.203 119.793 196.143 116.165 196.143C112.538 196.143 109.597 193.203 109.597 189.575C109.597 185.948 112.538 183.007 116.165 183.007C119.793 183.007 122.733 185.948 122.733 189.575Z" fill="#7AA5C1"/>
|
|
||||||
<circle cx="91.5343" cy="189.642" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="57.8852" cy="185.432" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="198.722" cy="195.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="193.34" cy="70.2917" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="98.9323" cy="156.735" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="25.247" cy="189.642" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="7.26164" cy="190.584" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="76.7592" cy="173.44" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="129.722" cy="172.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="147.722" cy="164.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="164.722" cy="173.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="174.465" cy="150.167" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="186.771" cy="169.871" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="198.266" cy="150.167" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="232.747" cy="151.176" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="273.722" cy="162.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="210.589" cy="163.303" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="204.834" cy="124.904" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="193.34" cy="108.553" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="171.181" cy="119.345" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="160.499" cy="138.04" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="139.153" cy="144.608" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="170.351" cy="95.4167" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="151.477" cy="115.121" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="125.204" cy="137.031" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="112.881" cy="163.303" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="204.834" cy="86.6427" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 3.9 KiB |
196
src/registrar/templates/admin/analytics.html
Normal file
196
src/registrar/templates/admin/analytics.html
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content_title %}<h1>Registrar Analytics</h1>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div id="content-main" class="analytics">
|
||||||
|
|
||||||
|
<div class="grid-row grid-gap-2">
|
||||||
|
<div class="tablet:grid-col-6 margin-top-2">
|
||||||
|
<div class="module height-full">
|
||||||
|
<h2>At a glance</h2>
|
||||||
|
<div class="padding-top-2 padding-x-2">
|
||||||
|
<ul>
|
||||||
|
<li>User Count: {{ data.user_count }}</li>
|
||||||
|
<li>Domain Count: {{ data.domain_count }}</li>
|
||||||
|
<li>Domains in READY state: {{ data.ready_domain_count }}</li>
|
||||||
|
<li>Domain applications (last 30 days): {{ data.last_30_days_applications }}</li>
|
||||||
|
<li>Approved applications (last 30 days): {{ data.last_30_days_approved_applications }}</li>
|
||||||
|
<li>Average approval time for applications (last 30 days): {{ data.average_application_approval_time_last_30_days }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tablet:grid-col-6 margin-top-2">
|
||||||
|
<div class="module height-full">
|
||||||
|
<h2>Current domains</h2>
|
||||||
|
<div class="padding-top-2 padding-x-2">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<a href="{% url 'export_data_type' %}" class="button" role="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">All domain metadata</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<a href="{% url 'export_data_full' %}" class="button" role="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">Current full</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<a href="{% url 'export_data_federal' %}" class="button" role="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">Current federal</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-row grid-gap-2 margin-top-2">
|
||||||
|
<div class="grid-col">
|
||||||
|
<div class="module">
|
||||||
|
<h2>Growth reports</h2>
|
||||||
|
<div class="padding-2">
|
||||||
|
{% comment %}
|
||||||
|
Inputs of type date suck for accessibility.
|
||||||
|
We'll need to replace those guys with a django form once we figure out how to hook one onto this page.
|
||||||
|
See the commit "Review for ticket #999"
|
||||||
|
{% endcomment %}
|
||||||
|
<div class="display-flex flex-align-baseline margin-top-1 margin-bottom-2">
|
||||||
|
<div class="margin-right-1">
|
||||||
|
<label for="start">Start date:</label>
|
||||||
|
<input type="date" id="start" name="start" value="2018-07-22" min="2018-01-01" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="end">End date:</label>
|
||||||
|
<input type="date" id="end" name="end" value="2023-12-01" min="2023-12-01" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button class="button exportLink" data-export-url="{% url 'export_domains_growth' %}" type="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">Domain growth</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button class="button exportLink" data-export-url="{% url 'export_requests_growth' %}" type="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">Request growth</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button class="button exportLink" data-export-url="{% url 'export_managed_domains' %}" type="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">Managed domains</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button class="button exportLink" data-export-url="{% url 'export_unmanaged_domains' %}" type="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">Unmanaged domains</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button class="button exportLink usa-button--secondary" data-export-url="{% url 'analytics' %}" type="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#assessment"></use>
|
||||||
|
</svg><span class="margin-left-05">Update charts</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="grid-row grid-gap-2 margin-y-2">
|
||||||
|
<div class="grid-col">
|
||||||
|
<canvas id="myChart1" width="400" height="200"
|
||||||
|
aria-label="Chart: {{ data.managed_domains_sliced_at_end_date.0 }} managed domains for {{ data.end_date }}"
|
||||||
|
role="img"
|
||||||
|
data-list-one="{{data.managed_domains_sliced_at_start_date}}"
|
||||||
|
data-list-two="{{data.managed_domains_sliced_at_end_date}}"
|
||||||
|
>
|
||||||
|
<h2>Chart: Managed domains</h2>
|
||||||
|
<p>{{ data.managed_domains_sliced_at_end_date.0 }} managed domains for {{ data.end_date }}</p>
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
<div class="grid-col">
|
||||||
|
<canvas id="myChart2" width="400" height="200"
|
||||||
|
aria-label="Chart: {{ data.unmanaged_domains_sliced_at_end_date.0 }} unmanaged domains for {{ data.end_date }}"
|
||||||
|
role="img"
|
||||||
|
data-list-one="{{data.unmanaged_domains_sliced_at_start_date}}"
|
||||||
|
data-list-two="{{data.unmanaged_domains_sliced_at_end_date}}"
|
||||||
|
>
|
||||||
|
<h2>Chart: Unmanaged domains</h2>
|
||||||
|
<p>{{ data.unmanaged_domains_sliced_at_end_date.0 }} unmanaged domains for {{ data.end_date }}</p>
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-row grid-gap-2 margin-y-2">
|
||||||
|
<div class="grid-col">
|
||||||
|
<canvas id="myChart3" width="400" height="200"
|
||||||
|
aria-label="Chart: {{ data.deleted_domains_sliced_at_end_date.0 }} deleted domains for {{ data.end_date }}"
|
||||||
|
role="img"
|
||||||
|
data-list-one="{{data.deleted_domains_sliced_at_start_date}}"
|
||||||
|
data-list-two="{{data.deleted_domains_sliced_at_end_date}}"
|
||||||
|
>
|
||||||
|
<h2>Chart: Deleted domains</h2>
|
||||||
|
<p>{{ data.deleted_domains_sliced_at_end_date.0 }} deleted domains for {{ data.end_date }}</p>
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
<div class="grid-col">
|
||||||
|
<canvas id="myChart4" width="400" height="200"
|
||||||
|
aria-label="Chart: {{ data.ready_domains_sliced_at_end_date.0 }} ready domains for {{ data.end_date }}"
|
||||||
|
role="img"
|
||||||
|
data-list-one="{{data.ready_domains_sliced_at_start_date}}"
|
||||||
|
data-list-two="{{data.ready_domains_sliced_at_end_date}}"
|
||||||
|
>
|
||||||
|
<h2>Chart: Ready domains</h2>
|
||||||
|
<p>{{ data.ready_domains_sliced_at_end_date.0 }} ready domains for {{ data.end_date }}</p>
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-row grid-gap-2 margin-y-2">
|
||||||
|
<div class="grid-col">
|
||||||
|
<canvas id="myChart5" width="400" height="200"
|
||||||
|
aria-label="Chart: {{ data.submitted_requests_sliced_at_end_date.0 }} submitted requests for {{ data.end_date }}"
|
||||||
|
role="img"
|
||||||
|
data-list-one="{{data.submitted_requests_sliced_at_start_date}}"
|
||||||
|
data-list-two="{{data.submitted_requests_sliced_at_end_date}}"
|
||||||
|
>
|
||||||
|
<h2>Chart: Submitted requests</h2>
|
||||||
|
<p>{{ data.submitted_requests_sliced_at_end_date.0 }} submitted requests for {{ data.end_date }}</p>
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
<div class="grid-col">
|
||||||
|
<canvas id="myChart6" width="400" height="200"
|
||||||
|
aria-label="Chart: {{ data.requests_sliced_at_end_date.0 }} requests for {{ data.end_date }}"
|
||||||
|
role="img"
|
||||||
|
data-list-one="{{data.requests_sliced_at_start_date}}"
|
||||||
|
data-list-two="{{data.requests_sliced_at_end_date}}"
|
||||||
|
>
|
||||||
|
<h2>Chart: All requests</h2>
|
||||||
|
<p>{{ data.requests_sliced_at_end_date.0 }} requests for {{ data.end_date }}</p>
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -64,6 +64,11 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<div class="module module--custom">
|
||||||
|
<h2>Analytics</h2>
|
||||||
|
<a class="display-block padding-y-1 padding-x-1" href="{% url 'analytics' %}">Dashboard</a>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,9 @@
|
||||||
>
|
>
|
||||||
<script src="{% static 'js/uswds-init.min.js' %}" defer></script>
|
<script src="{% static 'js/uswds-init.min.js' %}" defer></script>
|
||||||
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
||||||
|
<script type="application/javascript" src="{% static 'js/get-gov-reports.js' %}" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||||
|
|
|
@ -22,7 +22,7 @@ Load our custom filters to extract info from the django generated markup.
|
||||||
<th scope="col" class="action-checkbox-column" title="Toggle all">
|
<th scope="col" class="action-checkbox-column" title="Toggle all">
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<span>
|
<span>
|
||||||
<input type="checkbox" name="_selected_action" id="action-toggle">
|
<input type="checkbox" id="action-toggle">
|
||||||
<label for="action-toggle" class="usa-sr-only">Toggle all</label>
|
<label for="action-toggle" class="usa-sr-only">Toggle all</label>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
{% extends "admin/index.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div id="content-main">
|
|
||||||
{% include "admin/app_list.html" with app_list=app_list show_changelinks=True %}
|
|
||||||
<div class="custom-content module">
|
|
||||||
<h2>Reports</h2>
|
|
||||||
<h3>Domain growth report</h3>
|
|
||||||
|
|
||||||
{% comment %}
|
|
||||||
Inputs of type date suck for accessibility.
|
|
||||||
We'll need to replace those guys with a django form once we figure out how to hook one onto this page.
|
|
||||||
The challenge is in the path definition in urls. Itdoes NOT like admin/export_data/
|
|
||||||
|
|
||||||
See the commit "Review for ticket #999"
|
|
||||||
{% endcomment %}
|
|
||||||
|
|
||||||
<div class="display-flex flex-align-baseline flex-justify margin-y-1">
|
|
||||||
<div>
|
|
||||||
<label for="start">Start date:</label>
|
|
||||||
<input type="date" id="start" name="start" value="2018-07-22" min="2018-01-01" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="end">End date:</label>
|
|
||||||
<input type="date" id="end" name="end" value="2023-12-01" min="2023-12-01" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="exportLink" data-export-url="{% url 'admin_export_data' %}" type="button" class="button">Export</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
{% extends 'admin/change_form.html' %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block field_sets %}
|
||||||
|
{# Create an invisible <a> tag so that we can use a click event to toggle the modal. #}
|
||||||
|
<a id="invisible-ineligible-modal-toggler" class="display-none" href="#toggle-set-ineligible" aria-controls="toggle-set-ineligible" data-open-modal></a>
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block submit_buttons_bottom %}
|
||||||
|
{% comment %}
|
||||||
|
Modals behave very weirdly in django admin.
|
||||||
|
They tend to "strip out" any injected form elements, leaving only the main form.
|
||||||
|
In addition, USWDS handles modals by first destroying the element, then repopulating it toward the end of the page.
|
||||||
|
In effect, this means that the modal is not, and cannot, be surrounded by any form element at compile time.
|
||||||
|
|
||||||
|
The current workaround for this is to use javascript to inject a hidden input, and bind submit of that
|
||||||
|
element to the click of the confirmation button within this modal.
|
||||||
|
|
||||||
|
This is controlled by the class `dja-form-placeholder` on the button.
|
||||||
|
|
||||||
|
In addition, the modal element MUST be placed low in the DOM. The script loads slower on DJA than on other portions
|
||||||
|
of the application, so this means that it will briefly "populate", causing unintended visual effects.
|
||||||
|
{% endcomment %}
|
||||||
|
{# Create a modal for when a domain is marked as ineligible #}
|
||||||
|
<div
|
||||||
|
class="usa-modal"
|
||||||
|
id="toggle-set-ineligible"
|
||||||
|
aria-labelledby="Are you sure you want to select ineligible status?"
|
||||||
|
aria-describedby="This request will be marked as ineligible."
|
||||||
|
>
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||||
|
Are you sure you want to select ineligible status?
|
||||||
|
</h2>
|
||||||
|
<div class="usa-prose">
|
||||||
|
<p>
|
||||||
|
When a domain request is in ineligible status, the registrant's permissions within the registrar are restricted as follows:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li class="font-body-sm">They cannot edit the ineligible request or any other pending requests.</li>
|
||||||
|
<li class="font-body-sm">They cannot manage any of their approved domains.</li>
|
||||||
|
<li class="font-body-sm">They cannot initiate a new domain request.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
The restrictions will not take effect until you “save” the changes for this domain request.
|
||||||
|
This action can be reversed, if needed.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Domain: <b>{{ original.requested_domain.name }}</b>
|
||||||
|
{# Acts as a <br> #}
|
||||||
|
<div class="display-inline"></div>
|
||||||
|
New status: <b>{{ original.DomainRequestStatus.INELIGIBLE|capfirst }}</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="usa-button"
|
||||||
|
name="_set_domain_request_ineligible"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Yes, select ineligible status
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
|
name="_cancel_domain_request_ineligible"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-modal__close"
|
||||||
|
aria-label="Close this window"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
|
@ -11,18 +11,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop:flex-align-self-end">
|
<div class="desktop:flex-align-self-end">
|
||||||
{% if original.state != original.State.DELETED %}
|
{% if original.state != original.State.DELETED %}
|
||||||
<a
|
<a class="text-middle" href="#toggle-extend-expiration-alert" aria-controls="toggle-extend-expiration-alert" data-open-modal>
|
||||||
class="text-middle"
|
|
||||||
href="#toggle-extend-expiration-alert"
|
|
||||||
aria-controls="toggle-extend-expiration-alert"
|
|
||||||
data-open-modal
|
|
||||||
>
|
|
||||||
Extend expiration date
|
Extend expiration date
|
||||||
</a>
|
</a>
|
||||||
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if original.state == original.State.READY %}
|
{% if original.state == original.State.READY %}
|
||||||
<input type="submit" value="Place hold" name="_place_client_hold" class="custom-link-button">
|
<a class="text-middle" href="#toggle-place-on-hold" aria-controls="toggle-place-on-hold" data-open-modal>
|
||||||
|
Place hold
|
||||||
|
</a>
|
||||||
{% elif original.state == original.State.ON_HOLD %}
|
{% elif original.state == original.State.ON_HOLD %}
|
||||||
<input type="submit" value="Remove hold" name="_remove_client_hold" class="custom-link-button">
|
<input type="submit" value="Remove hold" name="_remove_client_hold" class="custom-link-button">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -30,7 +27,9 @@
|
||||||
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if original.state != original.State.DELETED %}
|
{% if original.state != original.State.DELETED %}
|
||||||
<input type="submit" value="Remove from registry" name="_delete_domain" class="custom-link-button">
|
<a class="text-middle" href="#toggle-remove-from-registry" aria-controls="toggle-remove-from-registry" data-open-modal>
|
||||||
|
Remove from registry
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,8 +51,10 @@
|
||||||
In addition, the modal element MUST be placed low in the DOM. The script loads slower on DJA than on other portions
|
In addition, the modal element MUST be placed low in the DOM. The script loads slower on DJA than on other portions
|
||||||
of the application, so this means that it will briefly "populate", causing unintended visual effects.
|
of the application, so this means that it will briefly "populate", causing unintended visual effects.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
|
{# Create a modal for the _extend_expiration_date button #}
|
||||||
<div
|
<div
|
||||||
class="usa-modal"
|
class="usa-modal usa-modal--django-admin"
|
||||||
id="toggle-extend-expiration-alert"
|
id="toggle-extend-expiration-alert"
|
||||||
aria-labelledby="Are you sure you want to extend the expiration date?"
|
aria-labelledby="Are you sure you want to extend the expiration date?"
|
||||||
aria-describedby="This expiration date will be extended."
|
aria-describedby="This expiration date will be extended."
|
||||||
|
@ -78,7 +79,7 @@
|
||||||
{{test}}
|
{{test}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="usa-modal__footer">
|
<div class="usa-modal__footer">
|
||||||
<ul class="usa-button-group">
|
<ul class="usa-button-group">
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
|
@ -114,5 +115,140 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Create a modal for the _on_hold button #}
|
||||||
|
<div
|
||||||
|
class="usa-modal usa-modal--django-admin"
|
||||||
|
id="toggle-place-on-hold"
|
||||||
|
aria-labelledby="Are you sure you want to place this domain on hold?"
|
||||||
|
aria-describedby="This domain will be put on hold"
|
||||||
|
>
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||||
|
Are you sure you want to place this domain on hold?
|
||||||
|
</h2>
|
||||||
|
<div class="usa-prose">
|
||||||
|
<p>
|
||||||
|
When a domain is on hold:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li class="font-body-sm">The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.</li>
|
||||||
|
<li class="font-body-sm">The domain will still appear in the registrar / admin.</li>
|
||||||
|
<li class="font-body-sm">Domain managers won’t be able to edit the domain.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
This action can be reversed, if needed.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Domain: <b>{{ original.name }}</b>
|
||||||
|
{# Acts as a <br> #}
|
||||||
|
<div class="display-inline"></div>
|
||||||
|
New status: <b>{{ original.State.ON_HOLD|capfirst }}</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="usa-button dja-form-placeholder"
|
||||||
|
name="_place_client_hold"
|
||||||
|
>
|
||||||
|
Yes, place hold
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-modal__close"
|
||||||
|
aria-label="Close this window"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{# Create a modal for the _remove_domain button #}
|
||||||
|
<div
|
||||||
|
class="usa-modal usa-modal--django-admin"
|
||||||
|
id="toggle-remove-from-registry"
|
||||||
|
aria-labelledby="Are you sure you want to remove this domain from the registry?"
|
||||||
|
aria-describedby="This domain will be removed."
|
||||||
|
>
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||||
|
Are you sure you want to remove this domain from the registry?
|
||||||
|
</h2>
|
||||||
|
<div class="usa-prose">
|
||||||
|
<p>
|
||||||
|
When a domain is removed from the registry:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li class="font-body-sm">The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.</li>
|
||||||
|
<li class="font-body-sm">The domain will still appear in the registrar / admin.</li>
|
||||||
|
<li class="font-body-sm">Domain managers won’t be able to edit the domain.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Domain: <b>{{ original.name }}</b>
|
||||||
|
{# Acts as a <br> #}
|
||||||
|
<div class="display-inline"></div>
|
||||||
|
New status: <b>{{ original.State.DELETED|capfirst }}</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="usa-button dja-form-placeholder"
|
||||||
|
name="_delete_domain"
|
||||||
|
>
|
||||||
|
Yes, remove from registry
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-modal__close"
|
||||||
|
aria-label="Close this window"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
{% extends "admin/change_list.html" %}
|
|
||||||
|
|
||||||
{% block object-tools %}
|
|
||||||
|
|
||||||
<ul class="object-tools">
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'admin:registrar_domain_export_data_type' %}" class="button">Export all domain metadata</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'admin:registrar_domain_export_data_full' %}" class="button">Export current-full.csv</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'admin:registrar_domain_export_data_federal' %}" class="button">Export current-federal.csv</a>
|
|
||||||
</li>
|
|
||||||
{% if has_add_permission %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'admin:registrar_domain_add' %}" class="addlink">
|
|
||||||
Add domain
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends 'admin/delete_confirmation.html' %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="usa-summary-box width-tablet"
|
||||||
|
role="region"
|
||||||
|
aria-labelledby="summary-box-description"
|
||||||
|
>
|
||||||
|
<div class="usa-summary-box__body">
|
||||||
|
<h3 class="usa-summary-box__heading usa-summary-box__dhs-color" id="summary-box-description">
|
||||||
|
When a domain is deleted:
|
||||||
|
</h3>
|
||||||
|
<div class="usa-summary-box__text">
|
||||||
|
<ul class="usa-list">
|
||||||
|
<li>The domain will no longer appear in the registrar / admin.</li>
|
||||||
|
<li>It will be removed from the registry. </li>
|
||||||
|
<li>The domain and its subdomains won’t resolve in DNS.</li>
|
||||||
|
<li>Any infrastructure (like websites) will go offline.</li>
|
||||||
|
</ul>
|
||||||
|
<p>You should probably remove this domain from the registry instead of deleting it.</p>
|
||||||
|
<p><strong>This action cannot be undone.</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends 'admin/delete_selected_confirmation.html' %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div
|
||||||
|
class="usa-summary-box width-tablet"
|
||||||
|
role="region"
|
||||||
|
aria-labelledby="summary-box-description"
|
||||||
|
>
|
||||||
|
<div class="usa-summary-box__body">
|
||||||
|
<h3 class="usa-summary-box__heading usa-summary-box__dhs-color" id="summary-box-description">
|
||||||
|
When a domain is deleted:
|
||||||
|
</h3>
|
||||||
|
<div class="usa-summary-box__text">
|
||||||
|
<ul class="usa-list">
|
||||||
|
<li>The domain will no longer appear in the registrar / admin.</li>
|
||||||
|
<li>It will be removed from the registry. </li>
|
||||||
|
<li>The domain and its subdomains won’t resolve in DNS.</li>
|
||||||
|
<li>Any infrastructure (like websites) will go offline.</li>
|
||||||
|
</ul>
|
||||||
|
<p>You should probably remove these domains from the registry instead.</p>
|
||||||
|
<p><strong>This action cannot be undone.</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
|
@ -11,12 +11,28 @@
|
||||||
|
|
||||||
<p>Your authorizing official is a person within your organization who can
|
<p>Your authorizing official is a person within your organization who can
|
||||||
authorize domain requests. This person must be in a role of significant, executive responsibility within the organization. Read more about <a class="usa-link" rel="noopener noreferrer" target="_blank" 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>
|
authorize domain requests. This person must be in a role of significant, executive responsibility within the organization. Read more about <a class="usa-link" rel="noopener noreferrer" target="_blank" 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>
|
||||||
|
|
||||||
|
{% if organization_type == "federal" or organization_type == "tribal" %}
|
||||||
|
<p>
|
||||||
|
The authorizing official for your organization can’t be updated here.
|
||||||
|
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% if organization_type == "federal" or organization_type == "tribal" %}
|
||||||
|
{# If all fields are disabled, add SR content #}
|
||||||
|
<div class="usa-sr-only" aria-labelledby="id_first_name" id="sr-ao-first-name">{{ form.first_name.value }}</div>
|
||||||
|
<div class="usa-sr-only" aria-labelledby="id_last_name" id="sr-ao-last-name">{{ form.last_name.value }}</div>
|
||||||
|
<div class="usa-sr-only" aria-labelledby="id_title" id="sr-ao-title">{{ form.title.value }}</div>
|
||||||
|
<div class="usa-sr-only" aria-labelledby="id_email" id="sr-ao-email">{{ form.email.value }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% input_with_errors form.first_name %}
|
{% input_with_errors form.first_name %}
|
||||||
|
|
||||||
{% input_with_errors form.last_name %}
|
{% input_with_errors form.last_name %}
|
||||||
|
@ -24,11 +40,9 @@
|
||||||
{% input_with_errors form.title %}
|
{% input_with_errors form.title %}
|
||||||
|
|
||||||
{% input_with_errors form.email %}
|
{% input_with_errors form.email %}
|
||||||
|
|
||||||
<button
|
{% if organization_type != "federal" and organization_type != "tribal" %}
|
||||||
type="submit"
|
<button type="submit" class="usa-button">Save</button>
|
||||||
class="usa-button"
|
{% endif %}
|
||||||
>Save</button>
|
</form>
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endblock %} {# domain_content #}
|
{% endblock %} {# domain_content #}
|
||||||
|
|
|
@ -11,6 +11,18 @@
|
||||||
|
|
||||||
<p>The name of your organization will be publicly listed as the domain registrant.</p>
|
<p>The name of your organization will be publicly listed as the domain registrant.</p>
|
||||||
|
|
||||||
|
{% if domain.domain_info.organization_type == "federal" %}
|
||||||
|
<p>
|
||||||
|
The federal agency for your organization can’t be updated here.
|
||||||
|
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
|
</p>
|
||||||
|
{% elif domain.domain_info.organization_type == "tribal" %}
|
||||||
|
<p>
|
||||||
|
Your organization name can’t be updated here.
|
||||||
|
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form_fields %}
|
{% block form_fields %}
|
||||||
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
|
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||||
{% input_with_errors forms.0.about_your_organization %}
|
{% input_with_errors forms.0.about_your_organization %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
|
|
||||||
{% block form_fields %}
|
{% block form_fields %}
|
||||||
{% with add_label_class="usa-sr-only" attr_maxlength=1000 %}
|
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||||
{% input_with_errors forms.0.anything_else %}
|
{% input_with_errors forms.0.anything_else %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form_fields %}
|
{% block form_fields %}
|
||||||
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
|
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||||
{% input_with_errors forms.0.purpose %}
|
{% input_with_errors forms.0.purpose %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
1
src/registrar/templates/emails/metadata_body.txt
Normal file
1
src/registrar/templates/emails/metadata_body.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
An export of all .gov metadata.
|
2
src/registrar/templates/emails/metadata_subject.txt
Normal file
2
src/registrar/templates/emails/metadata_subject.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Domain metadata - {{current_date_str}}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import datetime
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -13,6 +12,8 @@ from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
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 django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
Contact,
|
Contact,
|
||||||
|
@ -35,6 +36,7 @@ from epplibwrapper import (
|
||||||
ErrorCode,
|
ErrorCode,
|
||||||
responses,
|
responses,
|
||||||
)
|
)
|
||||||
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
|
||||||
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
|
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
|
||||||
|
|
||||||
|
@ -97,7 +99,7 @@ def less_console_noise(output_stream=None):
|
||||||
class GenericTestHelper(TestCase):
|
class GenericTestHelper(TestCase):
|
||||||
"""A helper class that contains various helper functions for TestCases"""
|
"""A helper class that contains various helper functions for TestCases"""
|
||||||
|
|
||||||
def __init__(self, admin, model=None, url=None, user=None, factory=None, **kwargs):
|
def __init__(self, admin, model=None, url=None, user=None, factory=None, client=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Parameters:
|
Parameters:
|
||||||
admin (ModelAdmin): The Django ModelAdmin instance associated with the model.
|
admin (ModelAdmin): The Django ModelAdmin instance associated with the model.
|
||||||
|
@ -112,6 +114,7 @@ class GenericTestHelper(TestCase):
|
||||||
self.admin = admin
|
self.admin = admin
|
||||||
self.model = model
|
self.model = model
|
||||||
self.url = url
|
self.url = url
|
||||||
|
self.client = client
|
||||||
|
|
||||||
def assert_table_sorted(self, o_index, sort_fields):
|
def assert_table_sorted(self, o_index, sort_fields):
|
||||||
"""
|
"""
|
||||||
|
@ -147,9 +150,7 @@ class GenericTestHelper(TestCase):
|
||||||
dummy_request.user = self.user
|
dummy_request.user = self.user
|
||||||
|
|
||||||
# Mock a user request
|
# Mock a user request
|
||||||
middleware = SessionMiddleware(lambda req: req)
|
dummy_request = self._mock_user_request_for_factory(dummy_request)
|
||||||
middleware.process_request(dummy_request)
|
|
||||||
dummy_request.session.save()
|
|
||||||
|
|
||||||
expected_sort_order = list(self.model.objects.order_by(*sort_fields))
|
expected_sort_order = list(self.model.objects.order_by(*sort_fields))
|
||||||
|
|
||||||
|
@ -160,6 +161,27 @@ class GenericTestHelper(TestCase):
|
||||||
|
|
||||||
self.assertEqual(expected_sort_order, returned_sort_order)
|
self.assertEqual(expected_sort_order, returned_sort_order)
|
||||||
|
|
||||||
|
def _mock_user_request_for_factory(self, request):
|
||||||
|
"""Adds sessionmiddleware when using factory to associate session information"""
|
||||||
|
middleware = SessionMiddleware(lambda req: req)
|
||||||
|
middleware.process_request(request)
|
||||||
|
request.session.save()
|
||||||
|
return request
|
||||||
|
|
||||||
|
def get_table_delete_confirmation_page(self, selected_across: str, index: str):
|
||||||
|
"""
|
||||||
|
Grabs the response for the delete confirmation page (generated from the actions toolbar).
|
||||||
|
selected_across and index must both be numbers encoded as str, e.g. "0" rather than 0
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
self.url,
|
||||||
|
{"action": "delete_selected", "select_across": selected_across, "index": index, "_selected_action": "23"},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
print(f"what is the response? {response}")
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class MockUserLogin:
|
class MockUserLogin:
|
||||||
def __init__(self, get_response):
|
def __init__(self, get_response):
|
||||||
|
@ -472,6 +494,184 @@ class AuditedAdminMockData:
|
||||||
return domain_request
|
return domain_request
|
||||||
|
|
||||||
|
|
||||||
|
class MockDb(TestCase):
|
||||||
|
"""Hardcoded mocks make test case assertions straightforward."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
username = "test_user"
|
||||||
|
first_name = "First"
|
||||||
|
last_name = "Last"
|
||||||
|
email = "info@example.com"
|
||||||
|
self.user = get_user_model().objects.create(
|
||||||
|
username=username, first_name=first_name, last_name=last_name, email=email
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a time-aware current date
|
||||||
|
current_datetime = timezone.now()
|
||||||
|
# Extract the date part
|
||||||
|
current_date = current_datetime.date()
|
||||||
|
# Create start and end dates using timedelta
|
||||||
|
self.end_date = current_date + timedelta(days=2)
|
||||||
|
self.start_date = current_date - timedelta(days=2)
|
||||||
|
|
||||||
|
self.domain_1, _ = Domain.objects.get_or_create(
|
||||||
|
name="cdomain1.gov", state=Domain.State.READY, first_ready=timezone.now()
|
||||||
|
)
|
||||||
|
self.domain_2, _ = Domain.objects.get_or_create(name="adomain2.gov", state=Domain.State.DNS_NEEDED)
|
||||||
|
self.domain_3, _ = Domain.objects.get_or_create(name="ddomain3.gov", state=Domain.State.ON_HOLD)
|
||||||
|
self.domain_4, _ = Domain.objects.get_or_create(name="bdomain4.gov", state=Domain.State.UNKNOWN)
|
||||||
|
self.domain_4, _ = Domain.objects.get_or_create(name="bdomain4.gov", state=Domain.State.UNKNOWN)
|
||||||
|
self.domain_5, _ = Domain.objects.get_or_create(
|
||||||
|
name="bdomain5.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(2023, 11, 1))
|
||||||
|
)
|
||||||
|
self.domain_6, _ = Domain.objects.get_or_create(
|
||||||
|
name="bdomain6.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(1980, 10, 16))
|
||||||
|
)
|
||||||
|
self.domain_7, _ = Domain.objects.get_or_create(
|
||||||
|
name="xdomain7.gov", state=Domain.State.DELETED, deleted=timezone.now()
|
||||||
|
)
|
||||||
|
self.domain_8, _ = Domain.objects.get_or_create(
|
||||||
|
name="sdomain8.gov", state=Domain.State.DELETED, deleted=timezone.now()
|
||||||
|
)
|
||||||
|
# We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today())
|
||||||
|
# and a specific time (using datetime.min.time()).
|
||||||
|
# Deleted yesterday
|
||||||
|
self.domain_9, _ = Domain.objects.get_or_create(
|
||||||
|
name="zdomain9.gov",
|
||||||
|
state=Domain.State.DELETED,
|
||||||
|
deleted=timezone.make_aware(datetime.combine(date.today() - timedelta(days=1), datetime.min.time())),
|
||||||
|
)
|
||||||
|
# ready tomorrow
|
||||||
|
self.domain_10, _ = Domain.objects.get_or_create(
|
||||||
|
name="adomain10.gov",
|
||||||
|
state=Domain.State.READY,
|
||||||
|
first_ready=timezone.make_aware(datetime.combine(date.today() + timedelta(days=1), datetime.min.time())),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.domain_information_1, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_1,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="World War I Centennial Commission",
|
||||||
|
federal_type="executive",
|
||||||
|
is_election_board=True,
|
||||||
|
)
|
||||||
|
self.domain_information_2, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user, domain=self.domain_2, organization_type="interstate", is_election_board=True
|
||||||
|
)
|
||||||
|
self.domain_information_3, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_3,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=True,
|
||||||
|
)
|
||||||
|
self.domain_information_4, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_4,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=True,
|
||||||
|
)
|
||||||
|
self.domain_information_5, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_5,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=False,
|
||||||
|
)
|
||||||
|
self.domain_information_6, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_6,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=False,
|
||||||
|
)
|
||||||
|
self.domain_information_7, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_7,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=False,
|
||||||
|
)
|
||||||
|
self.domain_information_8, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_8,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=False,
|
||||||
|
)
|
||||||
|
self.domain_information_9, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_9,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=False,
|
||||||
|
)
|
||||||
|
self.domain_information_10, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_10,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
meoward_user = get_user_model().objects.create(
|
||||||
|
username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
lebowski_user = get_user_model().objects.create(
|
||||||
|
username="big_lebowski", first_name="big", last_name="lebowski", email="big_lebowski@dude.co"
|
||||||
|
)
|
||||||
|
|
||||||
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
|
user=meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
|
user=lebowski_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
|
user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
self.domain_request_1 = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED, name="city1.gov"
|
||||||
|
)
|
||||||
|
self.domain_request_2 = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW, name="city2.gov"
|
||||||
|
)
|
||||||
|
self.domain_request_3 = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED, name="city3.gov"
|
||||||
|
)
|
||||||
|
self.domain_request_4 = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED, name="city4.gov"
|
||||||
|
)
|
||||||
|
self.domain_request_5 = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.APPROVED, name="city5.gov"
|
||||||
|
)
|
||||||
|
self.domain_request_3.submit()
|
||||||
|
self.domain_request_3.save()
|
||||||
|
self.domain_request_4.submit()
|
||||||
|
self.domain_request_4.save()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
PublicContact.objects.all().delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
UserDomainRole.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
def mock_user():
|
def mock_user():
|
||||||
"""A simple user."""
|
"""A simple user."""
|
||||||
user_kwargs = dict(
|
user_kwargs = dict(
|
||||||
|
@ -660,7 +860,7 @@ class MockEppLib(TestCase):
|
||||||
self,
|
self,
|
||||||
id,
|
id,
|
||||||
email,
|
email,
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
pw="thisisnotapassword",
|
pw="thisisnotapassword",
|
||||||
):
|
):
|
||||||
fake = info.InfoContactResultData(
|
fake = info.InfoContactResultData(
|
||||||
|
@ -698,82 +898,82 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
mockDataInfoDomain = fakedEppObject(
|
mockDataInfoDomain = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
hosts=["fake.host.com"],
|
hosts=["fake.host.com"],
|
||||||
statuses=[
|
statuses=[
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoDomainSubdomain = fakedEppObject(
|
mockDataInfoDomainSubdomain = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
hosts=["fake.meoward.gov"],
|
hosts=["fake.meoward.gov"],
|
||||||
statuses=[
|
statuses=[
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoDomainSubdomainAndIPAddress = fakedEppObject(
|
mockDataInfoDomainSubdomainAndIPAddress = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
hosts=["fake.meow.gov"],
|
hosts=["fake.meow.gov"],
|
||||||
statuses=[
|
statuses=[
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
addrs=[common.Ip(addr="2.0.0.8")],
|
addrs=[common.Ip(addr="2.0.0.8")],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoDomainNotSubdomainNoIP = fakedEppObject(
|
mockDataInfoDomainNotSubdomainNoIP = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
hosts=["fake.meow.com"],
|
hosts=["fake.meow.com"],
|
||||||
statuses=[
|
statuses=[
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoDomainSubdomainNoIP = fakedEppObject(
|
mockDataInfoDomainSubdomainNoIP = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
hosts=["fake.subdomainwoip.gov"],
|
hosts=["fake.subdomainwoip.gov"],
|
||||||
statuses=[
|
statuses=[
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataExtensionDomain = fakedEppObject(
|
mockDataExtensionDomain = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
hosts=["fake.host.com"],
|
hosts=["fake.host.com"],
|
||||||
statuses=[
|
statuses=[
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 11, 15),
|
ex_date=date(2023, 11, 15),
|
||||||
)
|
)
|
||||||
mockDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
|
mockDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
|
||||||
"123", "123@mail.gov", datetime.datetime(2023, 5, 25, 19, 45, 35), "lastPw"
|
"123", "123@mail.gov", datetime(2023, 5, 25, 19, 45, 35), "lastPw"
|
||||||
)
|
)
|
||||||
InfoDomainWithContacts = fakedEppObject(
|
InfoDomainWithContacts = fakedEppObject(
|
||||||
"fakepw",
|
"fakepw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[
|
contacts=[
|
||||||
common.DomainContact(
|
common.DomainContact(
|
||||||
contact="securityContact",
|
contact="securityContact",
|
||||||
|
@ -798,7 +998,7 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
InfoDomainWithDefaultSecurityContact = fakedEppObject(
|
InfoDomainWithDefaultSecurityContact = fakedEppObject(
|
||||||
"fakepw",
|
"fakepw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[
|
contacts=[
|
||||||
common.DomainContact(
|
common.DomainContact(
|
||||||
contact="defaultSec",
|
contact="defaultSec",
|
||||||
|
@ -813,11 +1013,11 @@ class MockEppLib(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
mockVerisignDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
|
mockVerisignDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
|
||||||
"defaultVeri", "registrar@dotgov.gov", datetime.datetime(2023, 5, 25, 19, 45, 35), "lastPw"
|
"defaultVeri", "registrar@dotgov.gov", datetime(2023, 5, 25, 19, 45, 35), "lastPw"
|
||||||
)
|
)
|
||||||
InfoDomainWithVerisignSecurityContact = fakedEppObject(
|
InfoDomainWithVerisignSecurityContact = fakedEppObject(
|
||||||
"fakepw",
|
"fakepw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[
|
contacts=[
|
||||||
common.DomainContact(
|
common.DomainContact(
|
||||||
contact="defaultVeri",
|
contact="defaultVeri",
|
||||||
|
@ -833,7 +1033,7 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
InfoDomainWithDefaultTechnicalContact = fakedEppObject(
|
InfoDomainWithDefaultTechnicalContact = fakedEppObject(
|
||||||
"fakepw",
|
"fakepw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[
|
contacts=[
|
||||||
common.DomainContact(
|
common.DomainContact(
|
||||||
contact="defaultTech",
|
contact="defaultTech",
|
||||||
|
@ -858,14 +1058,14 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
infoDomainNoContact = fakedEppObject(
|
infoDomainNoContact = fakedEppObject(
|
||||||
"security",
|
"security",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[],
|
contacts=[],
|
||||||
hosts=["fake.host.com"],
|
hosts=["fake.host.com"],
|
||||||
)
|
)
|
||||||
|
|
||||||
infoDomainThreeHosts = fakedEppObject(
|
infoDomainThreeHosts = fakedEppObject(
|
||||||
"my-nameserver.gov",
|
"my-nameserver.gov",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[],
|
contacts=[],
|
||||||
hosts=[
|
hosts=[
|
||||||
"ns1.my-nameserver-1.com",
|
"ns1.my-nameserver-1.com",
|
||||||
|
@ -876,43 +1076,43 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
infoDomainNoHost = fakedEppObject(
|
infoDomainNoHost = fakedEppObject(
|
||||||
"my-nameserver.gov",
|
"my-nameserver.gov",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[],
|
contacts=[],
|
||||||
hosts=[],
|
hosts=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
infoDomainTwoHosts = fakedEppObject(
|
infoDomainTwoHosts = fakedEppObject(
|
||||||
"my-nameserver.gov",
|
"my-nameserver.gov",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[],
|
contacts=[],
|
||||||
hosts=["ns1.my-nameserver-1.com", "ns1.my-nameserver-2.com"],
|
hosts=["ns1.my-nameserver-1.com", "ns1.my-nameserver-2.com"],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoHosts = fakedEppObject(
|
mockDataInfoHosts = fakedEppObject(
|
||||||
"lastPw",
|
"lastPw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 8, 25, 19, 45, 35)),
|
||||||
addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")],
|
addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoHosts1IP = fakedEppObject(
|
mockDataInfoHosts1IP = fakedEppObject(
|
||||||
"lastPw",
|
"lastPw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 8, 25, 19, 45, 35)),
|
||||||
addrs=[common.Ip(addr="2.0.0.8")],
|
addrs=[common.Ip(addr="2.0.0.8")],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoHostsNotSubdomainNoIP = fakedEppObject(
|
mockDataInfoHostsNotSubdomainNoIP = fakedEppObject(
|
||||||
"lastPw",
|
"lastPw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 8, 26, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 8, 26, 19, 45, 35)),
|
||||||
addrs=[],
|
addrs=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoHostsSubdomainNoIP = fakedEppObject(
|
mockDataInfoHostsSubdomainNoIP = fakedEppObject(
|
||||||
"lastPw",
|
"lastPw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 8, 27, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 8, 27, 19, 45, 35)),
|
||||||
addrs=[],
|
addrs=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)))
|
mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime(2023, 8, 25, 19, 45, 35)))
|
||||||
addDsData1 = {
|
addDsData1 = {
|
||||||
"keyTag": 1234,
|
"keyTag": 1234,
|
||||||
"alg": 3,
|
"alg": 3,
|
||||||
|
@ -944,7 +1144,7 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
infoDomainHasIP = fakedEppObject(
|
infoDomainHasIP = fakedEppObject(
|
||||||
"nameserverwithip.gov",
|
"nameserverwithip.gov",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[
|
contacts=[
|
||||||
common.DomainContact(
|
common.DomainContact(
|
||||||
contact="securityContact",
|
contact="securityContact",
|
||||||
|
@ -969,7 +1169,7 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
justNameserver = fakedEppObject(
|
justNameserver = fakedEppObject(
|
||||||
"justnameserver.com",
|
"justnameserver.com",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[
|
contacts=[
|
||||||
common.DomainContact(
|
common.DomainContact(
|
||||||
contact="securityContact",
|
contact="securityContact",
|
||||||
|
@ -992,7 +1192,7 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
infoDomainCheckHostIPCombo = fakedEppObject(
|
infoDomainCheckHostIPCombo = fakedEppObject(
|
||||||
"nameserversubdomain.gov",
|
"nameserversubdomain.gov",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[],
|
contacts=[],
|
||||||
hosts=[
|
hosts=[
|
||||||
"ns1.nameserversubdomain.gov",
|
"ns1.nameserversubdomain.gov",
|
||||||
|
@ -1002,27 +1202,27 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
mockRenewedDomainExpDate = fakedEppObject(
|
mockRenewedDomainExpDate = fakedEppObject(
|
||||||
"fake.gov",
|
"fake.gov",
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockButtonRenewedDomainExpDate = fakedEppObject(
|
mockButtonRenewedDomainExpDate = fakedEppObject(
|
||||||
"fake.gov",
|
"fake.gov",
|
||||||
ex_date=datetime.date(2025, 5, 25),
|
ex_date=date(2025, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDnsNeededRenewedDomainExpDate = fakedEppObject(
|
mockDnsNeededRenewedDomainExpDate = fakedEppObject(
|
||||||
"fakeneeded.gov",
|
"fakeneeded.gov",
|
||||||
ex_date=datetime.date(2023, 2, 15),
|
ex_date=date(2023, 2, 15),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockMaximumRenewedDomainExpDate = fakedEppObject(
|
mockMaximumRenewedDomainExpDate = fakedEppObject(
|
||||||
"fakemaximum.gov",
|
"fakemaximum.gov",
|
||||||
ex_date=datetime.date(2024, 12, 31),
|
ex_date=date(2024, 12, 31),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockRecentRenewedDomainExpDate = fakedEppObject(
|
mockRecentRenewedDomainExpDate = fakedEppObject(
|
||||||
"waterbutpurple.gov",
|
"waterbutpurple.gov",
|
||||||
ex_date=datetime.date(2024, 11, 15),
|
ex_date=date(2024, 11, 15),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _mockDomainName(self, _name, _avail=False):
|
def _mockDomainName(self, _name, _avail=False):
|
||||||
|
|
|
@ -61,6 +61,16 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.app.set_user(self.superuser.username)
|
self.app.set_user(self.superuser.username)
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
|
# Contains some test tools
|
||||||
|
self.test_helper = GenericTestHelper(
|
||||||
|
factory=self.factory,
|
||||||
|
user=self.superuser,
|
||||||
|
admin=self.admin,
|
||||||
|
url=reverse("admin:registrar_domain_changelist"),
|
||||||
|
model=Domain,
|
||||||
|
client=self.client,
|
||||||
|
)
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
@skip("TODO for another ticket. This test case is grabbing old db data.")
|
@skip("TODO for another ticket. This test case is grabbing old db data.")
|
||||||
|
@ -230,6 +240,35 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
||||||
)
|
)
|
||||||
mock_add_message.assert_has_calls([expected_call], 1)
|
mock_add_message.assert_has_calls([expected_call], 1)
|
||||||
|
|
||||||
|
def test_custom_delete_confirmation_page(self):
|
||||||
|
"""Tests if we override the delete confirmation page for custom content"""
|
||||||
|
# Create a ready domain with a preset expiration date
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||||
|
|
||||||
|
domain_change_page = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
|
||||||
|
|
||||||
|
self.assertContains(domain_change_page, "fake.gov")
|
||||||
|
# click the "Manage" link
|
||||||
|
confirmation_page = domain_change_page.click("Delete", index=0)
|
||||||
|
|
||||||
|
content_slice = "When a domain is deleted:"
|
||||||
|
self.assertContains(confirmation_page, content_slice)
|
||||||
|
|
||||||
|
def test_custom_delete_confirmation_page_table(self):
|
||||||
|
"""Tests if we override the delete confirmation page for custom content on the table"""
|
||||||
|
# Create a ready domain
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||||
|
|
||||||
|
# Get the index. The post expects the index to be encoded as a string
|
||||||
|
index = f"{domain.id}"
|
||||||
|
|
||||||
|
# Simulate selecting a single record, then clicking "Delete selected domains"
|
||||||
|
response = self.test_helper.get_table_delete_confirmation_page("0", index)
|
||||||
|
|
||||||
|
# Check that our content exists
|
||||||
|
content_slice = "When a domain is deleted:"
|
||||||
|
self.assertContains(response, content_slice)
|
||||||
|
|
||||||
def test_short_org_name_in_domains_list(self):
|
def test_short_org_name_in_domains_list(self):
|
||||||
"""
|
"""
|
||||||
Make sure the short name is displaying in admin on the list page
|
Make sure the short name is displaying in admin on the list page
|
||||||
|
@ -309,6 +348,17 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, domain.name)
|
self.assertContains(response, domain.name)
|
||||||
self.assertContains(response, "Remove from registry")
|
self.assertContains(response, "Remove from registry")
|
||||||
|
|
||||||
|
# The contents of the modal should exist before and after the post.
|
||||||
|
# Check for the header
|
||||||
|
self.assertContains(response, "Are you sure you want to remove this domain from the registry?")
|
||||||
|
|
||||||
|
# Check for some of its body
|
||||||
|
self.assertContains(response, "When a domain is removed from the registry:")
|
||||||
|
|
||||||
|
# Check for some of the button content
|
||||||
|
self.assertContains(response, "Yes, remove from registry")
|
||||||
|
|
||||||
# Test the info dialog
|
# Test the info dialog
|
||||||
request = self.factory.post(
|
request = self.factory.post(
|
||||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||||
|
@ -325,8 +375,60 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
||||||
extra_tags="",
|
extra_tags="",
|
||||||
fail_silently=False,
|
fail_silently=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# The modal should still exist
|
||||||
|
self.assertContains(response, "Are you sure you want to remove this domain from the registry?")
|
||||||
|
self.assertContains(response, "When a domain is removed from the registry:")
|
||||||
|
self.assertContains(response, "Yes, remove from registry")
|
||||||
|
|
||||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||||
|
|
||||||
|
def test_on_hold_is_successful_web_test(self):
|
||||||
|
"""
|
||||||
|
Scenario: Domain on_hold is successful through webtest
|
||||||
|
"""
|
||||||
|
with less_console_noise():
|
||||||
|
domain = create_ready_domain()
|
||||||
|
|
||||||
|
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
|
||||||
|
|
||||||
|
# Check the contents of the modal
|
||||||
|
# Check for the header
|
||||||
|
self.assertContains(response, "Are you sure you want to place this domain on hold?")
|
||||||
|
|
||||||
|
# Check for some of its body
|
||||||
|
self.assertContains(response, "When a domain is on hold:")
|
||||||
|
|
||||||
|
# Check for some of the button content
|
||||||
|
self.assertContains(response, "Yes, place hold")
|
||||||
|
|
||||||
|
# Grab the form to submit
|
||||||
|
form = response.forms["domain_form"]
|
||||||
|
|
||||||
|
# Submit the form
|
||||||
|
response = form.submit("_place_client_hold")
|
||||||
|
|
||||||
|
# Follow the response
|
||||||
|
response = response.follow()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Remove hold")
|
||||||
|
|
||||||
|
# The modal should still exist
|
||||||
|
# Check for the header
|
||||||
|
self.assertContains(response, "Are you sure you want to place this domain on hold?")
|
||||||
|
|
||||||
|
# Check for some of its body
|
||||||
|
self.assertContains(response, "When a domain is on hold:")
|
||||||
|
|
||||||
|
# Check for some of the button content
|
||||||
|
self.assertContains(response, "Yes, place hold")
|
||||||
|
|
||||||
|
# Web test has issues grabbing up to date data from the db, so we can test
|
||||||
|
# the returned view instead
|
||||||
|
self.assertContains(response, '<div class="readonly">On hold</div>')
|
||||||
|
|
||||||
def test_deletion_ready_fsm_failure(self):
|
def test_deletion_ready_fsm_failure(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Domain deletion is unsuccessful
|
Scenario: Domain deletion is unsuccessful
|
||||||
|
@ -1119,7 +1221,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||||
|
|
||||||
# Create a mock request
|
# Create a mock request
|
||||||
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
|
request = self.factory.post(
|
||||||
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), follow=True
|
||||||
|
)
|
||||||
|
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
# Modify the domain request's property
|
# Modify the domain request's property
|
||||||
|
@ -1131,6 +1235,64 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
# Test that approved domain exists and equals requested domain
|
# Test that approved domain exists and equals requested domain
|
||||||
self.assertEqual(domain_request.creator.status, "restricted")
|
self.assertEqual(domain_request.creator.status, "restricted")
|
||||||
|
|
||||||
|
def test_user_sets_restricted_status_modal(self):
|
||||||
|
"""Tests the modal for when a user sets the status to restricted"""
|
||||||
|
with less_console_noise():
|
||||||
|
# make sure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
# Create a sample domain request
|
||||||
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||||
|
|
||||||
|
p = "userpass"
|
||||||
|
self.client.login(username="staffuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain_request.requested_domain.name)
|
||||||
|
|
||||||
|
# Check that the modal has the right content
|
||||||
|
# Check for the header
|
||||||
|
self.assertContains(response, "Are you sure you want to select ineligible status?")
|
||||||
|
|
||||||
|
# Check for some of its body
|
||||||
|
self.assertContains(response, "When a domain request is in ineligible status")
|
||||||
|
|
||||||
|
# Check for some of the button content
|
||||||
|
self.assertContains(response, "Yes, select ineligible status")
|
||||||
|
|
||||||
|
# Create a mock request
|
||||||
|
request = self.factory.post(
|
||||||
|
"/admin/registrar/domainrequest{}/change/".format(domain_request.pk), follow=True
|
||||||
|
)
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
|
# Modify the domain request's property
|
||||||
|
domain_request.status = DomainRequest.DomainRequestStatus.INELIGIBLE
|
||||||
|
|
||||||
|
# Use the model admin's save_model method
|
||||||
|
self.admin.save_model(request, domain_request, form=None, change=True)
|
||||||
|
|
||||||
|
# Test that approved domain exists and equals requested domain
|
||||||
|
self.assertEqual(domain_request.creator.status, "restricted")
|
||||||
|
|
||||||
|
# 'Get' to the domain request again
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain_request.requested_domain.name)
|
||||||
|
|
||||||
|
# The modal should be unchanged
|
||||||
|
self.assertContains(response, "Are you sure you want to select ineligible status?")
|
||||||
|
self.assertContains(response, "When a domain request is in ineligible status")
|
||||||
|
self.assertContains(response, "Yes, select ineligible status")
|
||||||
|
|
||||||
def test_readonly_when_restricted_creator(self):
|
def test_readonly_when_restricted_creator(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.urls import reverse
|
||||||
from registrar.tests.common import create_superuser
|
from registrar.tests.common import create_superuser
|
||||||
|
|
||||||
|
|
||||||
class TestViews(TestCase):
|
class TestAdminViews(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
|
@ -26,7 +26,7 @@ class TestViews(TestCase):
|
||||||
|
|
||||||
# Construct the URL for the export data view with start_date and end_date parameters:
|
# Construct the URL for the export data view with start_date and end_date parameters:
|
||||||
# This stuff is currently done in JS
|
# This stuff is currently done in JS
|
||||||
export_data_url = reverse("admin_export_data") + f"?start_date={start_date}&end_date={end_date}"
|
export_data_url = reverse("export_domains_growth") + f"?start_date={start_date}&end_date={end_date}"
|
||||||
|
|
||||||
# Make a GET request to the export data page
|
# Make a GET request to the export data page
|
||||||
response = self.client.get(export_data_url)
|
response = self.client.get(export_data_url)
|
||||||
|
|
|
@ -5,7 +5,8 @@ from unittest.mock import MagicMock
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from .common import completed_domain_request, less_console_noise
|
from .common import completed_domain_request, less_console_noise
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from registrar.utility import email
|
||||||
import boto3_mocking # type: ignore
|
import boto3_mocking # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
@ -182,3 +183,32 @@ class TestEmails(TestCase):
|
||||||
self.assertNotIn("Anything else", body)
|
self.assertNotIn("Anything else", body)
|
||||||
# spacing should be right between adjacent elements
|
# spacing should be right between adjacent elements
|
||||||
self.assertRegex(body, r"5557\n\n----")
|
self.assertRegex(body, r"5557\n\n----")
|
||||||
|
|
||||||
|
@boto3_mocking.patching
|
||||||
|
def test_send_email_with_attachment(self):
|
||||||
|
with boto3_mocking.clients.handler_for("ses", self.mock_client_class):
|
||||||
|
sender_email = "sender@example.com"
|
||||||
|
recipient_email = "recipient@example.com"
|
||||||
|
subject = "Test Subject"
|
||||||
|
body = "Test Body"
|
||||||
|
attachment_file = b"Attachment file content"
|
||||||
|
current_date = datetime.now().strftime("%m%d%Y")
|
||||||
|
current_filename = f"domain-metadata-{current_date}.zip"
|
||||||
|
|
||||||
|
email.send_email_with_attachment(
|
||||||
|
sender_email, recipient_email, subject, body, attachment_file, self.mock_client
|
||||||
|
)
|
||||||
|
# Assert that the `send_raw_email` method of the mocked SES client was called with the expected params
|
||||||
|
self.mock_client.send_raw_email.assert_called_once()
|
||||||
|
|
||||||
|
# Get the args passed to the `send_raw_email` method
|
||||||
|
call_args = self.mock_client.send_raw_email.call_args[1]
|
||||||
|
|
||||||
|
# Assert that the attachment filename is correct
|
||||||
|
self.assertEqual(call_args["RawMessage"]["Data"].count(f'filename="{current_filename}"'), 1)
|
||||||
|
|
||||||
|
# Assert that the attachment content is encrypted
|
||||||
|
self.assertIn("Content-Type: application/octet-stream", call_args["RawMessage"]["Data"])
|
||||||
|
self.assertIn("Content-Transfer-Encoding: base64", call_args["RawMessage"]["Data"])
|
||||||
|
self.assertIn("Content-Disposition: attachment;", call_args["RawMessage"]["Data"])
|
||||||
|
self.assertNotIn("Attachment file content", call_args["RawMessage"]["Data"])
|
||||||
|
|
|
@ -226,7 +226,7 @@ class TestFormValidation(MockEppLib):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_purpose_form_character_count_invalid(self):
|
def test_purpose_form_character_count_invalid(self):
|
||||||
"""Response must be less than 1000 characters."""
|
"""Response must be less than 2000 characters."""
|
||||||
form = PurposeForm(
|
form = PurposeForm(
|
||||||
data={
|
data={
|
||||||
"purpose": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
"purpose": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||||
|
@ -247,15 +247,33 @@ class TestFormValidation(MockEppLib):
|
||||||
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
||||||
"beef ribs rump jowl tenderloin swine sausage biltong"
|
"beef ribs rump jowl tenderloin swine sausage biltong"
|
||||||
"bacon rump tail boudin meatball boudin meatball boudin."
|
"bacon rump tail boudin meatball boudin meatball boudin."
|
||||||
|
"Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||||
|
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
||||||
|
"Buffalo biltong chuck pork chop tongue bresaola turkey. Doner"
|
||||||
|
"ground round strip steak, jowl tail chuck ribeye bacon"
|
||||||
|
"beef ribs swine filet ball tip pancetta strip steak sirloin"
|
||||||
|
"mignon ham spare ribs rump. Tail shank biltong beef ribs doner"
|
||||||
|
"buffalo swine bacon. Tongue cow picanha brisket bacon chuck"
|
||||||
|
"leberkas pork loin pork, drumstick capicola. Doner short loin"
|
||||||
|
"ground round fatback turducken chislic shoulder turducken"
|
||||||
|
"spare ribs, burgdoggen kielbasa kevin frankfurter ball tip"
|
||||||
|
"pancetta cupim. Turkey meatball andouille porchetta hamburger"
|
||||||
|
"pork chop corned beef. Brisket short ribs turducken, pork chop"
|
||||||
|
"chislic turkey ball pork chop leberkas rump, rump bacon, jowl"
|
||||||
|
"tip ham. Shankle salami tongue venison short ribs kielbasa"
|
||||||
|
"tri-tip ham hock swine hamburger. Flank meatball corned beef"
|
||||||
|
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
||||||
|
"beef ribs rump jowl tenderloin swine sausage biltong"
|
||||||
|
"bacon rump tail boudin meatball boudin meatball boudin."
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
form.errors["purpose"],
|
form.errors["purpose"],
|
||||||
["Response must be less than 1000 characters."],
|
["Response must be less than 2000 characters."],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_anything_else_form_about_your_organization_character_count_invalid(self):
|
def test_anything_else_form_about_your_organization_character_count_invalid(self):
|
||||||
"""Response must be less than 1000 characters."""
|
"""Response must be less than 2000 characters."""
|
||||||
form = AnythingElseForm(
|
form = AnythingElseForm(
|
||||||
data={
|
data={
|
||||||
"anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
"anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||||
|
@ -276,15 +294,32 @@ class TestFormValidation(MockEppLib):
|
||||||
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
||||||
"beef ribs rump jowl tenderloin swine sausage biltong"
|
"beef ribs rump jowl tenderloin swine sausage biltong"
|
||||||
"bacon rump tail boudin meatball boudin meatball boudin."
|
"bacon rump tail boudin meatball boudin meatball boudin."
|
||||||
|
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
||||||
|
"Buffalo biltong chuck pork chop tongue bresaola turkey. Doner"
|
||||||
|
"ground round strip steak, jowl tail chuck ribeye bacon"
|
||||||
|
"beef ribs swine filet ball tip pancetta strip steak sirloin"
|
||||||
|
"mignon ham spare ribs rump. Tail shank biltong beef ribs doner"
|
||||||
|
"buffalo swine bacon. Tongue cow picanha brisket bacon chuck"
|
||||||
|
"leberkas pork loin pork, drumstick capicola. Doner short loin"
|
||||||
|
"ground round fatback turducken chislic shoulder turducken"
|
||||||
|
"spare ribs, burgdoggen kielbasa kevin frankfurter ball tip"
|
||||||
|
"pancetta cupim. Turkey meatball andouille porchetta hamburger"
|
||||||
|
"pork chop corned beef. Brisket short ribs turducken, pork chop"
|
||||||
|
"chislic turkey ball pork chop leberkas rump, rump bacon, jowl"
|
||||||
|
"tip ham. Shankle salami tongue venison short ribs kielbasa"
|
||||||
|
"tri-tip ham hock swine hamburger. Flank meatball corned beef"
|
||||||
|
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
||||||
|
"beef ribs rump jowl tenderloin swine sausage biltong"
|
||||||
|
"bacon rump tail boudin meatball boudin meatball boudin."
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
form.errors["anything_else"],
|
form.errors["anything_else"],
|
||||||
["Response must be less than 1000 characters."],
|
["Response must be less than 2000 characters."],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_anything_else_form_character_count_invalid(self):
|
def test_anything_else_form_character_count_invalid(self):
|
||||||
"""Response must be less than 1000 characters."""
|
"""Response must be less than 2000 characters."""
|
||||||
form = AboutYourOrganizationForm(
|
form = AboutYourOrganizationForm(
|
||||||
data={
|
data={
|
||||||
"about_your_organization": "Bacon ipsum dolor amet fatback"
|
"about_your_organization": "Bacon ipsum dolor amet fatback"
|
||||||
|
@ -306,11 +341,29 @@ class TestFormValidation(MockEppLib):
|
||||||
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
||||||
"beef ribs rump jowl tenderloin swine sausage biltong"
|
"beef ribs rump jowl tenderloin swine sausage biltong"
|
||||||
"bacon rump tail boudin meatball boudin meatball boudin."
|
"bacon rump tail boudin meatball boudin meatball boudin."
|
||||||
|
"strip steak pastrami"
|
||||||
|
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
||||||
|
"Buffalo biltong chuck pork chop tongue bresaola turkey. Doner"
|
||||||
|
"ground round strip steak, jowl tail chuck ribeye bacon"
|
||||||
|
"beef ribs swine filet ball tip pancetta strip steak sirloin"
|
||||||
|
"mignon ham spare ribs rump. Tail shank biltong beef ribs doner"
|
||||||
|
"buffalo swine bacon. Tongue cow picanha brisket bacon chuck"
|
||||||
|
"leberkas pork loin pork, drumstick capicola. Doner short loin"
|
||||||
|
"ground round fatback turducken chislic shoulder turducken"
|
||||||
|
"spare ribs, burgdoggen kielbasa kevin frankfurter ball tip"
|
||||||
|
"pancetta cupim. Turkey meatball andouille porchetta hamburger"
|
||||||
|
"pork chop corned beef. Brisket short ribs turducken, pork chop"
|
||||||
|
"chislic turkey ball pork chop leberkas rump, rump bacon, jowl"
|
||||||
|
"tip ham. Shankle salami tongue venison short ribs kielbasa"
|
||||||
|
"tri-tip ham hock swine hamburger. Flank meatball corned beef"
|
||||||
|
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
||||||
|
"beef ribs rump jowl tenderloin swine sausage biltong"
|
||||||
|
"bacon rump tail boudin meatball boudin meatball boudin."
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
form.errors["about_your_organization"],
|
form.errors["about_your_organization"],
|
||||||
["Response must be less than 1000 characters."],
|
["Response must be less than 2000 characters."],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_your_contact_email_invalid(self):
|
def test_your_contact_email_invalid(self):
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
from django.test import Client, RequestFactory, TestCase
|
from django.test import Client, RequestFactory
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from registrar.models.domain_information import DomainInformation
|
from registrar.models.domain_request import DomainRequest
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
from registrar.models.public_contact import PublicContact
|
|
||||||
from registrar.models.user import User
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
|
||||||
from registrar.tests.common import MockEppLib
|
|
||||||
from registrar.utility.csv_export import (
|
from registrar.utility.csv_export import (
|
||||||
write_csv,
|
export_data_managed_domains_to_csv,
|
||||||
|
export_data_unmanaged_domains_to_csv,
|
||||||
|
get_sliced_domains,
|
||||||
|
get_sliced_requests,
|
||||||
|
write_domains_csv,
|
||||||
get_default_start_date,
|
get_default_start_date,
|
||||||
get_default_end_date,
|
get_default_end_date,
|
||||||
|
write_requests_csv,
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
|
@ -22,62 +22,19 @@ from django.conf import settings
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
import boto3_mocking
|
import boto3_mocking
|
||||||
from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore
|
from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from .common import less_console_noise
|
from .common import MockDb, MockEppLib, less_console_noise
|
||||||
|
|
||||||
|
|
||||||
class CsvReportsTest(TestCase):
|
class CsvReportsTest(MockDb):
|
||||||
"""Tests to determine if we are uploading our reports correctly"""
|
"""Tests to determine if we are uploading our reports correctly"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Create fake domain data"""
|
"""Create fake domain data"""
|
||||||
|
super().setUp()
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
username = "test_user"
|
|
||||||
first_name = "First"
|
|
||||||
last_name = "Last"
|
|
||||||
email = "info@example.com"
|
|
||||||
self.user = get_user_model().objects.create(
|
|
||||||
username=username, first_name=first_name, last_name=last_name, email=email
|
|
||||||
)
|
|
||||||
|
|
||||||
self.domain_1, _ = Domain.objects.get_or_create(name="cdomain1.gov", state=Domain.State.READY)
|
|
||||||
self.domain_2, _ = Domain.objects.get_or_create(name="adomain2.gov", state=Domain.State.DNS_NEEDED)
|
|
||||||
self.domain_3, _ = Domain.objects.get_or_create(name="ddomain3.gov", state=Domain.State.ON_HOLD)
|
|
||||||
self.domain_4, _ = Domain.objects.get_or_create(name="bdomain4.gov", state=Domain.State.UNKNOWN)
|
|
||||||
|
|
||||||
self.domain_information_1, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_1,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="World War I Centennial Commission",
|
|
||||||
federal_type="executive",
|
|
||||||
)
|
|
||||||
self.domain_information_2, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_2,
|
|
||||||
organization_type="interstate",
|
|
||||||
)
|
|
||||||
self.domain_information_3, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_3,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_4, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_4,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
"""Delete all faked data"""
|
|
||||||
Domain.objects.all().delete()
|
|
||||||
DomainInformation.objects.all().delete()
|
|
||||||
User.objects.all().delete()
|
|
||||||
super().tearDown()
|
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_generate_federal_report(self):
|
def test_generate_federal_report(self):
|
||||||
|
@ -88,6 +45,7 @@ class CsvReportsTest(TestCase):
|
||||||
expected_file_content = [
|
expected_file_content = [
|
||||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||||
|
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||||
]
|
]
|
||||||
# We don't actually want to write anything for a test case,
|
# We don't actually want to write anything for a test case,
|
||||||
|
@ -108,6 +66,7 @@ class CsvReportsTest(TestCase):
|
||||||
expected_file_content = [
|
expected_file_content = [
|
||||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||||
|
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||||
call("adomain2.gov,Interstate,,,,, \r\n"),
|
call("adomain2.gov,Interstate,,,,, \r\n"),
|
||||||
]
|
]
|
||||||
|
@ -166,6 +125,7 @@ class CsvReportsTest(TestCase):
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_load_federal_report(self):
|
def test_load_federal_report(self):
|
||||||
"""Tests the get_current_federal api endpoint"""
|
"""Tests the get_current_federal api endpoint"""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_instance = mock_client.return_value
|
mock_client_instance = mock_client.return_value
|
||||||
|
@ -199,6 +159,7 @@ class CsvReportsTest(TestCase):
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_load_full_report(self):
|
def test_load_full_report(self):
|
||||||
"""Tests the current-federal api link"""
|
"""Tests the current-federal api link"""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_instance = mock_client.return_value
|
mock_client_instance = mock_client.return_value
|
||||||
|
@ -231,141 +192,17 @@ class CsvReportsTest(TestCase):
|
||||||
self.assertEqual(expected_file_content, response.content)
|
self.assertEqual(expected_file_content, response.content)
|
||||||
|
|
||||||
|
|
||||||
class ExportDataTest(MockEppLib):
|
class ExportDataTest(MockDb, MockEppLib):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
username = "test_user"
|
|
||||||
first_name = "First"
|
|
||||||
last_name = "Last"
|
|
||||||
email = "info@example.com"
|
|
||||||
self.user = get_user_model().objects.create(
|
|
||||||
username=username, first_name=first_name, last_name=last_name, email=email
|
|
||||||
)
|
|
||||||
|
|
||||||
self.domain_1, _ = Domain.objects.get_or_create(
|
|
||||||
name="cdomain1.gov", state=Domain.State.READY, first_ready=timezone.now()
|
|
||||||
)
|
|
||||||
self.domain_2, _ = Domain.objects.get_or_create(name="adomain2.gov", state=Domain.State.DNS_NEEDED)
|
|
||||||
self.domain_3, _ = Domain.objects.get_or_create(name="ddomain3.gov", state=Domain.State.ON_HOLD)
|
|
||||||
self.domain_4, _ = Domain.objects.get_or_create(name="bdomain4.gov", state=Domain.State.UNKNOWN)
|
|
||||||
self.domain_4, _ = Domain.objects.get_or_create(name="bdomain4.gov", state=Domain.State.UNKNOWN)
|
|
||||||
self.domain_5, _ = Domain.objects.get_or_create(
|
|
||||||
name="bdomain5.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(2023, 11, 1))
|
|
||||||
)
|
|
||||||
self.domain_6, _ = Domain.objects.get_or_create(
|
|
||||||
name="bdomain6.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(1980, 10, 16))
|
|
||||||
)
|
|
||||||
self.domain_7, _ = Domain.objects.get_or_create(
|
|
||||||
name="xdomain7.gov", state=Domain.State.DELETED, deleted=timezone.now()
|
|
||||||
)
|
|
||||||
self.domain_8, _ = Domain.objects.get_or_create(
|
|
||||||
name="sdomain8.gov", state=Domain.State.DELETED, deleted=timezone.now()
|
|
||||||
)
|
|
||||||
# We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today())
|
|
||||||
# and a specific time (using datetime.min.time()).
|
|
||||||
# Deleted yesterday
|
|
||||||
self.domain_9, _ = Domain.objects.get_or_create(
|
|
||||||
name="zdomain9.gov",
|
|
||||||
state=Domain.State.DELETED,
|
|
||||||
deleted=timezone.make_aware(datetime.combine(date.today() - timedelta(days=1), datetime.min.time())),
|
|
||||||
)
|
|
||||||
# ready tomorrow
|
|
||||||
self.domain_10, _ = Domain.objects.get_or_create(
|
|
||||||
name="adomain10.gov",
|
|
||||||
state=Domain.State.READY,
|
|
||||||
first_ready=timezone.make_aware(datetime.combine(date.today() + timedelta(days=1), datetime.min.time())),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.domain_information_1, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_1,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="World War I Centennial Commission",
|
|
||||||
federal_type="executive",
|
|
||||||
)
|
|
||||||
self.domain_information_2, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_2,
|
|
||||||
organization_type="interstate",
|
|
||||||
)
|
|
||||||
self.domain_information_3, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_3,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_4, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_4,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_5, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_5,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_6, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_6,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_7, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_7,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_8, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_8,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_9, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_9,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_10, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_10,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
|
|
||||||
meoward_user = get_user_model().objects.create(
|
|
||||||
username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test for more than 1 domain manager
|
|
||||||
_, created = UserDomainRole.objects.get_or_create(
|
|
||||||
user=meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
|
|
||||||
)
|
|
||||||
|
|
||||||
_, created = UserDomainRole.objects.get_or_create(
|
|
||||||
user=self.user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test for just 1 domain manager
|
|
||||||
_, created = UserDomainRole.objects.get_or_create(
|
|
||||||
user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER
|
|
||||||
)
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
PublicContact.objects.all().delete()
|
|
||||||
Domain.objects.all().delete()
|
|
||||||
DomainInformation.objects.all().delete()
|
|
||||||
User.objects.all().delete()
|
|
||||||
UserDomainRole.objects.all().delete()
|
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
def test_export_domains_to_writer_security_emails(self):
|
def test_export_domains_to_writer_security_emails(self):
|
||||||
"""Test that export_domains_to_writer returns the
|
"""Test that export_domains_to_writer returns the
|
||||||
expected security email"""
|
expected security email"""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# Add security email information
|
# Add security email information
|
||||||
self.domain_1.name = "defaultsecurity.gov"
|
self.domain_1.name = "defaultsecurity.gov"
|
||||||
|
@ -403,7 +240,7 @@ class ExportDataTest(MockEppLib):
|
||||||
}
|
}
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv(
|
write_domains_csv(
|
||||||
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -427,10 +264,11 @@ class ExportDataTest(MockEppLib):
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
def test_write_csv(self):
|
def test_write_domains_csv(self):
|
||||||
"""Test that write_body returns the
|
"""Test that write_body returns the
|
||||||
existing domain, test that sort by domain name works,
|
existing domain, test that sort by domain name works,
|
||||||
test that filter works"""
|
test that filter works"""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# Create a CSV file in memory
|
# Create a CSV file in memory
|
||||||
csv_file = StringIO()
|
csv_file = StringIO()
|
||||||
|
@ -462,7 +300,7 @@ class ExportDataTest(MockEppLib):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv(
|
write_domains_csv(
|
||||||
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||||
)
|
)
|
||||||
# Reset the CSV file's position to the beginning
|
# Reset the CSV file's position to the beginning
|
||||||
|
@ -486,8 +324,9 @@ class ExportDataTest(MockEppLib):
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
def test_write_body_additional(self):
|
def test_write_domains_body_additional(self):
|
||||||
"""An additional test for filters and multi-column sort"""
|
"""An additional test for filters and multi-column sort"""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# Create a CSV file in memory
|
# Create a CSV file in memory
|
||||||
csv_file = StringIO()
|
csv_file = StringIO()
|
||||||
|
@ -512,7 +351,7 @@ class ExportDataTest(MockEppLib):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv(
|
write_domains_csv(
|
||||||
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||||
)
|
)
|
||||||
# Reset the CSV file's position to the beginning
|
# Reset the CSV file's position to the beginning
|
||||||
|
@ -535,27 +374,23 @@ class ExportDataTest(MockEppLib):
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
def test_write_body_with_date_filter_pulls_domains_in_range(self):
|
def test_write_domains_body_with_date_filter_pulls_domains_in_range(self):
|
||||||
"""Test that domains that are
|
"""Test that domains that are
|
||||||
1. READY and their first_ready dates are in range
|
1. READY and their first_ready dates are in range
|
||||||
2. DELETED and their deleted dates are in range
|
2. DELETED and their deleted dates are in range
|
||||||
are pulled when the growth report conditions are applied to export_domains_to_writed.
|
are pulled when the growth report conditions are applied to export_domains_to_writed.
|
||||||
Test that ready domains are sorted by first_ready/deleted dates first, names second.
|
Test that ready domains are sorted by first_ready/deleted dates first, names second.
|
||||||
|
|
||||||
We considered testing export_data_growth_to_csv which calls write_body
|
We considered testing export_data_domain_growth_to_csv which calls write_body
|
||||||
and would have been easy to set up, but expected_content would contain created_at dates
|
and would have been easy to set up, but expected_content would contain created_at dates
|
||||||
which are hard to mock.
|
which are hard to mock.
|
||||||
|
|
||||||
TODO: Simplify is created_at is not needed for the report."""
|
TODO: Simplify if created_at is not needed for the report."""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# Create a CSV file in memory
|
# Create a CSV file in memory
|
||||||
csv_file = StringIO()
|
csv_file = StringIO()
|
||||||
writer = csv.writer(csv_file)
|
writer = csv.writer(csv_file)
|
||||||
# We use timezone.make_aware to sync to server time a datetime object with the current date
|
|
||||||
# (using date.today()) and a specific time (using datetime.min.time()).
|
|
||||||
end_date = timezone.make_aware(datetime.combine(date.today() + timedelta(days=2), datetime.min.time()))
|
|
||||||
start_date = timezone.make_aware(datetime.combine(date.today() - timedelta(days=2), datetime.min.time()))
|
|
||||||
|
|
||||||
# Define columns, sort fields, and filter condition
|
# Define columns, sort fields, and filter condition
|
||||||
columns = [
|
columns = [
|
||||||
"Domain name",
|
"Domain name",
|
||||||
|
@ -579,19 +414,19 @@ class ExportDataTest(MockEppLib):
|
||||||
"domain__state__in": [
|
"domain__state__in": [
|
||||||
Domain.State.READY,
|
Domain.State.READY,
|
||||||
],
|
],
|
||||||
"domain__first_ready__lte": end_date,
|
"domain__first_ready__lte": self.end_date,
|
||||||
"domain__first_ready__gte": start_date,
|
"domain__first_ready__gte": self.start_date,
|
||||||
}
|
}
|
||||||
filter_conditions_for_deleted_domains = {
|
filter_conditions_for_deleted_domains = {
|
||||||
"domain__state__in": [
|
"domain__state__in": [
|
||||||
Domain.State.DELETED,
|
Domain.State.DELETED,
|
||||||
],
|
],
|
||||||
"domain__deleted__lte": end_date,
|
"domain__deleted__lte": self.end_date,
|
||||||
"domain__deleted__gte": start_date,
|
"domain__deleted__gte": self.start_date,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv(
|
write_domains_csv(
|
||||||
writer,
|
writer,
|
||||||
columns,
|
columns,
|
||||||
sort_fields,
|
sort_fields,
|
||||||
|
@ -599,7 +434,7 @@ class ExportDataTest(MockEppLib):
|
||||||
get_domain_managers=False,
|
get_domain_managers=False,
|
||||||
should_write_header=True,
|
should_write_header=True,
|
||||||
)
|
)
|
||||||
write_csv(
|
write_domains_csv(
|
||||||
writer,
|
writer,
|
||||||
columns,
|
columns,
|
||||||
sort_fields_for_deleted_domains,
|
sort_fields_for_deleted_domains,
|
||||||
|
@ -634,13 +469,13 @@ class ExportDataTest(MockEppLib):
|
||||||
|
|
||||||
def test_export_domains_to_writer_domain_managers(self):
|
def test_export_domains_to_writer_domain_managers(self):
|
||||||
"""Test that export_domains_to_writer returns the
|
"""Test that export_domains_to_writer returns the
|
||||||
expected domain managers"""
|
expected domain managers."""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# Create a CSV file in memory
|
# Create a CSV file in memory
|
||||||
csv_file = StringIO()
|
csv_file = StringIO()
|
||||||
writer = csv.writer(csv_file)
|
writer = csv.writer(csv_file)
|
||||||
# Define columns, sort fields, and filter condition
|
# Define columns, sort fields, and filter condition
|
||||||
|
|
||||||
columns = [
|
columns = [
|
||||||
"Domain name",
|
"Domain name",
|
||||||
"Status",
|
"Status",
|
||||||
|
@ -664,7 +499,7 @@ class ExportDataTest(MockEppLib):
|
||||||
}
|
}
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv(
|
write_domains_csv(
|
||||||
writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True
|
writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -677,11 +512,11 @@ class ExportDataTest(MockEppLib):
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Status,Expiration date,Domain type,Agency,"
|
"Domain name,Status,Expiration date,Domain type,Agency,"
|
||||||
"Organization name,City,State,AO,AO email,"
|
"Organization name,City,State,AO,AO email,"
|
||||||
"Security contact email,Domain manager email 1,Domain manager email 2,\n"
|
"Security contact email,Domain manager email 1,Domain manager email 2,Domain manager email 3\n"
|
||||||
"adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,\n"
|
"adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,\n"
|
||||||
"adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com\n"
|
"adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com\n"
|
||||||
"cdomain1.gov,Ready,,Federal - Executive,World War I Centennial Commission,,,"
|
"cdomain1.gov,Ready,,Federal - Executive,World War I Centennial Commission,,,"
|
||||||
", , , ,meoward@rocks.com,info@example.com\n"
|
", , , ,meoward@rocks.com,info@example.com,big_lebowski@dude.co\n"
|
||||||
"ddomain3.gov,On hold,,Federal,Armed Forces Retirement Home,,,, , , ,,\n"
|
"ddomain3.gov,On hold,,Federal,Armed Forces Retirement Home,,,, , , ,,\n"
|
||||||
)
|
)
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
|
@ -690,8 +525,132 @@ class ExportDataTest(MockEppLib):
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
|
def test_export_data_managed_domains_to_csv(self):
|
||||||
|
"""Test get counts for domains that have domain managers for two different dates,
|
||||||
|
get list of managed domains at end_date."""
|
||||||
|
|
||||||
class HelperFunctions(TestCase):
|
with less_console_noise():
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
export_data_managed_domains_to_csv(
|
||||||
|
csv_file, self.start_date.strftime("%Y-%m-%d"), self.end_date.strftime("%Y-%m-%d")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reset the CSV file's position to the beginning
|
||||||
|
csv_file.seek(0)
|
||||||
|
# Read the content into a variable
|
||||||
|
csv_content = csv_file.read()
|
||||||
|
self.maxDiff = None
|
||||||
|
# We expect the READY domain names with the domain managers: Their counts, and listing at end_date.
|
||||||
|
expected_content = (
|
||||||
|
"MANAGED DOMAINS COUNTS AT START DATE\n"
|
||||||
|
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||||
|
"School district,Election office\n"
|
||||||
|
"0,0,0,0,0,0,0,0,0,0\n"
|
||||||
|
"\n"
|
||||||
|
"MANAGED DOMAINS COUNTS AT END DATE\n"
|
||||||
|
"Total,Federal,Interstate,State or territory,Tribal,County,City,"
|
||||||
|
"Special district,School district,Election office\n"
|
||||||
|
"1,1,0,0,0,0,0,0,0,1\n"
|
||||||
|
"\n"
|
||||||
|
"Domain name,Domain type,Domain manager email 1,Domain manager email 2,Domain manager email 3\n"
|
||||||
|
"cdomain1.gov,Federal - Executive,meoward@rocks.com,info@example.com,big_lebowski@dude.co\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Normalize line endings and remove commas,
|
||||||
|
# spaces and leading/trailing whitespace
|
||||||
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
|
|
||||||
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
|
def test_export_data_unmanaged_domains_to_csv(self):
|
||||||
|
"""Test get counts for domains that do not have domain managers for two different dates,
|
||||||
|
get list of unmanaged domains at end_date."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
export_data_unmanaged_domains_to_csv(
|
||||||
|
csv_file, self.start_date.strftime("%Y-%m-%d"), self.end_date.strftime("%Y-%m-%d")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reset the CSV file's position to the beginning
|
||||||
|
csv_file.seek(0)
|
||||||
|
# Read the content into a variable
|
||||||
|
csv_content = csv_file.read()
|
||||||
|
self.maxDiff = None
|
||||||
|
# We expect the READY domain names with the domain managers: Their counts, and listing at end_date.
|
||||||
|
expected_content = (
|
||||||
|
"UNMANAGED DOMAINS AT START DATE\n"
|
||||||
|
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||||
|
"School district,Election office\n"
|
||||||
|
"0,0,0,0,0,0,0,0,0,0\n"
|
||||||
|
"\n"
|
||||||
|
"UNMANAGED DOMAINS AT END DATE\n"
|
||||||
|
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||||
|
"School district,Election office\n"
|
||||||
|
"1,1,0,0,0,0,0,0,0,0\n"
|
||||||
|
"\n"
|
||||||
|
"Domain name,Domain type\n"
|
||||||
|
"adomain10.gov,Federal\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Normalize line endings and remove commas,
|
||||||
|
# spaces and leading/trailing whitespace
|
||||||
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
|
|
||||||
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
|
def test_write_requests_body_with_date_filter_pulls_requests_in_range(self):
|
||||||
|
"""Test that requests that are
|
||||||
|
1. SUBMITTED and their submission_date are in range
|
||||||
|
are pulled when the growth report conditions are applied to export_requests_to_writed.
|
||||||
|
Test that requests are sorted by requested domain name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
# Define columns, sort fields, and filter condition
|
||||||
|
# We'll skip submission date because it's dynamic and therefore
|
||||||
|
# impossible to set in expected_content
|
||||||
|
columns = [
|
||||||
|
"Requested domain",
|
||||||
|
"Organization type",
|
||||||
|
]
|
||||||
|
sort_fields = [
|
||||||
|
"requested_domain__name",
|
||||||
|
]
|
||||||
|
filter_condition = {
|
||||||
|
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
|
"submission_date__lte": self.end_date,
|
||||||
|
"submission_date__gte": self.start_date,
|
||||||
|
}
|
||||||
|
write_requests_csv(writer, columns, sort_fields, filter_condition, should_write_header=True)
|
||||||
|
# Reset the CSV file's position to the beginning
|
||||||
|
csv_file.seek(0)
|
||||||
|
# Read the content into a variable
|
||||||
|
csv_content = csv_file.read()
|
||||||
|
# We expect READY domains first, created between today-2 and today+2, sorted by created_at then name
|
||||||
|
# and DELETED domains deleted between today-2 and today+2, sorted by deleted then name
|
||||||
|
expected_content = (
|
||||||
|
"Requested domain,Organization type\n"
|
||||||
|
"city3.gov,Federal - Executive\n"
|
||||||
|
"city4.gov,Federal - Executive\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Normalize line endings and remove commas,
|
||||||
|
# spaces and leading/trailing whitespace
|
||||||
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
|
|
||||||
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
|
|
||||||
|
class HelperFunctions(MockDb):
|
||||||
"""This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""
|
"""This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""
|
||||||
|
|
||||||
def test_get_default_start_date(self):
|
def test_get_default_start_date(self):
|
||||||
|
@ -704,3 +663,33 @@ class HelperFunctions(TestCase):
|
||||||
expected_date = timezone.now()
|
expected_date = timezone.now()
|
||||||
actual_date = get_default_end_date()
|
actual_date = get_default_end_date()
|
||||||
self.assertEqual(actual_date.date(), expected_date.date())
|
self.assertEqual(actual_date.date(), expected_date.date())
|
||||||
|
|
||||||
|
def test_get_sliced_domains(self):
|
||||||
|
"""Should get fitered domains counts sliced by org type and election office."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
filter_condition = {
|
||||||
|
"domain__permissions__isnull": False,
|
||||||
|
"domain__first_ready__lte": self.end_date,
|
||||||
|
}
|
||||||
|
# Test with distinct
|
||||||
|
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition, True)
|
||||||
|
expected_content = [1, 1, 0, 0, 0, 0, 0, 0, 0, 1]
|
||||||
|
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
||||||
|
|
||||||
|
# Test without distinct
|
||||||
|
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
|
||||||
|
expected_content = [1, 3, 0, 0, 0, 0, 0, 0, 0, 1]
|
||||||
|
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
||||||
|
|
||||||
|
def test_get_sliced_requests(self):
|
||||||
|
"""Should get fitered requests counts sliced by org type and election office."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
filter_condition = {
|
||||||
|
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
|
"submission_date__lte": self.end_date,
|
||||||
|
}
|
||||||
|
submitted_requests_sliced_at_end_date = get_sliced_requests(filter_condition)
|
||||||
|
expected_content = [2, 2, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||||
|
self.assertEqual(submitted_requests_sliced_at_end_date, expected_content)
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
from django.test import Client, TestCase, override_settings
|
from django.test import Client, TestCase, override_settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from .common import MockEppLib # type: ignore
|
from api.tests.common import less_console_noise_decorator
|
||||||
|
from registrar.models.domain import Domain
|
||||||
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
from registrar.views.domain import DomainNameserversView
|
||||||
|
|
||||||
|
from .common import MockEppLib # type: ignore
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
DomainRequest,
|
DomainRequest,
|
||||||
|
@ -66,6 +72,7 @@ class TestEnvironmentVariablesEffects(TestCase):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
Domain.objects.all().delete()
|
||||||
self.user.delete()
|
self.user.delete()
|
||||||
|
|
||||||
@override_settings(IS_PRODUCTION=True)
|
@override_settings(IS_PRODUCTION=True)
|
||||||
|
@ -79,3 +86,52 @@ class TestEnvironmentVariablesEffects(TestCase):
|
||||||
"""Banner on non-prod."""
|
"""Banner on non-prod."""
|
||||||
home_page = self.client.get("/")
|
home_page = self.client.get("/")
|
||||||
self.assertContains(home_page, "You are on a test site.")
|
self.assertContains(home_page, "You are on a test site.")
|
||||||
|
|
||||||
|
def side_effect_raise_value_error(self):
|
||||||
|
"""Side effect that raises a 500 error"""
|
||||||
|
raise ValueError("Some error")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_settings(IS_PRODUCTION=False)
|
||||||
|
def test_non_production_environment_raises_500_and_shows_banner(self):
|
||||||
|
"""Tests if the non-prod banner is still shown on a 500"""
|
||||||
|
fake_domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
|
||||||
|
# Add a role
|
||||||
|
fake_role, _ = UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(DomainNameserversView, "get_initial", side_effect=self.side_effect_raise_value_error):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
contact_page_500 = self.client.get(
|
||||||
|
reverse("domain-dns-nameservers", kwargs={"pk": fake_domain.id}),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that a 500 response is returned
|
||||||
|
self.assertEqual(contact_page_500.status_code, 500)
|
||||||
|
|
||||||
|
self.assertContains(contact_page_500, "You are on a test site.")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_settings(IS_PRODUCTION=True)
|
||||||
|
def test_production_environment_raises_500_and_doesnt_show_banner(self):
|
||||||
|
"""Test if the non-prod banner is not shown on production when a 500 is raised"""
|
||||||
|
|
||||||
|
fake_domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
|
||||||
|
# Add a role
|
||||||
|
fake_role, _ = UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(DomainNameserversView, "get_initial", side_effect=self.side_effect_raise_value_error):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
contact_page_500 = self.client.get(
|
||||||
|
reverse("domain-dns-nameservers", kwargs={"pk": fake_domain.id}),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that a 500 response is returned
|
||||||
|
self.assertEqual(contact_page_500.status_code, 500)
|
||||||
|
|
||||||
|
self.assertNotContains(contact_page_500, "You are on a test site.")
|
||||||
|
|
|
@ -1021,6 +1021,144 @@ class TestDomainAuthorizingOfficial(TestDomainOverview):
|
||||||
self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name)
|
self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name)
|
||||||
self.assertEqual(ao_pk, self.domain_information.authorizing_official.id)
|
self.assertEqual(ao_pk, self.domain_information.authorizing_official.id)
|
||||||
|
|
||||||
|
def assert_all_form_fields_have_expected_values(self, form, test_cases, test_for_disabled=False):
|
||||||
|
"""
|
||||||
|
Asserts that each specified form field has the expected value and, optionally, checks if the field is disabled.
|
||||||
|
|
||||||
|
This method iterates over a list of tuples, where each
|
||||||
|
tuple contains a field name and the expected value for that field.
|
||||||
|
It uses subtests to isolate each assertion, allowing multiple field
|
||||||
|
checks within a single test method without stopping at the first failure.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
test_cases = [
|
||||||
|
("first_name", "John"),
|
||||||
|
("last_name", "Doe"),
|
||||||
|
("email", "john.doe@example.com"),
|
||||||
|
]
|
||||||
|
self.assert_all_form_fields_have_expected_values(my_form, test_cases, test_for_disabled=True)
|
||||||
|
"""
|
||||||
|
for field_name, expected_value in test_cases:
|
||||||
|
with self.subTest(field_name=field_name, expected_value=expected_value):
|
||||||
|
# Test that each field has the value we expect
|
||||||
|
self.assertEqual(expected_value, form[field_name].value)
|
||||||
|
|
||||||
|
if test_for_disabled:
|
||||||
|
# Test for disabled on each field
|
||||||
|
self.assertTrue("disabled" in form[field_name].attrs)
|
||||||
|
|
||||||
|
def test_domain_edit_authorizing_official_federal(self):
|
||||||
|
"""Tests that no edit can occur when the underlying domain is federal"""
|
||||||
|
|
||||||
|
# Set the org type to federal
|
||||||
|
self.domain_information.organization_type = DomainInformation.OrganizationChoices.FEDERAL
|
||||||
|
self.domain_information.save()
|
||||||
|
|
||||||
|
# Add an AO. We can do this at the model level, just not the form level.
|
||||||
|
self.domain_information.authorizing_official = Contact(
|
||||||
|
first_name="Apple", last_name="Tester", title="CIO", email="nobody@igorville.gov"
|
||||||
|
)
|
||||||
|
self.domain_information.authorizing_official.save()
|
||||||
|
self.domain_information.save()
|
||||||
|
|
||||||
|
ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}))
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# Test if the form is populating data correctly
|
||||||
|
ao_form = ao_page.forms[0]
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
("first_name", "Apple"),
|
||||||
|
("last_name", "Tester"),
|
||||||
|
("title", "CIO"),
|
||||||
|
("email", "nobody@igorville.gov"),
|
||||||
|
]
|
||||||
|
self.assert_all_form_fields_have_expected_values(ao_form, test_cases, test_for_disabled=True)
|
||||||
|
|
||||||
|
# Attempt to change data on each field. Because this domain is federal,
|
||||||
|
# this should not succeed.
|
||||||
|
ao_form["first_name"] = "Orange"
|
||||||
|
ao_form["last_name"] = "Smoothie"
|
||||||
|
ao_form["title"] = "Cat"
|
||||||
|
ao_form["email"] = "somebody@igorville.gov"
|
||||||
|
|
||||||
|
submission = ao_form.submit()
|
||||||
|
|
||||||
|
# A 302 indicates this page underwent a redirect.
|
||||||
|
self.assertEqual(submission.status_code, 302)
|
||||||
|
|
||||||
|
followed_submission = submission.follow()
|
||||||
|
|
||||||
|
# Test the returned form for data accuracy. These values should be unchanged.
|
||||||
|
new_form = followed_submission.forms[0]
|
||||||
|
self.assert_all_form_fields_have_expected_values(new_form, test_cases, test_for_disabled=True)
|
||||||
|
|
||||||
|
# refresh domain information. Test these values in the DB.
|
||||||
|
self.domain_information.refresh_from_db()
|
||||||
|
|
||||||
|
# All values should be unchanged. These are defined manually for code clarity.
|
||||||
|
self.assertEqual("Apple", self.domain_information.authorizing_official.first_name)
|
||||||
|
self.assertEqual("Tester", self.domain_information.authorizing_official.last_name)
|
||||||
|
self.assertEqual("CIO", self.domain_information.authorizing_official.title)
|
||||||
|
self.assertEqual("nobody@igorville.gov", self.domain_information.authorizing_official.email)
|
||||||
|
|
||||||
|
def test_domain_edit_authorizing_official_tribal(self):
|
||||||
|
"""Tests that no edit can occur when the underlying domain is tribal"""
|
||||||
|
|
||||||
|
# Set the org type to federal
|
||||||
|
self.domain_information.organization_type = DomainInformation.OrganizationChoices.TRIBAL
|
||||||
|
self.domain_information.save()
|
||||||
|
|
||||||
|
# Add an AO. We can do this at the model level, just not the form level.
|
||||||
|
self.domain_information.authorizing_official = Contact(
|
||||||
|
first_name="Apple", last_name="Tester", title="CIO", email="nobody@igorville.gov"
|
||||||
|
)
|
||||||
|
self.domain_information.authorizing_official.save()
|
||||||
|
self.domain_information.save()
|
||||||
|
|
||||||
|
ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}))
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# Test if the form is populating data correctly
|
||||||
|
ao_form = ao_page.forms[0]
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
("first_name", "Apple"),
|
||||||
|
("last_name", "Tester"),
|
||||||
|
("title", "CIO"),
|
||||||
|
("email", "nobody@igorville.gov"),
|
||||||
|
]
|
||||||
|
self.assert_all_form_fields_have_expected_values(ao_form, test_cases, test_for_disabled=True)
|
||||||
|
|
||||||
|
# Attempt to change data on each field. Because this domain is federal,
|
||||||
|
# this should not succeed.
|
||||||
|
ao_form["first_name"] = "Orange"
|
||||||
|
ao_form["last_name"] = "Smoothie"
|
||||||
|
ao_form["title"] = "Cat"
|
||||||
|
ao_form["email"] = "somebody@igorville.gov"
|
||||||
|
|
||||||
|
submission = ao_form.submit()
|
||||||
|
|
||||||
|
# A 302 indicates this page underwent a redirect.
|
||||||
|
self.assertEqual(submission.status_code, 302)
|
||||||
|
|
||||||
|
followed_submission = submission.follow()
|
||||||
|
|
||||||
|
# Test the returned form for data accuracy. These values should be unchanged.
|
||||||
|
new_form = followed_submission.forms[0]
|
||||||
|
self.assert_all_form_fields_have_expected_values(new_form, test_cases, test_for_disabled=True)
|
||||||
|
|
||||||
|
# refresh domain information. Test these values in the DB.
|
||||||
|
self.domain_information.refresh_from_db()
|
||||||
|
|
||||||
|
# All values should be unchanged. These are defined manually for code clarity.
|
||||||
|
self.assertEqual("Apple", self.domain_information.authorizing_official.first_name)
|
||||||
|
self.assertEqual("Tester", self.domain_information.authorizing_official.last_name)
|
||||||
|
self.assertEqual("CIO", self.domain_information.authorizing_official.title)
|
||||||
|
self.assertEqual("nobody@igorville.gov", self.domain_information.authorizing_official.email)
|
||||||
|
|
||||||
def test_domain_edit_authorizing_official_creates_new(self):
|
def test_domain_edit_authorizing_official_creates_new(self):
|
||||||
"""When editing an authorizing official for domain information and AO IS
|
"""When editing an authorizing official for domain information and AO IS
|
||||||
joined to another object"""
|
joined to another object"""
|
||||||
|
@ -1088,6 +1226,149 @@ class TestDomainOrganization(TestDomainOverview):
|
||||||
self.assertContains(success_result_page, "Not igorville")
|
self.assertContains(success_result_page, "Not igorville")
|
||||||
self.assertContains(success_result_page, "Faketown")
|
self.assertContains(success_result_page, "Faketown")
|
||||||
|
|
||||||
|
def test_domain_org_name_address_form_tribal(self):
|
||||||
|
"""
|
||||||
|
Submitting a change to organization_name is blocked for tribal domains
|
||||||
|
"""
|
||||||
|
# Set the current domain to a tribal organization with a preset value.
|
||||||
|
# Save first, so we can test if saving is unaffected (it should be).
|
||||||
|
tribal_org_type = DomainInformation.OrganizationChoices.TRIBAL
|
||||||
|
self.domain_information.organization_type = tribal_org_type
|
||||||
|
self.domain_information.save()
|
||||||
|
try:
|
||||||
|
# Add an org name
|
||||||
|
self.domain_information.organization_name = "Town of Igorville"
|
||||||
|
self.domain_information.save()
|
||||||
|
except ValueError as err:
|
||||||
|
self.fail(f"A ValueError was caught during the test: {err}")
|
||||||
|
|
||||||
|
self.assertEqual(self.domain_information.organization_type, tribal_org_type)
|
||||||
|
|
||||||
|
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||||
|
|
||||||
|
form = org_name_page.forms[0]
|
||||||
|
# Check the value of the input field
|
||||||
|
organization_name_input = form.fields["organization_name"][0]
|
||||||
|
self.assertEqual(organization_name_input.value, "Town of Igorville")
|
||||||
|
|
||||||
|
# Check if the input field is disabled
|
||||||
|
self.assertTrue("disabled" in organization_name_input.attrs)
|
||||||
|
self.assertEqual(organization_name_input.attrs.get("disabled"), "")
|
||||||
|
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
|
||||||
|
org_name_page.form["organization_name"] = "Not igorville"
|
||||||
|
org_name_page.form["city"] = "Faketown"
|
||||||
|
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# Make the change. The org name should be unchanged, but city should be modifiable.
|
||||||
|
success_result_page = org_name_page.form.submit()
|
||||||
|
self.assertEqual(success_result_page.status_code, 200)
|
||||||
|
|
||||||
|
# Check for the old and new value
|
||||||
|
self.assertContains(success_result_page, "Town of Igorville")
|
||||||
|
self.assertNotContains(success_result_page, "Not igorville")
|
||||||
|
|
||||||
|
# Do another check on the form itself
|
||||||
|
form = success_result_page.forms[0]
|
||||||
|
# Check the value of the input field
|
||||||
|
organization_name_input = form.fields["organization_name"][0]
|
||||||
|
self.assertEqual(organization_name_input.value, "Town of Igorville")
|
||||||
|
|
||||||
|
# Check if the input field is disabled
|
||||||
|
self.assertTrue("disabled" in organization_name_input.attrs)
|
||||||
|
self.assertEqual(organization_name_input.attrs.get("disabled"), "")
|
||||||
|
|
||||||
|
# Check for the value we want to update
|
||||||
|
self.assertContains(success_result_page, "Faketown")
|
||||||
|
|
||||||
|
def test_domain_org_name_address_form_federal(self):
|
||||||
|
"""
|
||||||
|
Submitting a change to federal_agency is blocked for federal domains
|
||||||
|
"""
|
||||||
|
# Set the current domain to a tribal organization with a preset value.
|
||||||
|
# Save first, so we can test if saving is unaffected (it should be).
|
||||||
|
fed_org_type = DomainInformation.OrganizationChoices.FEDERAL
|
||||||
|
self.domain_information.organization_type = fed_org_type
|
||||||
|
self.domain_information.save()
|
||||||
|
try:
|
||||||
|
self.domain_information.federal_agency = "AMTRAK"
|
||||||
|
self.domain_information.save()
|
||||||
|
except ValueError as err:
|
||||||
|
self.fail(f"A ValueError was caught during the test: {err}")
|
||||||
|
|
||||||
|
self.assertEqual(self.domain_information.organization_type, fed_org_type)
|
||||||
|
|
||||||
|
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||||
|
|
||||||
|
form = org_name_page.forms[0]
|
||||||
|
# Check the value of the input field
|
||||||
|
agency_input = form.fields["federal_agency"][0]
|
||||||
|
self.assertEqual(agency_input.value, "AMTRAK")
|
||||||
|
|
||||||
|
# Check if the input field is disabled
|
||||||
|
self.assertTrue("disabled" in agency_input.attrs)
|
||||||
|
self.assertEqual(agency_input.attrs.get("disabled"), "")
|
||||||
|
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
|
||||||
|
org_name_page.form["federal_agency"] = "Department of State"
|
||||||
|
org_name_page.form["city"] = "Faketown"
|
||||||
|
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
# Make the change. The agency should be unchanged, but city should be modifiable.
|
||||||
|
success_result_page = org_name_page.form.submit()
|
||||||
|
self.assertEqual(success_result_page.status_code, 200)
|
||||||
|
|
||||||
|
# Check for the old and new value
|
||||||
|
self.assertContains(success_result_page, "AMTRAK")
|
||||||
|
self.assertNotContains(success_result_page, "Department of State")
|
||||||
|
|
||||||
|
# Do another check on the form itself
|
||||||
|
form = success_result_page.forms[0]
|
||||||
|
# Check the value of the input field
|
||||||
|
organization_name_input = form.fields["federal_agency"][0]
|
||||||
|
self.assertEqual(organization_name_input.value, "AMTRAK")
|
||||||
|
|
||||||
|
# Check if the input field is disabled
|
||||||
|
self.assertTrue("disabled" in organization_name_input.attrs)
|
||||||
|
self.assertEqual(organization_name_input.attrs.get("disabled"), "")
|
||||||
|
|
||||||
|
# Check for the value we want to update
|
||||||
|
self.assertContains(success_result_page, "Faketown")
|
||||||
|
|
||||||
|
def test_federal_agency_submit_blocked(self):
|
||||||
|
"""
|
||||||
|
Submitting a change to federal_agency is blocked for federal domains
|
||||||
|
"""
|
||||||
|
# Set the current domain to a tribal organization with a preset value.
|
||||||
|
# Save first, so we can test if saving is unaffected (it should be).
|
||||||
|
federal_org_type = DomainInformation.OrganizationChoices.FEDERAL
|
||||||
|
self.domain_information.organization_type = federal_org_type
|
||||||
|
self.domain_information.save()
|
||||||
|
|
||||||
|
old_federal_agency_value = ("AMTRAK", "AMTRAK")
|
||||||
|
try:
|
||||||
|
# Add a federal agency. Defined as a tuple since this list may change order.
|
||||||
|
self.domain_information.federal_agency = old_federal_agency_value
|
||||||
|
self.domain_information.save()
|
||||||
|
except ValueError as err:
|
||||||
|
self.fail(f"A ValueError was caught during the test: {err}")
|
||||||
|
|
||||||
|
self.assertEqual(self.domain_information.organization_type, federal_org_type)
|
||||||
|
|
||||||
|
new_value = ("Department of State", "Department of State")
|
||||||
|
self.client.post(
|
||||||
|
reverse("domain-org-name-address", kwargs={"pk": self.domain.id}),
|
||||||
|
{
|
||||||
|
"federal_agency": new_value,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(self.domain_information.federal_agency, old_federal_agency_value)
|
||||||
|
self.assertNotEqual(self.domain_information.federal_agency, new_value)
|
||||||
|
|
||||||
|
|
||||||
class TestDomainContactInformation(TestDomainOverview):
|
class TestDomainContactInformation(TestDomainOverview):
|
||||||
def test_domain_your_contact_information(self):
|
def test_domain_your_contact_information(self):
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
from collections import Counter
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
|
from registrar.models.domain_request import DomainRequest
|
||||||
from registrar.models.domain_information import DomainInformation
|
from registrar.models.domain_information import DomainInformation
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
@ -19,16 +21,22 @@ def write_header(writer, columns):
|
||||||
Receives params from the parent methods and outputs a CSV with a header row.
|
Receives params from the parent methods and outputs a CSV with a header row.
|
||||||
Works with write_header as long as the same writer object is passed.
|
Works with write_header as long as the same writer object is passed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
writer.writerow(columns)
|
writer.writerow(columns)
|
||||||
|
|
||||||
|
|
||||||
def get_domain_infos(filter_condition, sort_fields):
|
def get_domain_infos(filter_condition, sort_fields):
|
||||||
|
"""
|
||||||
|
Returns DomainInformation objects filtered and sorted based on the provided conditions.
|
||||||
|
filter_condition -> A dictionary of conditions to filter the objects.
|
||||||
|
sort_fields -> A list of fields to sort the resulting query set.
|
||||||
|
returns: A queryset of DomainInformation objects
|
||||||
|
"""
|
||||||
domain_infos = (
|
domain_infos = (
|
||||||
DomainInformation.objects.select_related("domain", "authorizing_official")
|
DomainInformation.objects.select_related("domain", "authorizing_official")
|
||||||
.prefetch_related("domain__permissions")
|
.prefetch_related("domain__permissions")
|
||||||
.filter(**filter_condition)
|
.filter(**filter_condition)
|
||||||
.order_by(*sort_fields)
|
.order_by(*sort_fields)
|
||||||
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Do a mass concat of the first and last name fields for authorizing_official.
|
# Do a mass concat of the first and last name fields for authorizing_official.
|
||||||
|
@ -45,7 +53,7 @@ def get_domain_infos(filter_condition, sort_fields):
|
||||||
return domain_infos_cleaned
|
return domain_infos_cleaned
|
||||||
|
|
||||||
|
|
||||||
def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False):
|
def parse_domain_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False):
|
||||||
"""Given a set of columns, generate a new row from cleaned column data"""
|
"""Given a set of columns, generate a new row from cleaned column data"""
|
||||||
|
|
||||||
# Domain should never be none when parsing this information
|
# Domain should never be none when parsing this information
|
||||||
|
@ -129,7 +137,7 @@ def _get_security_emails(sec_contact_ids):
|
||||||
return security_emails_dict
|
return security_emails_dict
|
||||||
|
|
||||||
|
|
||||||
def write_csv(
|
def write_domains_csv(
|
||||||
writer,
|
writer,
|
||||||
columns,
|
columns,
|
||||||
sort_fields,
|
sort_fields,
|
||||||
|
@ -138,10 +146,10 @@ def write_csv(
|
||||||
should_write_header=True,
|
should_write_header=True,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Receives params from the parent methods and outputs a CSV with fltered and sorted domains.
|
Receives params from the parent methods and outputs a CSV with filtered and sorted domains.
|
||||||
Works with write_header as longas the same writer object is passed.
|
Works with write_header as long as the same writer object is passed.
|
||||||
get_domain_managers: Conditional bc we only use domain manager info for export_data_full_to_csv
|
get_domain_managers: Conditional bc we only use domain manager info for export_data_full_to_csv
|
||||||
should_write_header: Conditional bc export_data_growth_to_csv calls write_body twice
|
should_write_header: Conditional bc export_data_domain_growth_to_csv calls write_body twice
|
||||||
"""
|
"""
|
||||||
|
|
||||||
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
|
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
|
||||||
|
@ -175,7 +183,7 @@ def write_csv(
|
||||||
columns.append(column_name)
|
columns.append(column_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
row = parse_row(columns, domain_info, security_emails_dict, get_domain_managers)
|
row = parse_domain_row(columns, domain_info, security_emails_dict, get_domain_managers)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# This should not happen. If it does, just skip this row.
|
# This should not happen. If it does, just skip this row.
|
||||||
|
@ -189,6 +197,82 @@ def write_csv(
|
||||||
writer.writerows(total_body_rows)
|
writer.writerows(total_body_rows)
|
||||||
|
|
||||||
|
|
||||||
|
def get_requests(filter_condition, sort_fields):
|
||||||
|
"""
|
||||||
|
Returns DomainRequest objects filtered and sorted based on the provided conditions.
|
||||||
|
filter_condition -> A dictionary of conditions to filter the objects.
|
||||||
|
sort_fields -> A list of fields to sort the resulting query set.
|
||||||
|
returns: A queryset of DomainRequest objects
|
||||||
|
"""
|
||||||
|
requests = DomainRequest.objects.filter(**filter_condition).order_by(*sort_fields).distinct()
|
||||||
|
return requests
|
||||||
|
|
||||||
|
|
||||||
|
def parse_request_row(columns, request: DomainRequest):
|
||||||
|
"""Given a set of columns, generate a new row from cleaned column data"""
|
||||||
|
|
||||||
|
requested_domain_name = "No requested domain"
|
||||||
|
|
||||||
|
if request.requested_domain is not None:
|
||||||
|
requested_domain_name = request.requested_domain.name
|
||||||
|
|
||||||
|
if request.federal_type:
|
||||||
|
request_type = f"{request.get_organization_type_display()} - {request.get_federal_type_display()}"
|
||||||
|
else:
|
||||||
|
request_type = request.get_organization_type_display()
|
||||||
|
|
||||||
|
# create a dictionary of fields which can be included in output
|
||||||
|
FIELDS = {
|
||||||
|
"Requested domain": requested_domain_name,
|
||||||
|
"Status": request.get_status_display(),
|
||||||
|
"Organization type": request_type,
|
||||||
|
"Agency": request.federal_agency,
|
||||||
|
"Organization name": request.organization_name,
|
||||||
|
"City": request.city,
|
||||||
|
"State": request.state_territory,
|
||||||
|
"AO email": request.authorizing_official.email if request.authorizing_official else " ",
|
||||||
|
"Security contact email": request,
|
||||||
|
"Created at": request.created_at,
|
||||||
|
"Submission date": request.submission_date,
|
||||||
|
}
|
||||||
|
|
||||||
|
row = [FIELDS.get(column, "") for column in columns]
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def write_requests_csv(
|
||||||
|
writer,
|
||||||
|
columns,
|
||||||
|
sort_fields,
|
||||||
|
filter_condition,
|
||||||
|
should_write_header=True,
|
||||||
|
):
|
||||||
|
"""Receives params from the parent methods and outputs a CSV with filtered and sorted requests.
|
||||||
|
Works with write_header as long as the same writer object is passed."""
|
||||||
|
|
||||||
|
all_requests = get_requests(filter_condition, sort_fields)
|
||||||
|
|
||||||
|
# Reduce the memory overhead when performing the write operation
|
||||||
|
paginator = Paginator(all_requests, 1000)
|
||||||
|
|
||||||
|
for page_num in paginator.page_range:
|
||||||
|
page = paginator.page(page_num)
|
||||||
|
rows = []
|
||||||
|
for request in page.object_list:
|
||||||
|
try:
|
||||||
|
row = parse_request_row(columns, request)
|
||||||
|
rows.append(row)
|
||||||
|
except ValueError:
|
||||||
|
# This should not happen. If it does, just skip this row.
|
||||||
|
# It indicates that DomainInformation.domain is None.
|
||||||
|
logger.error("csv_export -> Error when parsing row, domain was None")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if should_write_header:
|
||||||
|
write_header(writer, columns)
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
|
||||||
def export_data_type_to_csv(csv_file):
|
def export_data_type_to_csv(csv_file):
|
||||||
"""All domains report with extra columns"""
|
"""All domains report with extra columns"""
|
||||||
|
|
||||||
|
@ -223,7 +307,9 @@ def export_data_type_to_csv(csv_file):
|
||||||
Domain.State.ON_HOLD,
|
Domain.State.ON_HOLD,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True)
|
write_domains_csv(
|
||||||
|
writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def export_data_full_to_csv(csv_file):
|
def export_data_full_to_csv(csv_file):
|
||||||
|
@ -254,7 +340,9 @@ def export_data_full_to_csv(csv_file):
|
||||||
Domain.State.ON_HOLD,
|
Domain.State.ON_HOLD,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True)
|
write_domains_csv(
|
||||||
|
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def export_data_federal_to_csv(csv_file):
|
def export_data_federal_to_csv(csv_file):
|
||||||
|
@ -286,7 +374,9 @@ def export_data_federal_to_csv(csv_file):
|
||||||
Domain.State.ON_HOLD,
|
Domain.State.ON_HOLD,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True)
|
write_domains_csv(
|
||||||
|
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_default_start_date():
|
def get_default_start_date():
|
||||||
|
@ -299,7 +389,15 @@ def get_default_end_date():
|
||||||
return timezone.now()
|
return timezone.now()
|
||||||
|
|
||||||
|
|
||||||
def export_data_growth_to_csv(csv_file, start_date, end_date):
|
def format_start_date(start_date):
|
||||||
|
return timezone.make_aware(datetime.strptime(start_date, "%Y-%m-%d")) if start_date else get_default_start_date()
|
||||||
|
|
||||||
|
|
||||||
|
def format_end_date(end_date):
|
||||||
|
return timezone.make_aware(datetime.strptime(end_date, "%Y-%m-%d")) if end_date else get_default_end_date()
|
||||||
|
|
||||||
|
|
||||||
|
def export_data_domain_growth_to_csv(csv_file, start_date, end_date):
|
||||||
"""
|
"""
|
||||||
Growth report:
|
Growth report:
|
||||||
Receive start and end dates from the view, parse them.
|
Receive start and end dates from the view, parse them.
|
||||||
|
@ -308,16 +406,9 @@ def export_data_growth_to_csv(csv_file, start_date, end_date):
|
||||||
the start and end dates. Specify sort params for both lists.
|
the start and end dates. Specify sort params for both lists.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
start_date_formatted = (
|
start_date_formatted = format_start_date(start_date)
|
||||||
timezone.make_aware(datetime.strptime(start_date, "%Y-%m-%d")) if start_date else get_default_start_date()
|
end_date_formatted = format_end_date(end_date)
|
||||||
)
|
|
||||||
|
|
||||||
end_date_formatted = (
|
|
||||||
timezone.make_aware(datetime.strptime(end_date, "%Y-%m-%d")) if end_date else get_default_end_date()
|
|
||||||
)
|
|
||||||
|
|
||||||
writer = csv.writer(csv_file)
|
writer = csv.writer(csv_file)
|
||||||
|
|
||||||
# define columns to include in export
|
# define columns to include in export
|
||||||
columns = [
|
columns = [
|
||||||
"Domain name",
|
"Domain name",
|
||||||
|
@ -353,8 +444,10 @@ def export_data_growth_to_csv(csv_file, start_date, end_date):
|
||||||
"domain__deleted__gte": start_date_formatted,
|
"domain__deleted__gte": start_date_formatted,
|
||||||
}
|
}
|
||||||
|
|
||||||
write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True)
|
write_domains_csv(
|
||||||
write_csv(
|
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||||
|
)
|
||||||
|
write_domains_csv(
|
||||||
writer,
|
writer,
|
||||||
columns,
|
columns,
|
||||||
sort_fields_for_deleted_domains,
|
sort_fields_for_deleted_domains,
|
||||||
|
@ -362,3 +455,266 @@ def export_data_growth_to_csv(csv_file, start_date, end_date):
|
||||||
get_domain_managers=False,
|
get_domain_managers=False,
|
||||||
should_write_header=False,
|
should_write_header=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_sliced_domains(filter_condition, distinct=False):
|
||||||
|
"""Get filtered domains counts sliced by org type and election office.
|
||||||
|
Pass distinct=True when filtering by permissions so we do not to count multiples
|
||||||
|
when a domain has more that one manager.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Round trip 1: Get distinct domain names based on filter condition
|
||||||
|
domains_count = DomainInformation.objects.filter(**filter_condition).distinct().count()
|
||||||
|
|
||||||
|
# Round trip 2: Get counts for other slices
|
||||||
|
if distinct:
|
||||||
|
organization_types_query = (
|
||||||
|
DomainInformation.objects.filter(**filter_condition).values_list("organization_type", flat=True).distinct()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
organization_types_query = DomainInformation.objects.filter(**filter_condition).values_list(
|
||||||
|
"organization_type", flat=True
|
||||||
|
)
|
||||||
|
organization_type_counts = Counter(organization_types_query)
|
||||||
|
|
||||||
|
federal = organization_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0)
|
||||||
|
interstate = organization_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0)
|
||||||
|
state_or_territory = organization_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0)
|
||||||
|
tribal = organization_type_counts.get(DomainRequest.OrganizationChoices.TRIBAL, 0)
|
||||||
|
county = organization_type_counts.get(DomainRequest.OrganizationChoices.COUNTY, 0)
|
||||||
|
city = organization_type_counts.get(DomainRequest.OrganizationChoices.CITY, 0)
|
||||||
|
special_district = organization_type_counts.get(DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, 0)
|
||||||
|
school_district = organization_type_counts.get(DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, 0)
|
||||||
|
|
||||||
|
# Round trip 3
|
||||||
|
election_board = DomainInformation.objects.filter(is_election_board=True, **filter_condition).distinct().count()
|
||||||
|
|
||||||
|
return [
|
||||||
|
domains_count,
|
||||||
|
federal,
|
||||||
|
interstate,
|
||||||
|
state_or_territory,
|
||||||
|
tribal,
|
||||||
|
county,
|
||||||
|
city,
|
||||||
|
special_district,
|
||||||
|
school_district,
|
||||||
|
election_board,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_sliced_requests(filter_condition, distinct=False):
|
||||||
|
"""Get filtered requests counts sliced by org type and election office."""
|
||||||
|
|
||||||
|
# Round trip 1: Get distinct requests based on filter condition
|
||||||
|
requests_count = DomainRequest.objects.filter(**filter_condition).distinct().count()
|
||||||
|
|
||||||
|
# Round trip 2: Get counts for other slices
|
||||||
|
if distinct:
|
||||||
|
organization_types_query = (
|
||||||
|
DomainRequest.objects.filter(**filter_condition).values_list("organization_type", flat=True).distinct()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
organization_types_query = DomainRequest.objects.filter(**filter_condition).values_list(
|
||||||
|
"organization_type", flat=True
|
||||||
|
)
|
||||||
|
organization_type_counts = Counter(organization_types_query)
|
||||||
|
|
||||||
|
federal = organization_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0)
|
||||||
|
interstate = organization_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0)
|
||||||
|
state_or_territory = organization_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0)
|
||||||
|
tribal = organization_type_counts.get(DomainRequest.OrganizationChoices.TRIBAL, 0)
|
||||||
|
county = organization_type_counts.get(DomainRequest.OrganizationChoices.COUNTY, 0)
|
||||||
|
city = organization_type_counts.get(DomainRequest.OrganizationChoices.CITY, 0)
|
||||||
|
special_district = organization_type_counts.get(DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, 0)
|
||||||
|
school_district = organization_type_counts.get(DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, 0)
|
||||||
|
|
||||||
|
# Round trip 3
|
||||||
|
election_board = DomainRequest.objects.filter(is_election_board=True, **filter_condition).distinct().count()
|
||||||
|
|
||||||
|
return [
|
||||||
|
requests_count,
|
||||||
|
federal,
|
||||||
|
interstate,
|
||||||
|
state_or_territory,
|
||||||
|
tribal,
|
||||||
|
county,
|
||||||
|
city,
|
||||||
|
special_district,
|
||||||
|
school_district,
|
||||||
|
election_board,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
|
||||||
|
"""Get counts for domains that have domain managers for two different dates,
|
||||||
|
get list of managed domains at end_date."""
|
||||||
|
|
||||||
|
start_date_formatted = format_start_date(start_date)
|
||||||
|
end_date_formatted = format_end_date(end_date)
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
columns = [
|
||||||
|
"Domain name",
|
||||||
|
"Domain type",
|
||||||
|
]
|
||||||
|
sort_fields = [
|
||||||
|
"domain__name",
|
||||||
|
]
|
||||||
|
filter_managed_domains_start_date = {
|
||||||
|
"domain__permissions__isnull": False,
|
||||||
|
"domain__first_ready__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
managed_domains_sliced_at_start_date = get_sliced_domains(filter_managed_domains_start_date, True)
|
||||||
|
|
||||||
|
writer.writerow(["MANAGED DOMAINS COUNTS AT START DATE"])
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
"Total",
|
||||||
|
"Federal",
|
||||||
|
"Interstate",
|
||||||
|
"State or territory",
|
||||||
|
"Tribal",
|
||||||
|
"County",
|
||||||
|
"City",
|
||||||
|
"Special district",
|
||||||
|
"School district",
|
||||||
|
"Election office",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
writer.writerow(managed_domains_sliced_at_start_date)
|
||||||
|
writer.writerow([])
|
||||||
|
|
||||||
|
filter_managed_domains_end_date = {
|
||||||
|
"domain__permissions__isnull": False,
|
||||||
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
managed_domains_sliced_at_end_date = get_sliced_domains(filter_managed_domains_end_date, True)
|
||||||
|
|
||||||
|
writer.writerow(["MANAGED DOMAINS COUNTS AT END DATE"])
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
"Total",
|
||||||
|
"Federal",
|
||||||
|
"Interstate",
|
||||||
|
"State or territory",
|
||||||
|
"Tribal",
|
||||||
|
"County",
|
||||||
|
"City",
|
||||||
|
"Special district",
|
||||||
|
"School district",
|
||||||
|
"Election office",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
writer.writerow(managed_domains_sliced_at_end_date)
|
||||||
|
writer.writerow([])
|
||||||
|
|
||||||
|
write_domains_csv(
|
||||||
|
writer,
|
||||||
|
columns,
|
||||||
|
sort_fields,
|
||||||
|
filter_managed_domains_end_date,
|
||||||
|
get_domain_managers=True,
|
||||||
|
should_write_header=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def export_data_unmanaged_domains_to_csv(csv_file, start_date, end_date):
|
||||||
|
"""Get counts for domains that do not have domain managers for two different dates,
|
||||||
|
get list of unmanaged domains at end_date."""
|
||||||
|
|
||||||
|
start_date_formatted = format_start_date(start_date)
|
||||||
|
end_date_formatted = format_end_date(end_date)
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
columns = [
|
||||||
|
"Domain name",
|
||||||
|
"Domain type",
|
||||||
|
]
|
||||||
|
sort_fields = [
|
||||||
|
"domain__name",
|
||||||
|
]
|
||||||
|
|
||||||
|
filter_unmanaged_domains_start_date = {
|
||||||
|
"domain__permissions__isnull": True,
|
||||||
|
"domain__first_ready__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
unmanaged_domains_sliced_at_start_date = get_sliced_domains(filter_unmanaged_domains_start_date, True)
|
||||||
|
|
||||||
|
writer.writerow(["UNMANAGED DOMAINS AT START DATE"])
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
"Total",
|
||||||
|
"Federal",
|
||||||
|
"Interstate",
|
||||||
|
"State or territory",
|
||||||
|
"Tribal",
|
||||||
|
"County",
|
||||||
|
"City",
|
||||||
|
"Special district",
|
||||||
|
"School district",
|
||||||
|
"Election office",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
writer.writerow(unmanaged_domains_sliced_at_start_date)
|
||||||
|
writer.writerow([])
|
||||||
|
|
||||||
|
filter_unmanaged_domains_end_date = {
|
||||||
|
"domain__permissions__isnull": True,
|
||||||
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
unmanaged_domains_sliced_at_end_date = get_sliced_domains(filter_unmanaged_domains_end_date, True)
|
||||||
|
|
||||||
|
writer.writerow(["UNMANAGED DOMAINS AT END DATE"])
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
"Total",
|
||||||
|
"Federal",
|
||||||
|
"Interstate",
|
||||||
|
"State or territory",
|
||||||
|
"Tribal",
|
||||||
|
"County",
|
||||||
|
"City",
|
||||||
|
"Special district",
|
||||||
|
"School district",
|
||||||
|
"Election office",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
writer.writerow(unmanaged_domains_sliced_at_end_date)
|
||||||
|
writer.writerow([])
|
||||||
|
|
||||||
|
write_domains_csv(
|
||||||
|
writer,
|
||||||
|
columns,
|
||||||
|
sort_fields,
|
||||||
|
filter_unmanaged_domains_end_date,
|
||||||
|
get_domain_managers=False,
|
||||||
|
should_write_header=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def export_data_requests_growth_to_csv(csv_file, start_date, end_date):
|
||||||
|
"""
|
||||||
|
Growth report:
|
||||||
|
Receive start and end dates from the view, parse them.
|
||||||
|
Request from write_requests_body SUBMITTED requests that are created between
|
||||||
|
the start and end dates. Specify sort params.
|
||||||
|
"""
|
||||||
|
|
||||||
|
start_date_formatted = format_start_date(start_date)
|
||||||
|
end_date_formatted = format_end_date(end_date)
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
# define columns to include in export
|
||||||
|
columns = [
|
||||||
|
"Requested domain",
|
||||||
|
"Organization type",
|
||||||
|
"Submission date",
|
||||||
|
]
|
||||||
|
sort_fields = [
|
||||||
|
"requested_domain__name",
|
||||||
|
]
|
||||||
|
filter_condition = {
|
||||||
|
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
|
"submission_date__lte": end_date_formatted,
|
||||||
|
"submission_date__gte": start_date_formatted,
|
||||||
|
}
|
||||||
|
|
||||||
|
write_requests_csv(writer, columns, sort_fields, filter_condition, should_write_header=True)
|
||||||
|
|
|
@ -2,8 +2,12 @@
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
from email.mime.application import MIMEApplication
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -15,7 +19,14 @@ class EmailSendingError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def send_templated_email(template_name: str, subject_template_name: str, to_address: str, bcc_address="", context={}):
|
def send_templated_email(
|
||||||
|
template_name: str,
|
||||||
|
subject_template_name: str,
|
||||||
|
to_address: str,
|
||||||
|
bcc_address="",
|
||||||
|
context={},
|
||||||
|
attachment_file: str = None,
|
||||||
|
):
|
||||||
"""Send an email built from a template to one email address.
|
"""Send an email built from a template to one email address.
|
||||||
|
|
||||||
template_name and subject_template_name are relative to the same template
|
template_name and subject_template_name are relative to the same template
|
||||||
|
@ -45,15 +56,50 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr
|
||||||
destination["BccAddresses"] = [bcc_address]
|
destination["BccAddresses"] = [bcc_address]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ses_client.send_email(
|
if attachment_file is None:
|
||||||
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
ses_client.send_email(
|
||||||
Destination=destination,
|
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
||||||
Content={
|
Destination=destination,
|
||||||
"Simple": {
|
Content={
|
||||||
"Subject": {"Data": subject},
|
"Simple": {
|
||||||
"Body": {"Text": {"Data": email_body}},
|
"Subject": {"Data": subject},
|
||||||
|
"Body": {"Text": {"Data": email_body}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
)
|
||||||
)
|
else:
|
||||||
|
ses_client = boto3.client(
|
||||||
|
"ses",
|
||||||
|
region_name=settings.AWS_REGION,
|
||||||
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
|
config=settings.BOTO_CONFIG,
|
||||||
|
)
|
||||||
|
send_email_with_attachment(
|
||||||
|
settings.DEFAULT_FROM_EMAIL, to_address, subject, email_body, attachment_file, ses_client
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise EmailSendingError("Could not send SES email.") from exc
|
raise EmailSendingError("Could not send SES email.") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def send_email_with_attachment(sender, recipient, subject, body, attachment_file, ses_client):
|
||||||
|
# Create a multipart/mixed parent container
|
||||||
|
msg = MIMEMultipart("mixed")
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg["From"] = sender
|
||||||
|
msg["To"] = recipient
|
||||||
|
|
||||||
|
# Add the text part
|
||||||
|
text_part = MIMEText(body, "plain")
|
||||||
|
msg.attach(text_part)
|
||||||
|
|
||||||
|
# Add the attachment part
|
||||||
|
attachment_part = MIMEApplication(attachment_file)
|
||||||
|
# Adding attachment header + filename that the attachment will be called
|
||||||
|
current_date = datetime.now().strftime("%m%d%Y")
|
||||||
|
current_filename = f"domain-metadata-{current_date}.zip"
|
||||||
|
attachment_part.add_header("Content-Disposition", f'attachment; filename="{current_filename}"')
|
||||||
|
msg.attach(attachment_part)
|
||||||
|
|
||||||
|
response = ses_client.send_raw_email(Source=sender, Destinations=[recipient], RawMessage={"Data": msg.as_string()})
|
||||||
|
return response
|
||||||
|
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.db.models import Avg, F
|
||||||
|
from .. import models
|
||||||
|
import datetime
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from registrar.utility import csv_export
|
from registrar.utility import csv_export
|
||||||
|
|
||||||
|
@ -10,7 +16,157 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ExportData(View):
|
class AnalyticsView(View):
|
||||||
|
def get(self, request):
|
||||||
|
thirty_days_ago = datetime.datetime.today() - datetime.timedelta(days=30)
|
||||||
|
thirty_days_ago = timezone.make_aware(thirty_days_ago)
|
||||||
|
|
||||||
|
last_30_days_applications = models.DomainRequest.objects.filter(created_at__gt=thirty_days_ago)
|
||||||
|
last_30_days_approved_applications = models.DomainRequest.objects.filter(
|
||||||
|
created_at__gt=thirty_days_ago, status=models.DomainRequest.DomainRequestStatus.APPROVED
|
||||||
|
)
|
||||||
|
avg_approval_time = last_30_days_approved_applications.annotate(
|
||||||
|
approval_time=F("approved_domain__created_at") - F("submission_date")
|
||||||
|
).aggregate(Avg("approval_time"))["approval_time__avg"]
|
||||||
|
# Format the timedelta to display only days
|
||||||
|
if avg_approval_time is not None:
|
||||||
|
avg_approval_time_display = f"{avg_approval_time.days} days"
|
||||||
|
else:
|
||||||
|
avg_approval_time_display = "No approvals to use"
|
||||||
|
|
||||||
|
# The start and end dates are passed as url params
|
||||||
|
start_date = request.GET.get("start_date", "")
|
||||||
|
end_date = request.GET.get("end_date", "")
|
||||||
|
|
||||||
|
start_date_formatted = csv_export.format_start_date(start_date)
|
||||||
|
end_date_formatted = csv_export.format_end_date(end_date)
|
||||||
|
|
||||||
|
filter_managed_domains_start_date = {
|
||||||
|
"domain__permissions__isnull": False,
|
||||||
|
"domain__first_ready__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
filter_managed_domains_end_date = {
|
||||||
|
"domain__permissions__isnull": False,
|
||||||
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
managed_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_managed_domains_start_date, True)
|
||||||
|
managed_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_managed_domains_end_date, True)
|
||||||
|
|
||||||
|
filter_unmanaged_domains_start_date = {
|
||||||
|
"domain__permissions__isnull": True,
|
||||||
|
"domain__first_ready__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
filter_unmanaged_domains_end_date = {
|
||||||
|
"domain__permissions__isnull": True,
|
||||||
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
unmanaged_domains_sliced_at_start_date = csv_export.get_sliced_domains(
|
||||||
|
filter_unmanaged_domains_start_date, True
|
||||||
|
)
|
||||||
|
unmanaged_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_unmanaged_domains_end_date, True)
|
||||||
|
|
||||||
|
filter_ready_domains_start_date = {
|
||||||
|
"domain__state__in": [models.Domain.State.READY],
|
||||||
|
"domain__first_ready__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
filter_ready_domains_end_date = {
|
||||||
|
"domain__state__in": [models.Domain.State.READY],
|
||||||
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
ready_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_ready_domains_start_date)
|
||||||
|
ready_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_ready_domains_end_date)
|
||||||
|
|
||||||
|
filter_deleted_domains_start_date = {
|
||||||
|
"domain__state__in": [models.Domain.State.DELETED],
|
||||||
|
"domain__deleted__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
filter_deleted_domains_end_date = {
|
||||||
|
"domain__state__in": [models.Domain.State.DELETED],
|
||||||
|
"domain__deleted__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
deleted_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_deleted_domains_start_date)
|
||||||
|
deleted_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_deleted_domains_end_date)
|
||||||
|
|
||||||
|
filter_requests_start_date = {
|
||||||
|
"created_at__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
filter_requests_end_date = {
|
||||||
|
"created_at__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
requests_sliced_at_start_date = csv_export.get_sliced_requests(filter_requests_start_date)
|
||||||
|
requests_sliced_at_end_date = csv_export.get_sliced_requests(filter_requests_end_date)
|
||||||
|
|
||||||
|
filter_submitted_requests_start_date = {
|
||||||
|
"status": models.DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
|
"submission_date__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
filter_submitted_requests_end_date = {
|
||||||
|
"status": models.DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
|
"submission_date__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
submitted_requests_sliced_at_start_date = csv_export.get_sliced_requests(filter_submitted_requests_start_date)
|
||||||
|
submitted_requests_sliced_at_end_date = csv_export.get_sliced_requests(filter_submitted_requests_end_date)
|
||||||
|
|
||||||
|
context = dict(
|
||||||
|
# Generate a dictionary of context variables that are common across all admin templates
|
||||||
|
# (site_header, site_url, ...),
|
||||||
|
# include it in the larger context dictionary so it's available in the template rendering context.
|
||||||
|
# This ensures that the admin interface styling and behavior are consistent with other admin pages.
|
||||||
|
**admin.site.each_context(request),
|
||||||
|
data=dict(
|
||||||
|
user_count=models.User.objects.all().count(),
|
||||||
|
domain_count=models.Domain.objects.all().count(),
|
||||||
|
ready_domain_count=models.Domain.objects.filter(state=models.Domain.State.READY).count(),
|
||||||
|
last_30_days_applications=last_30_days_applications.count(),
|
||||||
|
last_30_days_approved_applications=last_30_days_approved_applications.count(),
|
||||||
|
average_application_approval_time_last_30_days=avg_approval_time_display,
|
||||||
|
managed_domains_sliced_at_start_date=managed_domains_sliced_at_start_date,
|
||||||
|
unmanaged_domains_sliced_at_start_date=unmanaged_domains_sliced_at_start_date,
|
||||||
|
managed_domains_sliced_at_end_date=managed_domains_sliced_at_end_date,
|
||||||
|
unmanaged_domains_sliced_at_end_date=unmanaged_domains_sliced_at_end_date,
|
||||||
|
ready_domains_sliced_at_start_date=ready_domains_sliced_at_start_date,
|
||||||
|
deleted_domains_sliced_at_start_date=deleted_domains_sliced_at_start_date,
|
||||||
|
ready_domains_sliced_at_end_date=ready_domains_sliced_at_end_date,
|
||||||
|
deleted_domains_sliced_at_end_date=deleted_domains_sliced_at_end_date,
|
||||||
|
requests_sliced_at_start_date=requests_sliced_at_start_date,
|
||||||
|
submitted_requests_sliced_at_start_date=submitted_requests_sliced_at_start_date,
|
||||||
|
requests_sliced_at_end_date=requests_sliced_at_end_date,
|
||||||
|
submitted_requests_sliced_at_end_date=submitted_requests_sliced_at_end_date,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return render(request, "admin/analytics.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataType(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# match the CSV example with all the fields
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = 'attachment; filename="domains-by-type.csv"'
|
||||||
|
csv_export.export_data_type_to_csv(response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataFull(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Smaller export based on 1
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = 'attachment; filename="current-full.csv"'
|
||||||
|
csv_export.export_data_full_to_csv(response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataFederal(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Federal only
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = 'attachment; filename="current-federal.csv"'
|
||||||
|
csv_export.export_data_federal_to_csv(response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataDomainsGrowth(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# Get start_date and end_date from the request's GET parameters
|
# Get start_date and end_date from the request's GET parameters
|
||||||
# #999: not needed if we switch to django forms
|
# #999: not needed if we switch to django forms
|
||||||
|
@ -19,8 +175,50 @@ class ExportData(View):
|
||||||
|
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = f'attachment; filename="domain-growth-report-{start_date}-to-{end_date}.csv"'
|
response["Content-Disposition"] = f'attachment; filename="domain-growth-report-{start_date}-to-{end_date}.csv"'
|
||||||
# For #999: set export_data_growth_to_csv to return the resulting queryset, which we can then use
|
# For #999: set export_data_domain_growth_to_csv to return the resulting queryset, which we can then use
|
||||||
# in context to display this data in the template.
|
# in context to display this data in the template.
|
||||||
csv_export.export_data_growth_to_csv(response, start_date, end_date)
|
csv_export.export_data_domain_growth_to_csv(response, start_date, end_date)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataRequestsGrowth(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Get start_date and end_date from the request's GET parameters
|
||||||
|
# #999: not needed if we switch to django forms
|
||||||
|
start_date = request.GET.get("start_date", "")
|
||||||
|
end_date = request.GET.get("end_date", "")
|
||||||
|
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = f'attachment; filename="requests-{start_date}-to-{end_date}.csv"'
|
||||||
|
# For #999: set export_data_domain_growth_to_csv to return the resulting queryset, which we can then use
|
||||||
|
# in context to display this data in the template.
|
||||||
|
csv_export.export_data_requests_growth_to_csv(response, start_date, end_date)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataManagedDomains(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Get start_date and end_date from the request's GET parameters
|
||||||
|
# #999: not needed if we switch to django forms
|
||||||
|
start_date = request.GET.get("start_date", "")
|
||||||
|
end_date = request.GET.get("end_date", "")
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = f'attachment; filename="managed-domains-{start_date}-to-{end_date}.csv"'
|
||||||
|
csv_export.export_data_managed_domains_to_csv(response, start_date, end_date)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataUnmanagedDomains(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Get start_date and end_date from the request's GET parameters
|
||||||
|
# #999: not needed if we switch to django forms
|
||||||
|
start_date = request.GET.get("start_date", "")
|
||||||
|
end_date = request.GET.get("end_date", "")
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = f'attachment; filename="unamanaged-domains-{start_date}-to-{end_date}.csv"'
|
||||||
|
csv_export.export_data_unmanaged_domains_to_csv(response, start_date, end_date)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -18,6 +18,8 @@ from django.conf import settings
|
||||||
|
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
Domain,
|
Domain,
|
||||||
|
DomainRequest,
|
||||||
|
DomainInformation,
|
||||||
DomainInvitation,
|
DomainInvitation,
|
||||||
User,
|
User,
|
||||||
UserDomainRole,
|
UserDomainRole,
|
||||||
|
@ -134,6 +136,20 @@ class DomainFormBaseView(DomainBaseView, FormMixin):
|
||||||
# superclass has the redirect
|
# superclass has the redirect
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
def get_domain_info_from_domain(self) -> DomainInformation | None:
|
||||||
|
"""
|
||||||
|
Grabs the underlying domain_info object based off of self.object.name.
|
||||||
|
Returns None if nothing is found.
|
||||||
|
"""
|
||||||
|
_domain_info = DomainInformation.objects.filter(domain__name=self.object.name)
|
||||||
|
current_domain_info = None
|
||||||
|
if _domain_info.exists() and _domain_info.count() == 1:
|
||||||
|
current_domain_info = _domain_info.get()
|
||||||
|
else:
|
||||||
|
logger.error("Could get domain_info. No domain info exists, or duplicates exist.")
|
||||||
|
|
||||||
|
return current_domain_info
|
||||||
|
|
||||||
|
|
||||||
class DomainView(DomainBaseView):
|
class DomainView(DomainBaseView):
|
||||||
"""Domain detail overview page."""
|
"""Domain detail overview page."""
|
||||||
|
@ -217,16 +233,29 @@ class DomainAuthorizingOfficialView(DomainFormBaseView):
|
||||||
"""Add domain_info.authorizing_official instance to make a bound form."""
|
"""Add domain_info.authorizing_official instance to make a bound form."""
|
||||||
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||||
form_kwargs["instance"] = self.object.domain_info.authorizing_official
|
form_kwargs["instance"] = self.object.domain_info.authorizing_official
|
||||||
|
|
||||||
|
domain_info = self.get_domain_info_from_domain()
|
||||||
|
invalid_fields = [DomainRequest.OrganizationChoices.FEDERAL, DomainRequest.OrganizationChoices.TRIBAL]
|
||||||
|
is_federal_or_tribal = domain_info and (domain_info.organization_type in invalid_fields)
|
||||||
|
|
||||||
|
form_kwargs["disable_fields"] = is_federal_or_tribal
|
||||||
return form_kwargs
|
return form_kwargs
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""Adds custom context."""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["organization_type"] = self.object.domain_info.organization_type
|
||||||
|
return context
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
"""Redirect to the overview page for the domain."""
|
"""Redirect to the overview page for the domain."""
|
||||||
return reverse("domain-authorizing-official", kwargs={"pk": self.object.pk})
|
return reverse("domain-authorizing-official", kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""The form is valid, save the authorizing official."""
|
"""The form is valid, save the authorizing official."""
|
||||||
|
|
||||||
# Set the domain information in the form so that it can be accessible
|
# Set the domain information in the form so that it can be accessible
|
||||||
# to associate a new Contact as authorizing official, if new Contact is needed
|
# to associate a new Contact, if a new Contact is needed
|
||||||
# in the save() method
|
# in the save() method
|
||||||
form.set_domain_info(self.object.domain_info)
|
form.set_domain_info(self.object.domain_info)
|
||||||
form.save()
|
form.save()
|
||||||
|
|
32
src/registrar/views/utility/error_views.py
Normal file
32
src/registrar/views/utility/error_views.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
"""
|
||||||
|
Custom views that allow for error view customization.
|
||||||
|
|
||||||
|
Used as a general handler for 500 errors both coming from the registrar app, but
|
||||||
|
also the djangooidc app.
|
||||||
|
|
||||||
|
If Djangooidc is left to its own devices and uses reverse directly,
|
||||||
|
then both context and session information will be obliterated due to:
|
||||||
|
|
||||||
|
a) Djangooidc being out of scope for context_processors
|
||||||
|
b) Potential cyclical import errors restricting what kind of data is passable.
|
||||||
|
|
||||||
|
Rather than dealing with that, we keep everything centralized in one location.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
|
||||||
|
def custom_500_error_view(request, context=None):
|
||||||
|
"""Used to redirect 500 errors to a custom view"""
|
||||||
|
if context is None:
|
||||||
|
return render(request, "500.html", status=500)
|
||||||
|
else:
|
||||||
|
return render(request, "500.html", context=context, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
def custom_401_error_view(request, context=None):
|
||||||
|
"""Used to redirect 401 errors to a custom view"""
|
||||||
|
if context is None:
|
||||||
|
return render(request, "401.html", status=401)
|
||||||
|
else:
|
||||||
|
return render(request, "401.html", context=context, status=401)
|
|
@ -1,8 +1,8 @@
|
||||||
-i https://pypi.python.org/simple
|
-i https://pypi.python.org/simple
|
||||||
annotated-types==0.6.0; python_version >= '3.8'
|
annotated-types==0.6.0; python_version >= '3.8'
|
||||||
asgiref==3.7.2; python_version >= '3.7'
|
asgiref==3.7.2; python_version >= '3.7'
|
||||||
boto3==1.34.54; python_version >= '3.8'
|
boto3==1.34.56; python_version >= '3.8'
|
||||||
botocore==1.34.54; python_version >= '3.8'
|
botocore==1.34.56; python_version >= '3.8'
|
||||||
cachetools==5.3.3; python_version >= '3.7'
|
cachetools==5.3.3; python_version >= '3.7'
|
||||||
certifi==2024.2.2; python_version >= '3.6'
|
certifi==2024.2.2; python_version >= '3.6'
|
||||||
cfenv==0.5.3
|
cfenv==0.5.3
|
||||||
|
@ -22,8 +22,8 @@ django-fsm==2.8.1
|
||||||
django-login-required-middleware==0.9.0
|
django-login-required-middleware==0.9.0
|
||||||
django-phonenumber-field[phonenumberslite]==7.3.0; python_version >= '3.8'
|
django-phonenumber-field[phonenumberslite]==7.3.0; python_version >= '3.8'
|
||||||
django-widget-tweaks==1.5.0; python_version >= '3.8'
|
django-widget-tweaks==1.5.0; python_version >= '3.8'
|
||||||
environs[django]==10.3.0; python_version >= '3.8'
|
environs[django]==11.0.0; python_version >= '3.8'
|
||||||
faker==23.3.0; python_version >= '3.8'
|
faker==24.0.0; python_version >= '3.8'
|
||||||
fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c
|
fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c
|
||||||
furl==2.1.3
|
furl==2.1.3
|
||||||
future==1.0.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
future==1.0.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||||
|
@ -35,7 +35,7 @@ jmespath==1.0.1; python_version >= '3.7'
|
||||||
lxml==5.1.0; python_version >= '3.6'
|
lxml==5.1.0; python_version >= '3.6'
|
||||||
mako==1.3.2; python_version >= '3.8'
|
mako==1.3.2; python_version >= '3.8'
|
||||||
markupsafe==2.1.5; python_version >= '3.7'
|
markupsafe==2.1.5; python_version >= '3.7'
|
||||||
marshmallow==3.21.0; python_version >= '3.8'
|
marshmallow==3.21.1; python_version >= '3.8'
|
||||||
oic==1.6.1; python_version ~= '3.7'
|
oic==1.6.1; python_version ~= '3.7'
|
||||||
orderedmultidict==1.0.1
|
orderedmultidict==1.0.1
|
||||||
packaging==23.2; python_version >= '3.7'
|
packaging==23.2; python_version >= '3.7'
|
||||||
|
@ -49,6 +49,7 @@ pydantic-settings==2.2.1; python_version >= '3.8'
|
||||||
pyjwkest==1.4.2
|
pyjwkest==1.4.2
|
||||||
python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||||
python-dotenv==1.0.1; python_version >= '3.8'
|
python-dotenv==1.0.1; python_version >= '3.8'
|
||||||
|
pyzipper==0.3.6; python_version >= '3.4'
|
||||||
requests==2.31.0; python_version >= '3.7'
|
requests==2.31.0; python_version >= '3.7'
|
||||||
s3transfer==0.10.0; python_version >= '3.8'
|
s3transfer==0.10.0; python_version >= '3.8'
|
||||||
setuptools==69.1.1; python_version >= '3.8'
|
setuptools==69.1.1; python_version >= '3.8'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue