mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-19 19:09:22 +02:00
Merge branch 'main' into za/2047-force-new-users-profile
This commit is contained in:
commit
6c823f1ade
22 changed files with 865 additions and 120 deletions
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
name: Designer Onboarding
|
name: Designer Onboarding
|
||||||
about: Onboarding steps for designers.
|
about: Onboarding steps for new designers joining the .gov team.
|
||||||
title: 'Designer Onboarding: GH_HANDLE'
|
title: 'Designer Onboarding: GH_HANDLE'
|
||||||
labels: design, onboarding
|
labels: design, onboarding
|
||||||
assignees: katherineosos
|
assignees: katherineosos
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
name: Developer Onboarding
|
name: Developer Onboarding
|
||||||
about: Onboarding steps for developers.
|
about: Onboarding steps for new developers joining the .gov team.
|
||||||
title: 'Developer Onboarding: GH_HANDLE'
|
title: 'Developer Onboarding: GH_HANDLE'
|
||||||
labels: dev, onboarding
|
labels: dev, onboarding
|
||||||
assignees: abroddrick
|
assignees: abroddrick
|
||||||
|
|
|
@ -320,9 +320,6 @@ it may help to resync your laptop with time.nist.gov:
|
||||||
sudo sntp -sS time.nist.gov
|
sudo sntp -sS time.nist.gov
|
||||||
```
|
```
|
||||||
|
|
||||||
## Connection pool
|
|
||||||
To handle our connection to the registry, we utilize a connection pool to keep a socket open to increase responsiveness. In order to accomplish this, we are utilizing a heavily modified version of the [geventconnpool](https://github.com/rasky/geventconnpool) library.
|
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
The config for the connection pool exists inside the `settings.py` file.
|
The config for the connection pool exists inside the `settings.py` file.
|
||||||
| Name | Purpose |
|
| Name | Purpose |
|
||||||
|
@ -333,20 +330,6 @@ The config for the connection pool exists inside the `settings.py` file.
|
||||||
|
|
||||||
Consider updating the `POOL_TIMEOUT` or `POOL_KEEP_ALIVE` periods if the pool often restarts. If the pool only restarts after a period of inactivity, update `POOL_KEEP_ALIVE`. If it restarts during the EPP call itself, then `POOL_TIMEOUT` needs to be updated.
|
Consider updating the `POOL_TIMEOUT` or `POOL_KEEP_ALIVE` periods if the pool often restarts. If the pool only restarts after a period of inactivity, update `POOL_KEEP_ALIVE`. If it restarts during the EPP call itself, then `POOL_TIMEOUT` needs to be updated.
|
||||||
|
|
||||||
### Test if the connection pool is running
|
|
||||||
Our connection pool has a built-in `pool_status` object which you can call at anytime to assess the current connection status of the pool. Follow these steps to access it.
|
|
||||||
|
|
||||||
1. `cf ssh getgov-{env-name} -i {instance-index}`
|
|
||||||
* env-name -> Which environment to target, e.g. `staging`
|
|
||||||
* instance-index -> Which instance to target. For instance, `cf ssh getgov-staging -i 0`
|
|
||||||
2. `/tmp/lifecycle/shell`
|
|
||||||
3. `./manage.py shell`
|
|
||||||
4. `from epplibwrapper import CLIENT as registry, commands`
|
|
||||||
5. `print(registry.pool_status.connection_success)`
|
|
||||||
* Should return true
|
|
||||||
|
|
||||||
If you have multiple instances (staging for example), then repeat commands 1-5 for each instance you want to test.
|
|
||||||
|
|
||||||
## Adding a S3 instance to your sandbox
|
## Adding a S3 instance to your sandbox
|
||||||
This can either be done through the CLI, or through the cloud.gov dashboard. Generally, it is better to do it through the dashboard as it handles app binding for you.
|
This can either be done through the CLI, or through the cloud.gov dashboard. Generally, it is better to do it through the dashboard as it handles app binding for you.
|
||||||
|
|
||||||
|
@ -378,4 +361,47 @@ You can view these variables by running the following command:
|
||||||
cf env getgov-{app name}
|
cf env getgov-{app name}
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, copy the variables under the section labled `s3`.
|
Then, copy the variables under the section labled `s3`.
|
||||||
|
|
||||||
|
## Signals
|
||||||
|
The application uses [Django signals](https://docs.djangoproject.com/en/5.0/topics/signals/). In particular, it uses a subset of prebuilt signals called [model signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.db.models.signals).
|
||||||
|
|
||||||
|
Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)"
|
||||||
|
|
||||||
|
In other words, signals are a mechanism that allows different parts of an application to communicate with each other by sending and receiving notifications when events occur. When an event occurs (such as creating, updating, or deleting a record), signals can automatically trigger specific actions in response. This allows different parts of an application to stay synchronized without tightly coupling the component.
|
||||||
|
|
||||||
|
### Rules of use
|
||||||
|
When using signals, try to adhere to these guidelines:
|
||||||
|
1. Don't use signals when you can use another method, such as an override of `save()` or `__init__`.
|
||||||
|
2. Document its usage in this readme (or another centralized location), as well as briefly on the underlying class it is associated with. For instance, since the `handle_profile` directly affects the class `Contact`, the class description notes this and links to [signals.py](../../src/registrar/signals.py).
|
||||||
|
3. Where possible, avoid chaining signals together (i.e. a signal that calls a signal). If this has to be done, clearly document the flow.
|
||||||
|
4. Minimize logic complexity within the signal as much as possible.
|
||||||
|
|
||||||
|
### When should you use signals?
|
||||||
|
Generally, you would use signals when you want an event to be synchronized across multiple areas of code at once (such as with two models or more models at once) in a way that would otherwise be difficult to achieve by overriding functions.
|
||||||
|
|
||||||
|
However, in most scenarios, if you can get away with avoiding signals - you should. The reasoning for this is that [signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch).
|
||||||
|
|
||||||
|
Consider using signals when:
|
||||||
|
1. Synchronizing events across multiple models or areas of code.
|
||||||
|
2. Performing logic before or after saving a model to the database (when otherwise difficult through `save()`).
|
||||||
|
3. Encountering an import loop when overriding functions such as `save()`.
|
||||||
|
4. You are otherwise unable to achieve the intended behavior by overrides or other means.
|
||||||
|
5. (Rare) Offloading tasks when multi-threading.
|
||||||
|
|
||||||
|
For the vast majority of use cases, the [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) and [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) signals are sufficient in terms of model-to-model management.
|
||||||
|
|
||||||
|
### Where should you use them?
|
||||||
|
This project compiles signals in a unified location to maintain readability. If you are adding a signal or otherwise utilizing one, you should always define them in [signals.py](../../src/registrar/signals.py). Except under rare circumstances, this should be adhered to for the reasons mentioned above.
|
||||||
|
|
||||||
|
### How are we currently using signals?
|
||||||
|
At the time of writing, we currently only use signals for the Contact and User objects when synchronizing data returned from Login.gov. This is because the `Contact` object holds information that the user specified in our system, whereas the `User` object holds information that was specified in Login.gov.
|
||||||
|
|
||||||
|
To keep our signal usage coherent and well-documented, add to this document when a new function is added for ease of reference and use.
|
||||||
|
|
||||||
|
#### handle_profile
|
||||||
|
This function is triggered by the post_save event on the User model, designed to manage the synchronization between User and Contact entities. It operates under the following conditions:
|
||||||
|
|
||||||
|
1. For New Users: Upon the creation of a new user, it checks for an existing `Contact` by email. If no matching contact is found, it creates a new Contact using the user's details from Login.gov. If a matching contact is found, it associates this contact with the user. In cases where multiple contacts with the same email exist, it logs a warning and associates the first contact found.
|
||||||
|
|
||||||
|
2. For Existing Users: For users logging in subsequent times, the function ensures that any updates from Login.gov are applied to the associated User record. However, it does not alter any existing Contact records.
|
||||||
|
|
66
docs/operations/import_export.md
Normal file
66
docs/operations/import_export.md
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# Export / Import Tables
|
||||||
|
|
||||||
|
A means is provided to export and import individual tables from
|
||||||
|
one environment to another. This allows for replication of
|
||||||
|
production data in a development environment. Import and export
|
||||||
|
are provided through the django admin interface, through a modified
|
||||||
|
library, django-import-export. Each supported model has an Import
|
||||||
|
and an Export button on the list view.
|
||||||
|
|
||||||
|
### Export
|
||||||
|
|
||||||
|
When exporting models from the source environment, make sure that
|
||||||
|
no filters are selected. This will ensure that all rows of the model
|
||||||
|
are exported. Due to database dependencies, the following models
|
||||||
|
need to be exported:
|
||||||
|
|
||||||
|
* User
|
||||||
|
* Contact
|
||||||
|
* Domain
|
||||||
|
* DomainRequest
|
||||||
|
* DomainInformation
|
||||||
|
* DomainUserRole
|
||||||
|
* DraftDomain
|
||||||
|
* Websites
|
||||||
|
* Host
|
||||||
|
* HostIP
|
||||||
|
|
||||||
|
### Import
|
||||||
|
|
||||||
|
When importing into the target environment, if the target environment
|
||||||
|
is different than the source environment, it must be prepared for the
|
||||||
|
import. This involves clearing out rows in the appropriate tables so
|
||||||
|
that there are no database conflicts on import.
|
||||||
|
|
||||||
|
#### Preparing Target Environment
|
||||||
|
|
||||||
|
Delete all rows from tables in the following order through django admin:
|
||||||
|
|
||||||
|
* DomainInformation
|
||||||
|
* DomainRequest
|
||||||
|
* Domain
|
||||||
|
* User (all but the current user)
|
||||||
|
* Contact
|
||||||
|
* Websites
|
||||||
|
* DraftDomain
|
||||||
|
* HostIP
|
||||||
|
* Host
|
||||||
|
|
||||||
|
#### Importing into Target Environment
|
||||||
|
|
||||||
|
Once target environment is prepared, files can be imported in the following
|
||||||
|
order:
|
||||||
|
|
||||||
|
* User (After importing User table, you need to delete all rows from Contact table before importing Contacts)
|
||||||
|
* Contact
|
||||||
|
* Domain
|
||||||
|
* Host
|
||||||
|
* HostIP
|
||||||
|
* DraftDomain
|
||||||
|
* Websites
|
||||||
|
* DomainRequest
|
||||||
|
* DomainInformation
|
||||||
|
* UserDomainRole
|
||||||
|
|
||||||
|
Optional step:
|
||||||
|
* Run fixtures to load fixture users back in
|
|
@ -32,6 +32,7 @@ fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"}
|
||||||
pyzipper="*"
|
pyzipper="*"
|
||||||
tblib = "*"
|
tblib = "*"
|
||||||
django-admin-multiple-choice-list-filter = "*"
|
django-admin-multiple-choice-list-filter = "*"
|
||||||
|
django-import-export = "*"
|
||||||
django-waffle = "*"
|
django-waffle = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
131
src/Pipfile.lock
generated
131
src/Pipfile.lock
generated
|
@ -272,6 +272,14 @@
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
"version": "==0.7.1"
|
"version": "==0.7.1"
|
||||||
},
|
},
|
||||||
|
"diff-match-patch": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:953019cdb9c9d2c9e47b5b12bcff3cf4746fc4598eb406076fa1fc27e6a1f15c",
|
||||||
|
"sha256:dce43505fb7b1b317de7195579388df0746d90db07015ed47a85e5e44930ef93"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==20230430"
|
||||||
|
},
|
||||||
"dj-database-url": {
|
"dj-database-url": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0",
|
"sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0",
|
||||||
|
@ -352,6 +360,15 @@
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.8.1"
|
"version": "==2.8.1"
|
||||||
},
|
},
|
||||||
|
"django-import-export": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2eac09e8cec8670f36e24314760448011ad23c51e8fb930d55f50d0c3c926da0",
|
||||||
|
"sha256:4deabc557801d368093608c86fd0f4831bc9540e2ea41ca2f023e2efb3eb6f48"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==3.3.8"
|
||||||
|
},
|
||||||
"django-login-required-middleware": {
|
"django-login-required-middleware": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:847ae9a69fd7a07618ed53192b3c06946af70a0caf6d0f4eb40a8f37593cd970"
|
"sha256:847ae9a69fd7a07618ed53192b3c06946af70a0caf6d0f4eb40a8f37593cd970"
|
||||||
|
@ -399,6 +416,14 @@
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==11.0.0"
|
"version": "==11.0.0"
|
||||||
},
|
},
|
||||||
|
"et-xmlfile": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c",
|
||||||
|
"sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
"faker": {
|
"faker": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:87ef41e24b39a5be66ecd874af86f77eebd26782a2681200e86c5326340a95d3",
|
"sha256:87ef41e24b39a5be66ecd874af86f77eebd26782a2681200e86c5326340a95d3",
|
||||||
|
@ -733,6 +758,12 @@
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.3.3"
|
"version": "==1.3.3"
|
||||||
},
|
},
|
||||||
|
"markuppy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1adee2c0a542af378fe84548ff6f6b0168f3cb7f426b46961038a2bcfaad0d5f"
|
||||||
|
],
|
||||||
|
"version": "==1.14"
|
||||||
|
},
|
||||||
"markupsafe": {
|
"markupsafe": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
|
"sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
|
||||||
|
@ -807,6 +838,13 @@
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==3.21.1"
|
"version": "==3.21.1"
|
||||||
},
|
},
|
||||||
|
"odfpy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec",
|
||||||
|
"sha256:fc3b8d1bc098eba4a0fda865a76d9d1e577c4ceec771426bcb169a82c5e9dfe0"
|
||||||
|
],
|
||||||
|
"version": "==1.4.1"
|
||||||
|
},
|
||||||
"oic": {
|
"oic": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b74bd06c7de1ab4f8e798f714062e6a68f68ad9cdbed1f1c30a7fb887602f321",
|
"sha256:b74bd06c7de1ab4f8e798f714062e6a68f68ad9cdbed1f1c30a7fb887602f321",
|
||||||
|
@ -816,6 +854,13 @@
|
||||||
"markers": "python_version ~= '3.8'",
|
"markers": "python_version ~= '3.8'",
|
||||||
"version": "==1.7.0"
|
"version": "==1.7.0"
|
||||||
},
|
},
|
||||||
|
"openpyxl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184",
|
||||||
|
"sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5"
|
||||||
|
],
|
||||||
|
"version": "==3.1.2"
|
||||||
|
},
|
||||||
"orderedmultidict": {
|
"orderedmultidict": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad",
|
"sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad",
|
||||||
|
@ -1088,6 +1133,62 @@
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.0.1"
|
"version": "==1.0.1"
|
||||||
},
|
},
|
||||||
|
"pyyaml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
|
||||||
|
"sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
|
||||||
|
"sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
|
||||||
|
"sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
|
||||||
|
"sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
|
||||||
|
"sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
|
||||||
|
"sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
|
||||||
|
"sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
|
||||||
|
"sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
|
||||||
|
"sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
|
||||||
|
"sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
|
||||||
|
"sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
|
||||||
|
"sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
|
||||||
|
"sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
|
||||||
|
"sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
|
||||||
|
"sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
|
||||||
|
"sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
|
||||||
|
"sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
|
||||||
|
"sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
|
||||||
|
"sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
|
||||||
|
"sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
|
||||||
|
"sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
|
||||||
|
"sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
|
||||||
|
"sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
|
||||||
|
"sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
|
||||||
|
"sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
|
||||||
|
"sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
|
||||||
|
"sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
|
||||||
|
"sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
|
||||||
|
"sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef",
|
||||||
|
"sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
|
||||||
|
"sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
|
||||||
|
"sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
|
||||||
|
"sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
|
||||||
|
"sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
|
||||||
|
"sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
|
||||||
|
"sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
|
||||||
|
"sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
|
||||||
|
"sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
|
||||||
|
"sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
|
||||||
|
"sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
|
||||||
|
"sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
|
||||||
|
"sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
|
||||||
|
"sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
|
||||||
|
"sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
|
||||||
|
"sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
|
||||||
|
"sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
|
||||||
|
"sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
|
||||||
|
"sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
|
||||||
|
"sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
|
||||||
|
"sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
|
||||||
|
],
|
||||||
|
"version": "==6.0.1"
|
||||||
|
},
|
||||||
"pyzipper": {
|
"pyzipper": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0adca90a00c36a93fbe49bfa8c5add452bfe4ef85a1b8e3638739dd1c7b26bfc",
|
"sha256:0adca90a00c36a93fbe49bfa8c5add452bfe4ef85a1b8e3638739dd1c7b26bfc",
|
||||||
|
@ -1138,6 +1239,21 @@
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==0.5.0"
|
"version": "==0.5.0"
|
||||||
},
|
},
|
||||||
|
"tablib": {
|
||||||
|
"extras": [
|
||||||
|
"html",
|
||||||
|
"ods",
|
||||||
|
"xls",
|
||||||
|
"xlsx",
|
||||||
|
"yaml"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9821caa9eca6062ff7299fa645e737aecff982e6b2b42046928a6413c8dabfd9",
|
||||||
|
"sha256:f6661dfc45e1d4f51fa8a6239f9c8349380859a5bfaa73280645f046d6c96e33"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==3.5.0"
|
||||||
|
},
|
||||||
"tblib": {
|
"tblib": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:80a6c77e59b55e83911e1e607c649836a69c103963c5f28a46cbeef44acf8129",
|
"sha256:80a6c77e59b55e83911e1e607c649836a69c103963c5f28a46cbeef44acf8129",
|
||||||
|
@ -1173,6 +1289,20 @@
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==6.6.0"
|
"version": "==6.6.0"
|
||||||
},
|
},
|
||||||
|
"xlrd": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd",
|
||||||
|
"sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88"
|
||||||
|
],
|
||||||
|
"version": "==2.0.1"
|
||||||
|
},
|
||||||
|
"xlwt": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e",
|
||||||
|
"sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88"
|
||||||
|
],
|
||||||
|
"version": "==1.3.0"
|
||||||
|
},
|
||||||
"zope.event": {
|
"zope.event": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26",
|
"sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26",
|
||||||
|
@ -1597,7 +1727,6 @@
|
||||||
"sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
|
"sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
|
||||||
"sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
|
"sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
|
||||||
"version": "==6.0.1"
|
"version": "==6.0.1"
|
||||||
},
|
},
|
||||||
"rich": {
|
"rich": {
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.db.models import Value, CharField, Q
|
||||||
from django.db.models.functions import Concat, Coalesce
|
from django.db.models.functions import Concat, Coalesce
|
||||||
from django.http import HttpResponseRedirect
|
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, FSMField
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
@ -30,12 +30,63 @@ from django.utils.safestring import mark_safe
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.contrib.auth.forms import UserChangeForm, UsernameField
|
from django.contrib.auth.forms import UserChangeForm, UsernameField
|
||||||
from django_admin_multiple_choice_list_filter.list_filters import MultipleChoiceListFilter
|
from django_admin_multiple_choice_list_filter.list_filters import MultipleChoiceListFilter
|
||||||
|
from import_export import resources
|
||||||
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FsmModelResource(resources.ModelResource):
|
||||||
|
"""ModelResource is extended to support importing of tables which
|
||||||
|
have FSMFields. ModelResource is extended with the following changes
|
||||||
|
to existing behavior:
|
||||||
|
When new objects are to be imported, FSMFields are initialized before
|
||||||
|
the object is initialized. This is because FSMFields do not allow
|
||||||
|
direct modification.
|
||||||
|
When objects, which are to be imported, are updated, the FSMFields
|
||||||
|
are skipped."""
|
||||||
|
|
||||||
|
def init_instance(self, row=None):
|
||||||
|
"""Overrides the init_instance method of ModelResource. Returns
|
||||||
|
an instance of the model, with the FSMFields already initialized
|
||||||
|
from data in the row."""
|
||||||
|
|
||||||
|
# Get fields which are fsm fields
|
||||||
|
fsm_fields = {}
|
||||||
|
|
||||||
|
for f in self._meta.model._meta.fields:
|
||||||
|
if isinstance(f, FSMField):
|
||||||
|
if row and f.name in row:
|
||||||
|
fsm_fields[f.name] = row[f.name]
|
||||||
|
|
||||||
|
# Initialize model instance with fsm_fields
|
||||||
|
return self._meta.model(**fsm_fields)
|
||||||
|
|
||||||
|
def import_field(self, field, obj, data, is_m2m=False, **kwargs):
|
||||||
|
"""Overrides the import_field method of ModelResource. If the
|
||||||
|
field being imported is an FSMField, it is not imported."""
|
||||||
|
|
||||||
|
is_fsm = False
|
||||||
|
|
||||||
|
# check each field in the object
|
||||||
|
for f in obj._meta.fields:
|
||||||
|
# if the field is an instance of FSMField
|
||||||
|
if field.attribute == f.name and isinstance(f, FSMField):
|
||||||
|
is_fsm = True
|
||||||
|
if not is_fsm:
|
||||||
|
super().import_field(field, obj, data, is_m2m, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class UserResource(resources.ModelResource):
|
||||||
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
import/export file"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
|
||||||
|
|
||||||
class MyUserAdminForm(UserChangeForm):
|
class MyUserAdminForm(UserChangeForm):
|
||||||
"""This form utilizes the custom widget for its class's ManyToMany UIs.
|
"""This form utilizes the custom widget for its class's ManyToMany UIs.
|
||||||
|
|
||||||
|
@ -498,9 +549,11 @@ class UserContactInline(admin.StackedInline):
|
||||||
model = models.Contact
|
model = models.Contact
|
||||||
|
|
||||||
|
|
||||||
class MyUserAdmin(BaseUserAdmin):
|
class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
"""Custom user admin class to use our inlines."""
|
"""Custom user admin class to use our inlines."""
|
||||||
|
|
||||||
|
resource_classes = [UserResource]
|
||||||
|
|
||||||
form = MyUserAdminForm
|
form = MyUserAdminForm
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -706,17 +759,52 @@ class HostIPInline(admin.StackedInline):
|
||||||
model = models.HostIP
|
model = models.HostIP
|
||||||
|
|
||||||
|
|
||||||
class MyHostAdmin(AuditedAdmin):
|
class HostResource(resources.ModelResource):
|
||||||
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
import/export file"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Host
|
||||||
|
|
||||||
|
|
||||||
|
class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
|
||||||
"""Custom host admin class to use our inlines."""
|
"""Custom host admin class to use our inlines."""
|
||||||
|
|
||||||
|
resource_classes = [HostResource]
|
||||||
|
|
||||||
search_fields = ["name", "domain__name"]
|
search_fields = ["name", "domain__name"]
|
||||||
search_help_text = "Search by domain or host name."
|
search_help_text = "Search by domain or host name."
|
||||||
inlines = [HostIPInline]
|
inlines = [HostIPInline]
|
||||||
|
|
||||||
|
|
||||||
class ContactAdmin(ListHeaderAdmin):
|
class HostIpResource(resources.ModelResource):
|
||||||
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
import/export file"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.HostIP
|
||||||
|
|
||||||
|
|
||||||
|
class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin):
|
||||||
|
"""Custom host ip admin class"""
|
||||||
|
|
||||||
|
resource_classes = [HostIpResource]
|
||||||
|
model = models.HostIP
|
||||||
|
|
||||||
|
|
||||||
|
class ContactResource(resources.ModelResource):
|
||||||
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
import/export file"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Contact
|
||||||
|
|
||||||
|
|
||||||
|
class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"""Custom contact admin class to add search."""
|
"""Custom contact admin class to add search."""
|
||||||
|
|
||||||
|
resource_classes = [ContactResource]
|
||||||
|
|
||||||
search_fields = ["email", "first_name", "last_name"]
|
search_fields = ["email", "first_name", "last_name"]
|
||||||
search_help_text = "Search by first name, last name or email."
|
search_help_text = "Search by first name, last name or email."
|
||||||
list_display = [
|
list_display = [
|
||||||
|
@ -837,9 +925,19 @@ class ContactAdmin(ListHeaderAdmin):
|
||||||
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
class WebsiteAdmin(ListHeaderAdmin):
|
class WebsiteResource(resources.ModelResource):
|
||||||
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
import/export file"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Website
|
||||||
|
|
||||||
|
|
||||||
|
class WebsiteAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"""Custom website admin class."""
|
"""Custom website admin class."""
|
||||||
|
|
||||||
|
resource_classes = [WebsiteResource]
|
||||||
|
|
||||||
# Search
|
# Search
|
||||||
search_fields = [
|
search_fields = [
|
||||||
"website",
|
"website",
|
||||||
|
@ -887,9 +985,19 @@ class WebsiteAdmin(ListHeaderAdmin):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class UserDomainRoleAdmin(ListHeaderAdmin):
|
class UserDomainRoleResource(resources.ModelResource):
|
||||||
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
import/export file"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.UserDomainRole
|
||||||
|
|
||||||
|
|
||||||
|
class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"""Custom user domain role admin class."""
|
"""Custom user domain role admin class."""
|
||||||
|
|
||||||
|
resource_classes = [UserDomainRoleResource]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Contains meta information about this class"""
|
"""Contains meta information about this class"""
|
||||||
|
|
||||||
|
@ -970,9 +1078,19 @@ class DomainInvitationAdmin(ListHeaderAdmin):
|
||||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||||
|
|
||||||
|
|
||||||
class DomainInformationAdmin(ListHeaderAdmin):
|
class DomainInformationResource(resources.ModelResource):
|
||||||
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
import/export file"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.DomainInformation
|
||||||
|
|
||||||
|
|
||||||
|
class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"""Customize domain information admin class."""
|
"""Customize domain information admin class."""
|
||||||
|
|
||||||
|
resource_classes = [DomainInformationResource]
|
||||||
|
|
||||||
form = DomainInformationAdminForm
|
form = DomainInformationAdminForm
|
||||||
|
|
||||||
# Columns
|
# Columns
|
||||||
|
@ -1101,9 +1219,19 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
||||||
return readonly_fields # Read-only fields for analysts
|
return readonly_fields # Read-only fields for analysts
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestAdmin(ListHeaderAdmin):
|
class DomainRequestResource(FsmModelResource):
|
||||||
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
import/export file"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.DomainRequest
|
||||||
|
|
||||||
|
|
||||||
|
class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"""Custom domain requests admin class."""
|
"""Custom domain requests admin class."""
|
||||||
|
|
||||||
|
resource_classes = [DomainRequestResource]
|
||||||
|
|
||||||
form = DomainRequestAdminForm
|
form = DomainRequestAdminForm
|
||||||
change_form_template = "django/admin/domain_request_change_form.html"
|
change_form_template = "django/admin/domain_request_change_form.html"
|
||||||
|
|
||||||
|
@ -1644,9 +1772,19 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
|
return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
|
||||||
|
|
||||||
|
|
||||||
class DomainAdmin(ListHeaderAdmin):
|
class DomainResource(FsmModelResource):
|
||||||
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
import/export file"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Domain
|
||||||
|
|
||||||
|
|
||||||
|
class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"""Custom domain admin class to add extra buttons."""
|
"""Custom domain admin class to add extra buttons."""
|
||||||
|
|
||||||
|
resource_classes = [DomainResource]
|
||||||
|
|
||||||
class ElectionOfficeFilter(admin.SimpleListFilter):
|
class ElectionOfficeFilter(admin.SimpleListFilter):
|
||||||
"""Define a custom filter for is_election_board"""
|
"""Define a custom filter for is_election_board"""
|
||||||
|
|
||||||
|
@ -2041,9 +2179,19 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
return super().has_change_permission(request, obj)
|
return super().has_change_permission(request, obj)
|
||||||
|
|
||||||
|
|
||||||
class DraftDomainAdmin(ListHeaderAdmin):
|
class DraftDomainResource(resources.ModelResource):
|
||||||
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
import/export file"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.DraftDomain
|
||||||
|
|
||||||
|
|
||||||
|
class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"""Custom draft domain admin class."""
|
"""Custom draft domain admin class."""
|
||||||
|
|
||||||
|
resource_classes = [DraftDomainResource]
|
||||||
|
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
search_help_text = "Search by draft domain name."
|
search_help_text = "Search by draft domain name."
|
||||||
|
|
||||||
|
@ -2179,9 +2327,8 @@ admin.site.register(models.DomainInformation, DomainInformationAdmin)
|
||||||
admin.site.register(models.Domain, DomainAdmin)
|
admin.site.register(models.Domain, DomainAdmin)
|
||||||
admin.site.register(models.DraftDomain, DraftDomainAdmin)
|
admin.site.register(models.DraftDomain, DraftDomainAdmin)
|
||||||
admin.site.register(models.FederalAgency, FederalAgencyAdmin)
|
admin.site.register(models.FederalAgency, FederalAgencyAdmin)
|
||||||
# Host and HostIP removed from django admin because changes in admin
|
|
||||||
# do not propagate to registry and logic not applied
|
|
||||||
admin.site.register(models.Host, MyHostAdmin)
|
admin.site.register(models.Host, MyHostAdmin)
|
||||||
|
admin.site.register(models.HostIP, HostIpAdmin)
|
||||||
admin.site.register(models.Website, WebsiteAdmin)
|
admin.site.register(models.Website, WebsiteAdmin)
|
||||||
admin.site.register(models.PublicContact, PublicContactAdmin)
|
admin.site.register(models.PublicContact, PublicContactAdmin)
|
||||||
admin.site.register(models.DomainRequest, DomainRequestAdmin)
|
admin.site.register(models.DomainRequest, DomainRequestAdmin)
|
||||||
|
|
|
@ -717,6 +717,10 @@ div.dja__model-description{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.import_export_text {
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.text-underline {
|
.text-underline {
|
||||||
text-decoration: underline !important;
|
text-decoration: underline !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,6 +147,8 @@ INSTALLED_APPS = [
|
||||||
"corsheaders",
|
"corsheaders",
|
||||||
# library for multiple choice filters in django admin
|
# library for multiple choice filters in django admin
|
||||||
"django_admin_multiple_choice_list_filter",
|
"django_admin_multiple_choice_list_filter",
|
||||||
|
# library for export and import of data
|
||||||
|
"import_export",
|
||||||
# Waffle feature flags
|
# Waffle feature flags
|
||||||
"waffle",
|
"waffle",
|
||||||
]
|
]
|
||||||
|
|
|
@ -97,6 +97,7 @@ class OrganizationElectionForm(RegistrarForm):
|
||||||
|
|
||||||
class OrganizationContactForm(RegistrarForm):
|
class OrganizationContactForm(RegistrarForm):
|
||||||
# for federal agencies we also want to know the top-level agency.
|
# for federal agencies we also want to know the top-level agency.
|
||||||
|
excluded_agencies = ["gov Administration", "Non-Federal Agency"]
|
||||||
federal_agency = forms.ModelChoiceField(
|
federal_agency = forms.ModelChoiceField(
|
||||||
label="Federal agency",
|
label="Federal agency",
|
||||||
# not required because this field won't be filled out unless
|
# not required because this field won't be filled out unless
|
||||||
|
@ -104,9 +105,8 @@ class OrganizationContactForm(RegistrarForm):
|
||||||
# if it has been filled in when required.
|
# if it has been filled in when required.
|
||||||
# uncomment to see if modelChoiceField can be an arg later
|
# uncomment to see if modelChoiceField can be an arg later
|
||||||
required=False,
|
required=False,
|
||||||
queryset=FederalAgency.objects.all(),
|
queryset=FederalAgency.objects.exclude(agency__in=excluded_agencies),
|
||||||
empty_label="--Select--",
|
empty_label="--Select--",
|
||||||
# choices=[("", "--Select--")] + DomainRequest.AGENCY_CHOICES,
|
|
||||||
)
|
)
|
||||||
organization_name = forms.CharField(
|
organization_name = forms.CharField(
|
||||||
label="Organization name",
|
label="Organization name",
|
||||||
|
|
|
@ -5,7 +5,16 @@ from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class Contact(TimeStampedModel):
|
class Contact(TimeStampedModel):
|
||||||
"""Contact information follows a similar pattern for each contact."""
|
"""
|
||||||
|
Contact information follows a similar pattern for each contact.
|
||||||
|
|
||||||
|
This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
|
||||||
|
When a new user is created through Login.gov, a contact object will be created and
|
||||||
|
associated on the `user` field.
|
||||||
|
|
||||||
|
If the `user` object already exists, the underlying user object
|
||||||
|
will be updated if any updates are made to it through Login.gov.
|
||||||
|
"""
|
||||||
|
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
"registrar.User",
|
"registrar.User",
|
||||||
|
|
|
@ -22,6 +22,13 @@ class User(AbstractUser):
|
||||||
"""
|
"""
|
||||||
A custom user model that performs identically to the default user model
|
A custom user model that performs identically to the default user model
|
||||||
but can be customized later.
|
but can be customized later.
|
||||||
|
|
||||||
|
This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
|
||||||
|
When a new user is created through Login.gov, a contact object will be created and
|
||||||
|
associated on the contacts `user` field.
|
||||||
|
|
||||||
|
If the `user` object already exists, said user object
|
||||||
|
will be updated if any updates are made to it through Login.gov.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class VerificationTypeChoices(models.TextChoices):
|
class VerificationTypeChoices(models.TextChoices):
|
||||||
|
|
|
@ -100,36 +100,41 @@ class CreateOrUpdateOrganizationTypeHelper:
|
||||||
# Update the field
|
# Update the field
|
||||||
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
|
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
|
||||||
|
|
||||||
def _handle_existing_instance(self, force_update_when_no_are_changes_found=False):
|
def _handle_existing_instance(self, force_update_when_no_changes_are_found=False):
|
||||||
# == Init variables == #
|
# == Init variables == #
|
||||||
# Instance is already in the database, fetch its current state
|
try:
|
||||||
current_instance = self.sender.objects.get(id=self.instance.id)
|
# Instance is already in the database, fetch its current state
|
||||||
|
current_instance = self.sender.objects.get(id=self.instance.id)
|
||||||
|
|
||||||
# Check the new and old values
|
# Check the new and old values
|
||||||
generic_org_type_changed = self.instance.generic_org_type != current_instance.generic_org_type
|
generic_org_type_changed = self.instance.generic_org_type != current_instance.generic_org_type
|
||||||
is_election_board_changed = self.instance.is_election_board != current_instance.is_election_board
|
is_election_board_changed = self.instance.is_election_board != current_instance.is_election_board
|
||||||
organization_type_changed = self.instance.organization_type != current_instance.organization_type
|
organization_type_changed = self.instance.organization_type != current_instance.organization_type
|
||||||
|
|
||||||
# == Check for invalid conditions before proceeding == #
|
# == Check for invalid conditions before proceeding == #
|
||||||
if organization_type_changed and (generic_org_type_changed or is_election_board_changed):
|
if organization_type_changed and (generic_org_type_changed or is_election_board_changed):
|
||||||
# Since organization type is linked with generic_org_type and election board,
|
# Since organization type is linked with generic_org_type and election board,
|
||||||
# we have to update one or the other, not both.
|
# we have to update one or the other, not both.
|
||||||
# This will not happen in normal flow as it is not possible otherwise.
|
# This will not happen in normal flow as it is not possible otherwise.
|
||||||
raise ValueError("Cannot update organization_type and generic_org_type simultaneously.")
|
raise ValueError("Cannot update organization_type and generic_org_type simultaneously.")
|
||||||
elif not organization_type_changed and (not generic_org_type_changed and not is_election_board_changed):
|
elif not organization_type_changed and (not generic_org_type_changed and not is_election_board_changed):
|
||||||
# No changes found
|
# No changes found
|
||||||
if force_update_when_no_are_changes_found:
|
if force_update_when_no_changes_are_found:
|
||||||
# If we want to force an update anyway, we can treat this record like
|
# If we want to force an update anyway, we can treat this record like
|
||||||
# its a new one in that we check for "None" values rather than changes.
|
# its a new one in that we check for "None" values rather than changes.
|
||||||
self._handle_new_instance()
|
self._handle_new_instance()
|
||||||
else:
|
else:
|
||||||
# == Update the linked values == #
|
# == Update the linked values == #
|
||||||
# Find out which field needs updating
|
# Find out which field needs updating
|
||||||
organization_type_needs_update = generic_org_type_changed or is_election_board_changed
|
organization_type_needs_update = generic_org_type_changed or is_election_board_changed
|
||||||
generic_org_type_needs_update = organization_type_changed
|
generic_org_type_needs_update = organization_type_changed
|
||||||
|
|
||||||
# Update the field
|
# Update the field
|
||||||
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
|
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
|
||||||
|
except self.sender.DoesNotExist:
|
||||||
|
# this exception should only be raised when import_export utility attempts to import
|
||||||
|
# a new row and already has an id
|
||||||
|
pass
|
||||||
|
|
||||||
def _update_fields(self, organization_type_needs_update, generic_org_type_needs_update):
|
def _update_fields(self, organization_type_needs_update, generic_org_type_needs_update):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% load admin_urls %}
|
||||||
|
|
||||||
|
{% if has_import_permission %}
|
||||||
|
{% if not IS_PRODUCTION %}
|
||||||
|
<li><a href='{% url opts|admin_urlname:"import" %}' class="import_link">{% trans "Import" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
191
src/registrar/templates/admin/import_export/import.html
Normal file
191
src/registrar/templates/admin/import_export/import.html
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
{% extends "admin/import_export/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load admin_urls %}
|
||||||
|
{% load import_export_tags %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "import_export/import.css" %}" />{% endblock %}
|
||||||
|
|
||||||
|
{% block extrahead %}{{ block.super }}
|
||||||
|
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
|
||||||
|
{% if confirm_form %}
|
||||||
|
{{ confirm_form.media }}
|
||||||
|
{% else %}
|
||||||
|
{{ form.media }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumbs_last %}
|
||||||
|
{% trans "Import" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% if confirm_form %}
|
||||||
|
{% block confirm_import_form %}
|
||||||
|
<form action="{% url opts|admin_urlname:"process_import" %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ confirm_form.as_p }}
|
||||||
|
<p class="import_export_text">
|
||||||
|
{% trans "Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'" %}
|
||||||
|
</p>
|
||||||
|
<div class="submit-row">
|
||||||
|
<input type="submit" class="default" name="confirm" value="{% trans "Confirm import" %}">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
{% else %}
|
||||||
|
{% block import_form %}
|
||||||
|
<form action="" method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% include "admin/import_export/resource_fields_list.html" with import_or_export="import" %}
|
||||||
|
{% block import_form_additional_info %}{% endblock %}
|
||||||
|
|
||||||
|
{% block form_detail %}
|
||||||
|
<fieldset class="module aligned">
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="form-row">
|
||||||
|
{{ field.errors }}
|
||||||
|
|
||||||
|
{{ field.label_tag }}
|
||||||
|
|
||||||
|
{{ field }}
|
||||||
|
|
||||||
|
{% if field.field.help_text %}
|
||||||
|
<p class="help">{{ field.field.help_text|safe }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</fieldset>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form_submit_button %}
|
||||||
|
<div class="submit-row">
|
||||||
|
<input type="submit" class="default" value="{% trans "Submit" %}">
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if result %}
|
||||||
|
|
||||||
|
{% if result.has_errors %}
|
||||||
|
{% block errors %}
|
||||||
|
<h2>{% trans "Errors" %}</h2>
|
||||||
|
<ul>
|
||||||
|
{% for error in result.base_errors %}
|
||||||
|
<li class="import_export_text">
|
||||||
|
{{ error.error }}
|
||||||
|
<div class="traceback">{{ error.traceback|linebreaks }}</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% for line, errors in result.row_errors %}
|
||||||
|
{% for error in errors %}
|
||||||
|
<li class="import_export_text">
|
||||||
|
{% trans "Line number" %}: {{ line }} - {{ error.error }}
|
||||||
|
<div><code class="import_export_text">{{ error.row.values|join:", " }}</code></div>
|
||||||
|
<div class="traceback">{{ error.traceback|linebreaks }}</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% elif result.has_validation_errors %}
|
||||||
|
|
||||||
|
{% block validation_errors %}
|
||||||
|
<h2>{% trans "Some rows failed to validate" %}</h2>
|
||||||
|
|
||||||
|
<p>{% trans "Please correct these errors in your data where possible, then reupload it using the form above." %}</p>
|
||||||
|
|
||||||
|
<table class="import-preview">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Row" %}</th>
|
||||||
|
<th>{% trans "Errors" %}</th>
|
||||||
|
{% for field in result.diff_headers %}
|
||||||
|
<th>{{ field }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in result.invalid_rows %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ row.number }} </td>
|
||||||
|
<td class="errors">
|
||||||
|
<span class="validation-error-count">{{ row.error_count }}</span>
|
||||||
|
<div class="validation-error-container">
|
||||||
|
<ul class="validation-error-list">
|
||||||
|
{% for field_name, error_list in row.field_specific_errors.items %}
|
||||||
|
<li>
|
||||||
|
<span class="validation-error-field-label">{{ field_name }}</span>
|
||||||
|
<ul>
|
||||||
|
{% for error in error_list %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if row.non_field_specific_errors %}
|
||||||
|
<li>
|
||||||
|
<span class="validation-error-field-label">{% trans "Non field specific" %}</span>
|
||||||
|
<ul>
|
||||||
|
{% for error in row.non_field_specific_errors %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{% for field in row.values %}
|
||||||
|
<td>{{ field }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% block preview %}
|
||||||
|
<h2>{% trans "Preview" %}</h2>
|
||||||
|
|
||||||
|
<table class="import-preview">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
{% for field in result.diff_headers %}
|
||||||
|
<th>{{ field }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% for row in result.valid_rows %}
|
||||||
|
<tr class="{{ row.import_type }}">
|
||||||
|
<td class="import-type">
|
||||||
|
{% if row.import_type == 'new' %}
|
||||||
|
{% trans "New" %}
|
||||||
|
{% elif row.import_type == 'skip' %}
|
||||||
|
{% trans "Skipped" %}
|
||||||
|
{% elif row.import_type == 'delete' %}
|
||||||
|
{% trans "Delete" %}
|
||||||
|
{% elif row.import_type == 'update' %}
|
||||||
|
{% trans "Update" %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% for field in row.diff %}
|
||||||
|
<td>{{ field }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% block fields_help %}
|
||||||
|
<p class="import_export_text">
|
||||||
|
{% if import_or_export == "export" %}
|
||||||
|
{% trans "This exporter will export the following fields: " %}
|
||||||
|
{% elif import_or_export == "import" %}
|
||||||
|
{% trans "This importer will import the following fields: " %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if fields_list|length <= 1 %}
|
||||||
|
<code class="import_export_text">{{ fields_list.0.1|join:", " }}</code>
|
||||||
|
{% else %}
|
||||||
|
<dl>
|
||||||
|
{% for resource, fields in fields_list %}
|
||||||
|
<dt>{{ resource }}</dt>
|
||||||
|
<dd><code class="import_export_text">{{ fields|join:", " }}</code></dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
|
@ -97,6 +97,11 @@ def less_console_noise(output_stream=None):
|
||||||
output_stream.close()
|
output_stream.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_time_aware_date(date=datetime(2023, 11, 1)):
|
||||||
|
"""Returns a time aware date"""
|
||||||
|
return timezone.make_aware(date)
|
||||||
|
|
||||||
|
|
||||||
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"""
|
||||||
|
|
||||||
|
@ -532,11 +537,9 @@ class MockDb(TestCase):
|
||||||
username=username, first_name=first_name, last_name=last_name, email=email
|
username=username, first_name=first_name, last_name=last_name, email=email
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a time-aware current date
|
current_date = get_time_aware_date(datetime(2024, 4, 2))
|
||||||
current_datetime = timezone.now()
|
|
||||||
# Extract the date part
|
|
||||||
current_date = current_datetime.date()
|
|
||||||
# Create start and end dates using timedelta
|
# Create start and end dates using timedelta
|
||||||
|
|
||||||
self.end_date = current_date + timedelta(days=2)
|
self.end_date = current_date + timedelta(days=2)
|
||||||
self.start_date = current_date - timedelta(days=2)
|
self.start_date = current_date - timedelta(days=2)
|
||||||
|
|
||||||
|
@ -544,22 +547,22 @@ class MockDb(TestCase):
|
||||||
self.federal_agency_2, _ = FederalAgency.objects.get_or_create(agency="Armed Forces Retirement Home")
|
self.federal_agency_2, _ = FederalAgency.objects.get_or_create(agency="Armed Forces Retirement Home")
|
||||||
|
|
||||||
self.domain_1, _ = Domain.objects.get_or_create(
|
self.domain_1, _ = Domain.objects.get_or_create(
|
||||||
name="cdomain1.gov", state=Domain.State.READY, first_ready=timezone.now()
|
name="cdomain1.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 2))
|
||||||
)
|
)
|
||||||
self.domain_2, _ = Domain.objects.get_or_create(name="adomain2.gov", state=Domain.State.DNS_NEEDED)
|
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_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(
|
self.domain_5, _ = Domain.objects.get_or_create(
|
||||||
name="bdomain5.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(2023, 11, 1))
|
name="bdomain5.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2023, 11, 1))
|
||||||
)
|
)
|
||||||
self.domain_6, _ = Domain.objects.get_or_create(
|
self.domain_6, _ = Domain.objects.get_or_create(
|
||||||
name="bdomain6.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(1980, 10, 16))
|
name="bdomain6.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(1980, 10, 16))
|
||||||
)
|
)
|
||||||
self.domain_7, _ = Domain.objects.get_or_create(
|
self.domain_7, _ = Domain.objects.get_or_create(
|
||||||
name="xdomain7.gov", state=Domain.State.DELETED, deleted=timezone.now()
|
name="xdomain7.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2024, 4, 2))
|
||||||
)
|
)
|
||||||
self.domain_8, _ = Domain.objects.get_or_create(
|
self.domain_8, _ = Domain.objects.get_or_create(
|
||||||
name="sdomain8.gov", state=Domain.State.DELETED, deleted=timezone.now()
|
name="sdomain8.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2024, 4, 2))
|
||||||
)
|
)
|
||||||
# We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today())
|
# 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()).
|
# and a specific time (using datetime.min.time()).
|
||||||
|
@ -567,19 +570,19 @@ class MockDb(TestCase):
|
||||||
self.domain_9, _ = Domain.objects.get_or_create(
|
self.domain_9, _ = Domain.objects.get_or_create(
|
||||||
name="zdomain9.gov",
|
name="zdomain9.gov",
|
||||||
state=Domain.State.DELETED,
|
state=Domain.State.DELETED,
|
||||||
deleted=timezone.make_aware(datetime.combine(date.today() - timedelta(days=1), datetime.min.time())),
|
deleted=get_time_aware_date(datetime(2024, 4, 1)),
|
||||||
)
|
)
|
||||||
# ready tomorrow
|
# ready tomorrow
|
||||||
self.domain_10, _ = Domain.objects.get_or_create(
|
self.domain_10, _ = Domain.objects.get_or_create(
|
||||||
name="adomain10.gov",
|
name="adomain10.gov",
|
||||||
state=Domain.State.READY,
|
state=Domain.State.READY,
|
||||||
first_ready=timezone.make_aware(datetime.combine(date.today() + timedelta(days=1), datetime.min.time())),
|
first_ready=get_time_aware_date(datetime(2024, 4, 3)),
|
||||||
)
|
)
|
||||||
self.domain_11, _ = Domain.objects.get_or_create(
|
self.domain_11, _ = Domain.objects.get_or_create(
|
||||||
name="cdomain11.gov", state=Domain.State.READY, first_ready=timezone.now()
|
name="cdomain11.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 2))
|
||||||
)
|
)
|
||||||
self.domain_12, _ = Domain.objects.get_or_create(
|
self.domain_12, _ = Domain.objects.get_or_create(
|
||||||
name="zdomain12.gov", state=Domain.State.READY, first_ready=timezone.now()
|
name="zdomain12.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 2))
|
||||||
)
|
)
|
||||||
|
|
||||||
self.domain_information_1, _ = DomainInformation.objects.get_or_create(
|
self.domain_information_1, _ = DomainInformation.objects.get_or_create(
|
||||||
|
@ -716,23 +719,31 @@ class MockDb(TestCase):
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
self.domain_request_1 = completed_domain_request(
|
self.domain_request_1 = completed_domain_request(
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED, name="city1.gov"
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
name="city1.gov",
|
||||||
)
|
)
|
||||||
self.domain_request_2 = completed_domain_request(
|
self.domain_request_2 = completed_domain_request(
|
||||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW, name="city2.gov"
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
name="city2.gov",
|
||||||
)
|
)
|
||||||
self.domain_request_3 = completed_domain_request(
|
self.domain_request_3 = completed_domain_request(
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED, name="city3.gov"
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
name="city3.gov",
|
||||||
)
|
)
|
||||||
self.domain_request_4 = completed_domain_request(
|
self.domain_request_4 = completed_domain_request(
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED, name="city4.gov"
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
name="city4.gov",
|
||||||
)
|
)
|
||||||
self.domain_request_5 = completed_domain_request(
|
self.domain_request_5 = completed_domain_request(
|
||||||
status=DomainRequest.DomainRequestStatus.APPROVED, name="city5.gov"
|
status=DomainRequest.DomainRequestStatus.APPROVED,
|
||||||
|
name="city5.gov",
|
||||||
)
|
)
|
||||||
self.domain_request_3.submit()
|
self.domain_request_3.submit()
|
||||||
self.domain_request_3.save()
|
|
||||||
self.domain_request_4.submit()
|
self.domain_request_4.submit()
|
||||||
|
|
||||||
|
self.domain_request_3.submission_date = get_time_aware_date(datetime(2024, 4, 2))
|
||||||
|
self.domain_request_4.submission_date = get_time_aware_date(datetime(2024, 4, 2))
|
||||||
|
self.domain_request_3.save()
|
||||||
self.domain_request_4.save()
|
self.domain_request_4.save()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
@ -873,6 +884,7 @@ def completed_domain_request(
|
||||||
|
|
||||||
if organization_type:
|
if organization_type:
|
||||||
domain_request_kwargs["organization_type"] = organization_type
|
domain_request_kwargs["organization_type"] = organization_type
|
||||||
|
|
||||||
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
|
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
|
||||||
|
|
||||||
if has_other_contacts:
|
if has_other_contacts:
|
||||||
|
|
|
@ -21,6 +21,7 @@ from registrar.admin import (
|
||||||
MyHostAdmin,
|
MyHostAdmin,
|
||||||
UserDomainRoleAdmin,
|
UserDomainRoleAdmin,
|
||||||
VerifiedByStaffAdmin,
|
VerifiedByStaffAdmin,
|
||||||
|
FsmModelResource,
|
||||||
WebsiteAdmin,
|
WebsiteAdmin,
|
||||||
DraftDomainAdmin,
|
DraftDomainAdmin,
|
||||||
FederalAgencyAdmin,
|
FederalAgencyAdmin,
|
||||||
|
@ -61,7 +62,7 @@ from .common import (
|
||||||
)
|
)
|
||||||
from django.contrib.sessions.backends.db import SessionStore
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from unittest.mock import ANY, call, patch
|
from unittest.mock import ANY, call, patch, Mock
|
||||||
from unittest import skip
|
from unittest import skip
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -71,6 +72,49 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFsmModelResource(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.resource = FsmModelResource()
|
||||||
|
|
||||||
|
def test_init_instance(self):
|
||||||
|
"""Test initializing an instance of a class with a FSM field"""
|
||||||
|
|
||||||
|
# Mock a row with FSMField data
|
||||||
|
row_data = {"state": "ready"}
|
||||||
|
|
||||||
|
self.resource._meta.model = Domain
|
||||||
|
|
||||||
|
instance = self.resource.init_instance(row=row_data)
|
||||||
|
|
||||||
|
# Assert that the instance is initialized correctly
|
||||||
|
self.assertIsInstance(instance, Domain)
|
||||||
|
self.assertEqual(instance.state, "ready")
|
||||||
|
|
||||||
|
def test_import_field(self):
|
||||||
|
"""Test that importing a field does not import FSM field"""
|
||||||
|
|
||||||
|
# Mock a FSMField and a non-FSM-field
|
||||||
|
fsm_field_mock = Mock(attribute="state", column_name="state")
|
||||||
|
field_mock = Mock(attribute="name", column_name="name")
|
||||||
|
# Mock the data
|
||||||
|
data_mock = {"state": "unknown", "name": "test"}
|
||||||
|
# Define a mock Domain
|
||||||
|
obj = Domain(state=Domain.State.UNKNOWN, name="test")
|
||||||
|
|
||||||
|
# Mock the save() method of fields so that we can test if save is called
|
||||||
|
# save() is only supposed to be called for non FSM fields
|
||||||
|
field_mock.save = Mock()
|
||||||
|
fsm_field_mock.save = Mock()
|
||||||
|
|
||||||
|
# Call the method with FSMField and non-FSMField
|
||||||
|
self.resource.import_field(fsm_field_mock, obj, data=data_mock, is_m2m=False)
|
||||||
|
self.resource.import_field(field_mock, obj, data=data_mock, is_m2m=False)
|
||||||
|
|
||||||
|
# Assert that field.save() in super().import_field() is called only for non-FSMField
|
||||||
|
field_mock.save.assert_called_once()
|
||||||
|
fsm_field_mock.save.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
class TestDomainAdmin(MockEppLib, WebTest):
|
class TestDomainAdmin(MockEppLib, WebTest):
|
||||||
# csrf checks do not work with WebTest.
|
# csrf checks do not work with WebTest.
|
||||||
# We disable them here. TODO for another ticket.
|
# We disable them here. TODO for another ticket.
|
||||||
|
@ -600,10 +644,9 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
||||||
domain_request.approve()
|
domain_request.approve()
|
||||||
|
|
||||||
response = self.client.get("/admin/registrar/domain/")
|
response = self.client.get("/admin/registrar/domain/")
|
||||||
|
|
||||||
# There are 4 template references to Federal (4) plus four references in the table
|
# There are 4 template references to Federal (4) plus four references in the table
|
||||||
# for our actual domain_request
|
# for our actual domain_request
|
||||||
self.assertContains(response, "Federal", count=42)
|
self.assertContains(response, "Federal", count=54)
|
||||||
# This may be a bit more robust
|
# This may be a bit more robust
|
||||||
self.assertContains(response, '<td class="field-generic_org_type">Federal</td>', count=1)
|
self.assertContains(response, '<td class="field-generic_org_type">Federal</td>', count=1)
|
||||||
# Now let's make sure the long description does not exist
|
# Now let's make sure the long description does not exist
|
||||||
|
@ -852,6 +895,14 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
||||||
def test_place_and_remove_hold_epp(self):
|
def test_place_and_remove_hold_epp(self):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@override_settings(IS_PRODUCTION=True)
|
||||||
|
def test_prod_only_shows_export(self):
|
||||||
|
"""Test that production environment only displays export"""
|
||||||
|
with less_console_noise():
|
||||||
|
response = self.client.get("/admin/registrar/domain/")
|
||||||
|
self.assertContains(response, ">Export<")
|
||||||
|
self.assertNotContains(response, ">Import<")
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
PublicContact.objects.all().delete()
|
PublicContact.objects.all().delete()
|
||||||
|
@ -1376,7 +1427,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal")
|
response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal")
|
||||||
# There are 2 template references to Federal (4) and two in the results data
|
# There are 2 template references to Federal (4) and two in the results data
|
||||||
# of the request
|
# of the request
|
||||||
self.assertContains(response, "Federal", count=40)
|
self.assertContains(response, "Federal", count=52)
|
||||||
# This may be a bit more robust
|
# This may be a bit more robust
|
||||||
self.assertContains(response, '<td class="field-generic_org_type">Federal</td>', count=1)
|
self.assertContains(response, '<td class="field-generic_org_type">Federal</td>', count=1)
|
||||||
# Now let's make sure the long description does not exist
|
# Now let's make sure the long description does not exist
|
||||||
|
@ -2164,7 +2215,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.assertContains(response, "Yes, select ineligible status")
|
self.assertContains(response, "Yes, select ineligible status")
|
||||||
|
|
||||||
def test_readonly_when_restricted_creator(self):
|
def test_readonly_when_restricted_creator(self):
|
||||||
self.maxDiff = None
|
|
||||||
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)
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
|
|
|
@ -22,9 +22,8 @@ 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 datetime
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from .common import MockDb, MockEppLib, less_console_noise
|
from .common import MockDb, MockEppLib, less_console_noise, get_time_aware_date
|
||||||
|
|
||||||
|
|
||||||
class CsvReportsTest(MockDb):
|
class CsvReportsTest(MockDb):
|
||||||
|
@ -201,9 +200,9 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
def test_export_domains_to_writer_security_emails(self):
|
def test_export_domains_to_writer_security_emails_and_first_ready(self):
|
||||||
"""Test that export_domains_to_writer returns the
|
"""Test that export_domains_to_writer returns the
|
||||||
expected security email"""
|
expected security email and first_ready value"""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# Add security email information
|
# Add security email information
|
||||||
|
@ -215,6 +214,11 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
self.domain_2.security_contact
|
self.domain_2.security_contact
|
||||||
# Invoke setter
|
# Invoke setter
|
||||||
self.domain_3.security_contact
|
self.domain_3.security_contact
|
||||||
|
|
||||||
|
# Add a first ready date on the first domain. Leaving the others blank.
|
||||||
|
self.domain_1.first_ready = get_default_start_date()
|
||||||
|
self.domain_1.save()
|
||||||
|
|
||||||
# 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)
|
||||||
|
@ -231,6 +235,7 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
"Security contact email",
|
"Security contact email",
|
||||||
"Status",
|
"Status",
|
||||||
"Expiration date",
|
"Expiration date",
|
||||||
|
"First ready on",
|
||||||
]
|
]
|
||||||
sort_fields = ["domain__name"]
|
sort_fields = ["domain__name"]
|
||||||
filter_condition = {
|
filter_condition = {
|
||||||
|
@ -240,7 +245,7 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
Domain.State.ON_HOLD,
|
Domain.State.ON_HOLD,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
self.maxDiff = None
|
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv_for_domains(
|
write_csv_for_domains(
|
||||||
writer,
|
writer,
|
||||||
|
@ -259,13 +264,14 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
# sorted alphabetially by domain name
|
# sorted alphabetially by domain name
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Domain type,Agency,Organization name,City,State,AO,"
|
"Domain name,Domain type,Agency,Organization name,City,State,AO,"
|
||||||
"AO email,Security contact email,Status,Expiration date\n"
|
"AO email,Security contact email,Status,Expiration date, First ready on\n"
|
||||||
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
|
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready,(blank),2024-04-03\n"
|
||||||
"adomain2.gov,Interstate,(blank),Dns needed\n"
|
"adomain2.gov,Interstate,(blank),Dns needed,(blank),(blank)\n"
|
||||||
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady\n"
|
"cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank),2024-04-02\n"
|
||||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,security@mail.gov,On hold,2023-11-15\n"
|
"ddomain3.gov,Federal,Armed Forces Retirement Home,security@mail.gov,On hold,2023-11-15,(blank)\n"
|
||||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready\n"
|
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,"
|
||||||
"zdomain12.govInterstateReady\n"
|
"(blank),Ready,(blank),2023-11-01\n"
|
||||||
|
"zdomain12.govInterstateReady,(blank),2024-04-02\n"
|
||||||
)
|
)
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
|
@ -470,18 +476,18 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
# Read the content into a variable
|
# Read the content into a variable
|
||||||
csv_content = csv_file.read()
|
csv_content = csv_file.read()
|
||||||
|
|
||||||
# We expect READY domains first, created between today-2 and today+2, sorted by created_at then name
|
# We expect READY domains first, created between day-2 and day+2, sorted by created_at then name
|
||||||
# and DELETED domains deleted between today-2 and today+2, sorted by deleted then name
|
# and DELETED domains deleted between day-2 and day+2, sorted by deleted then name
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Domain type,Agency,Organization name,City,"
|
"Domain name,Domain type,Agency,Organization name,City,"
|
||||||
"State,Status,Expiration date\n"
|
"State,Status,Expiration date\n"
|
||||||
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,\n"
|
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,(blank)\n"
|
||||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,\n"
|
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n"
|
||||||
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady\n"
|
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n"
|
||||||
"zdomain12.govInterstateReady\n"
|
"zdomain12.govInterstateReady(blank)\n"
|
||||||
"zdomain9.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
|
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank)\n"
|
||||||
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
|
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank)\n"
|
||||||
"xdomain7.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
|
"xdomain7.gov,FederalArmedForcesRetirementHome,Deleted,(blank)\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
|
@ -526,7 +532,7 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
Domain.State.ON_HOLD,
|
Domain.State.ON_HOLD,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
self.maxDiff = None
|
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv_for_domains(
|
write_csv_for_domains(
|
||||||
writer,
|
writer,
|
||||||
|
@ -548,14 +554,14 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
"Organization name,City,State,AO,AO email,"
|
"Organization name,City,State,AO,AO email,"
|
||||||
"Security contact email,Domain manager 1,DM1 status,Domain manager 2,DM2 status,"
|
"Security contact email,Domain manager 1,DM1 status,Domain manager 2,DM2 status,"
|
||||||
"Domain manager 3,DM3 status,Domain manager 4,DM4 status\n"
|
"Domain manager 3,DM3 status,Domain manager 4,DM4 status\n"
|
||||||
"adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,squeaker@rocks.com, I\n"
|
"adomain10.gov,Ready,(blank),Federal,Armed Forces Retirement Home,,,, , ,squeaker@rocks.com, I\n"
|
||||||
"adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com, R,squeaker@rocks.com, I\n"
|
"adomain2.gov,Dns needed,(blank),Interstate,,,,, , , ,meoward@rocks.com, R,squeaker@rocks.com, I\n"
|
||||||
"cdomain11.govReadyFederal-ExecutiveWorldWarICentennialCommissionmeoward@rocks.comR\n"
|
"cdomain11.govReady,(blank),Federal-ExecutiveWorldWarICentennialCommissionmeoward@rocks.comR\n"
|
||||||
"cdomain1.gov,Ready,,Federal - Executive,World War I Centennial Commission,,,"
|
"cdomain1.gov,Ready,(blank),Federal - Executive,World War I Centennial Commission,,,"
|
||||||
", , , ,meoward@rocks.com,R,info@example.com,R,big_lebowski@dude.co,R,"
|
", , , ,meoward@rocks.com,R,info@example.com,R,big_lebowski@dude.co,R,"
|
||||||
"woofwardthethird@rocks.com,I\n"
|
"woofwardthethird@rocks.com,I\n"
|
||||||
"ddomain3.gov,On hold,,Federal,Armed Forces Retirement Home,,,, , , ,,\n"
|
"ddomain3.gov,On hold,(blank),Federal,Armed Forces Retirement Home,,,, , , ,,\n"
|
||||||
"zdomain12.govReadyInterstatemeoward@rocks.comR\n"
|
"zdomain12.gov,Ready,(blank),Interstate,meoward@rocks.com,R\n"
|
||||||
)
|
)
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
|
@ -580,7 +586,7 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
csv_file.seek(0)
|
csv_file.seek(0)
|
||||||
# Read the content into a variable
|
# Read the content into a variable
|
||||||
csv_content = csv_file.read()
|
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.
|
# We expect the READY domain names with the domain managers: Their counts, and listing at end_date.
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"MANAGED DOMAINS COUNTS AT START DATE\n"
|
"MANAGED DOMAINS COUNTS AT START DATE\n"
|
||||||
|
@ -623,7 +629,7 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
csv_file.seek(0)
|
csv_file.seek(0)
|
||||||
# Read the content into a variable
|
# Read the content into a variable
|
||||||
csv_content = csv_file.read()
|
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.
|
# We expect the READY domain names with the domain managers: Their counts, and listing at end_date.
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"UNMANAGED DOMAINS AT START DATE\n"
|
"UNMANAGED DOMAINS AT START DATE\n"
|
||||||
|
@ -698,7 +704,7 @@ 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):
|
||||||
expected_date = timezone.make_aware(datetime(2023, 11, 1))
|
expected_date = get_time_aware_date()
|
||||||
actual_date = get_default_start_date()
|
actual_date = get_default_start_date()
|
||||||
self.assertEqual(actual_date, expected_date)
|
self.assertEqual(actual_date, expected_date)
|
||||||
|
|
||||||
|
|
|
@ -723,6 +723,53 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
|
self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
|
||||||
|
|
||||||
|
def test_federal_agency_dropdown_excludes_expected_values(self):
|
||||||
|
"""The Federal Agency dropdown on a domain request form should not
|
||||||
|
include options for gov Administration and Non-Federal Agency"""
|
||||||
|
intro_page = self.app.get(reverse("domain-request:"))
|
||||||
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
|
# resetting the session key on each new request, thus destroying the concept
|
||||||
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
# and then setting the cookie on each request.
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
|
||||||
|
intro_form = intro_page.forms[0]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
intro_result = intro_form.submit()
|
||||||
|
|
||||||
|
# follow first redirect
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
type_page = intro_result.follow()
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
|
||||||
|
# ---- TYPE PAGE ----
|
||||||
|
type_form = type_page.forms[0]
|
||||||
|
type_form["generic_org_type-generic_org_type"] = "federal"
|
||||||
|
|
||||||
|
# test next button
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
type_result = type_form.submit()
|
||||||
|
|
||||||
|
# ---- FEDERAL BRANCH PAGE ----
|
||||||
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
federal_page = type_result.follow()
|
||||||
|
federal_form = federal_page.forms[0]
|
||||||
|
federal_form["organization_federal-federal_type"] = "executive"
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
federal_result = federal_form.submit()
|
||||||
|
|
||||||
|
# ---- ORG CONTACT PAGE ----
|
||||||
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
org_contact_page = federal_result.follow()
|
||||||
|
|
||||||
|
# gov Administration and Non-Federal Agency should not be federal agency options
|
||||||
|
self.assertNotContains(org_contact_page, "gov Administration")
|
||||||
|
self.assertNotContains(org_contact_page, "Non-Federal Agency")
|
||||||
|
# make sure correct federal agency options still show up
|
||||||
|
self.assertContains(org_contact_page, "General Services Administration")
|
||||||
|
|
||||||
def test_yes_no_contact_form_inits_blank_for_new_domain_request(self):
|
def test_yes_no_contact_form_inits_blank_for_new_domain_request(self):
|
||||||
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
|
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
|
||||||
new domain requests"""
|
new domain requests"""
|
||||||
|
|
|
@ -96,7 +96,8 @@ def parse_row_for_domain(
|
||||||
FIELDS = {
|
FIELDS = {
|
||||||
"Domain name": domain.name,
|
"Domain name": domain.name,
|
||||||
"Status": domain.get_state_display(),
|
"Status": domain.get_state_display(),
|
||||||
"Expiration date": domain.expiration_date,
|
"First ready on": domain.first_ready or "(blank)",
|
||||||
|
"Expiration date": domain.expiration_date or "(blank)",
|
||||||
"Domain type": domain_type,
|
"Domain type": domain_type,
|
||||||
"Agency": domain_info.federal_agency,
|
"Agency": domain_info.federal_agency,
|
||||||
"Organization name": domain_info.organization_name,
|
"Organization name": domain_info.organization_name,
|
||||||
|
@ -106,7 +107,6 @@ def parse_row_for_domain(
|
||||||
"AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ",
|
"AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ",
|
||||||
"Security contact email": security_email,
|
"Security contact email": security_email,
|
||||||
"Created at": domain.created_at,
|
"Created at": domain.created_at,
|
||||||
"First ready": domain.first_ready,
|
|
||||||
"Deleted": domain.deleted,
|
"Deleted": domain.deleted,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -378,13 +378,17 @@ def write_csv_for_requests(
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
|
This maps to the "All domain metadata" button.
|
||||||
|
"""
|
||||||
|
|
||||||
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",
|
||||||
"Status",
|
"Status",
|
||||||
|
"First ready on",
|
||||||
"Expiration date",
|
"Expiration date",
|
||||||
"Domain type",
|
"Domain type",
|
||||||
"Agency",
|
"Agency",
|
||||||
|
|
|
@ -10,6 +10,7 @@ cffi==1.16.0; platform_python_implementation != 'PyPy'
|
||||||
charset-normalizer==3.3.2; python_full_version >= '3.7.0'
|
charset-normalizer==3.3.2; python_full_version >= '3.7.0'
|
||||||
cryptography==42.0.5; python_version >= '3.7'
|
cryptography==42.0.5; python_version >= '3.7'
|
||||||
defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||||
|
diff-match-patch==20230430; python_version >= '3.7'
|
||||||
dj-database-url==2.1.0
|
dj-database-url==2.1.0
|
||||||
dj-email-url==1.0.6
|
dj-email-url==1.0.6
|
||||||
django==4.2.10; python_version >= '3.8'
|
django==4.2.10; python_version >= '3.8'
|
||||||
|
@ -20,11 +21,13 @@ django-cache-url==3.4.5
|
||||||
django-cors-headers==4.3.1; python_version >= '3.8'
|
django-cors-headers==4.3.1; python_version >= '3.8'
|
||||||
django-csp==3.8
|
django-csp==3.8
|
||||||
django-fsm==2.8.1
|
django-fsm==2.8.1
|
||||||
|
django-import-export==3.3.8; python_version >= '3.8'
|
||||||
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-waffle==4.1.0; python_version >= '3.8'
|
django-waffle==4.1.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]==11.0.0; python_version >= '3.8'
|
environs[django]==11.0.0; python_version >= '3.8'
|
||||||
|
et-xmlfile==1.1.0; python_version >= '3.6'
|
||||||
faker==25.0.0; python_version >= '3.8'
|
faker==25.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
|
||||||
|
@ -36,9 +39,12 @@ idna==3.7; python_version >= '3.5'
|
||||||
jmespath==1.0.1; python_version >= '3.7'
|
jmespath==1.0.1; python_version >= '3.7'
|
||||||
lxml==5.2.1; python_version >= '3.6'
|
lxml==5.2.1; python_version >= '3.6'
|
||||||
mako==1.3.3; python_version >= '3.8'
|
mako==1.3.3; python_version >= '3.8'
|
||||||
|
markuppy==1.14
|
||||||
markupsafe==2.1.5; python_version >= '3.7'
|
markupsafe==2.1.5; python_version >= '3.7'
|
||||||
marshmallow==3.21.1; python_version >= '3.8'
|
marshmallow==3.21.1; python_version >= '3.8'
|
||||||
|
odfpy==1.4.1
|
||||||
oic==1.7.0; python_version ~= '3.8'
|
oic==1.7.0; python_version ~= '3.8'
|
||||||
|
openpyxl==3.1.2
|
||||||
orderedmultidict==1.0.1
|
orderedmultidict==1.0.1
|
||||||
packaging==24.0; python_version >= '3.7'
|
packaging==24.0; python_version >= '3.7'
|
||||||
phonenumberslite==8.13.35
|
phonenumberslite==8.13.35
|
||||||
|
@ -51,15 +57,19 @@ 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'
|
||||||
|
pyyaml==6.0.1
|
||||||
pyzipper==0.3.6; python_version >= '3.4'
|
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.1; python_version >= '3.8'
|
s3transfer==0.10.1; python_version >= '3.8'
|
||||||
setuptools==69.5.1; python_version >= '3.8'
|
setuptools==69.5.1; python_version >= '3.8'
|
||||||
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||||
sqlparse==0.5.0; python_version >= '3.8'
|
sqlparse==0.5.0; python_version >= '3.8'
|
||||||
|
tablib[html,ods,xls,xlsx,yaml]==3.5.0; python_version >= '3.8'
|
||||||
tblib==3.0.0; python_version >= '3.8'
|
tblib==3.0.0; python_version >= '3.8'
|
||||||
typing-extensions==4.11.0; python_version >= '3.8'
|
typing-extensions==4.11.0; python_version >= '3.8'
|
||||||
urllib3==2.2.1; python_version >= '3.8'
|
urllib3==2.2.1; python_version >= '3.8'
|
||||||
whitenoise==6.6.0; python_version >= '3.8'
|
whitenoise==6.6.0; python_version >= '3.8'
|
||||||
|
xlrd==2.0.1
|
||||||
|
xlwt==1.3.0
|
||||||
zope.event==5.0; python_version >= '3.7'
|
zope.event==5.0; python_version >= '3.7'
|
||||||
zope.interface==6.3; python_version >= '3.7'
|
zope.interface==6.3; python_version >= '3.7'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue