Merge remote-tracking branch 'origin' into es/1793-link-federal-agency-table

This commit is contained in:
Rebecca Hsieh 2024-05-06 14:32:09 -07:00
commit dfda5b37d4
No known key found for this signature in database
42 changed files with 1601 additions and 164 deletions

View file

@ -0,0 +1,23 @@
# Adding feature flags
Feature flags are booleans (stored in our DB as the `WaffleFlag` object) that programmatically disable/enable "features" (such as DNS hosting) for a specified set of users.
We use [django-waffle](https://waffle.readthedocs.io/en/stable/) for our feature flags. Waffle makes using flags fairly straight forward.
## Adding feature flags through django admin
1. On the app, navigate to `\admin`.
2. Under models, click `Waffle flags`.
3. Click `Add waffle flag`.
4. Add the model as you would normally. Refer to waffle's documentation [regarding attributes](https://waffle.readthedocs.io/en/stable/types/flag.html#flag-attributes) for more information on them.
### Enabling the profile_feature flag
1. On the app, navigate to `\admin`.
2. Under models, click `Waffle flags`.
3. Click the `profile_feature` record. This should exist by default, if not - create one with that name.
4. (Important) Set the field `Everyone` to `Unknown`. This field overrides all other settings when set to anything else.
5. Configure the settings as you see fit.
## Using feature flags as boolean values
Waffle [provides a boolean](https://waffle.readthedocs.io/en/stable/usage/views.html) called `flag_is_active` that you can use as you otherwise would a boolean. This boolean requires a request object and the flag name.
## Using feature flags to disable/enable views
Waffle [provides a decorator](https://waffle.readthedocs.io/en/stable/usage/decorators.html) that you can use to enable/disable views. When disabled, the view will return a 404 if said user tries to navigate to it.

View file

@ -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-waffle = "*"
[dev-packages] [dev-packages]
django-debug-toolbar = "*" django-debug-toolbar = "*"

190
src/Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "16a0db98015509322cf1d27f06fced5b7635057c4eb98921a9419d63d51925ab" "sha256": "9095c4f98f58a9502444584067a63f329d5a5fc4b49454c4e129bda09552d19d"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@ -32,20 +32,20 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:2824e3dd18743ca50e5b10439d20e74647b1416e8a94509cb30beac92d27a18d", "sha256:decf52f8d5d8a1b10c9ff2a0e96ee207ed79e33d2e53fdf0880a5cbef70785e0",
"sha256:b2e5cb5b95efcc881e25a3bc872d7a24e75ff4e76f368138e4baf7b9d6ee3422" "sha256:e836b71d79671270fccac0a4d4c8ec239a6b82ea47c399b64675aa597d0ee63b"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.90" "version": "==1.34.95"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:113cd4c0cb63e13163ccbc2bb13d551be314ba7f8ba5bfab1c51a19ca01aa133", "sha256:6bd76a2eadb42b91fa3528392e981ad5b4dfdee3968fa5b904278acf6cbf15ff",
"sha256:d48f152498e2c60b43ce25b579d26642346a327b6fb2c632d57219e0a4f63392" "sha256:ead5823e0dd6751ece5498cb979fd9abf190e691c8833bcac6876fd6ca261fa7"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.90" "version": "==1.34.95"
}, },
"cachetools": { "cachetools": {
"hashes": [ "hashes": [
@ -370,6 +370,15 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==7.3.0" "version": "==7.3.0"
}, },
"django-waffle": {
"hashes": [
"sha256:5979a2f3dd674ef7086480525b39651fc2045427f6d8e6a614192656d3402c5b",
"sha256:e49d7d461d89f3bd8e53f20efe39310acca8f275c9888495e68e195345bf18b1"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==4.1.0"
},
"django-widget-tweaks": { "django-widget-tweaks": {
"hashes": [ "hashes": [
"sha256:1c2180681ebb994e922c754804c7ffebbe1245014777ac47897a81f57cc629c7", "sha256:1c2180681ebb994e922c754804c7ffebbe1245014777ac47897a81f57cc629c7",
@ -392,12 +401,12 @@
}, },
"faker": { "faker": {
"hashes": [ "hashes": [
"sha256:34b947581c2bced340c39b35f89dbfac4f356932cfff8fe893bde854903f0e6e", "sha256:87ef41e24b39a5be66ecd874af86f77eebd26782a2681200e86c5326340a95d3",
"sha256:adb98e771073a06bdc5d2d6710d8af07ac5da64c8dc2ae3b17bb32319e66fd82" "sha256:e23a2b74888885c3d23a9237bacb823041291c03d609a39acb9ebe6c123f3986"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==24.11.0" "version": "==25.0.0"
}, },
"fred-epplib": { "fred-epplib": {
"git": "https://github.com/cisagov/epplib.git", "git": "https://github.com/cisagov/epplib.git",
@ -588,7 +597,6 @@
"sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b", "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b",
"sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6", "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6",
"sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8", "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8",
"sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5",
"sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306", "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306",
"sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5", "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5",
"sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f", "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f",
@ -801,12 +809,12 @@
}, },
"oic": { "oic": {
"hashes": [ "hashes": [
"sha256:385a1f64bb59519df1e23840530921bf416740240f505ea6d161e331d3d39fad", "sha256:b74bd06c7de1ab4f8e798f714062e6a68f68ad9cdbed1f1c30a7fb887602f321",
"sha256:fcbf948a22e4d4df66f6bf57d327933f32a7b539640d9b42883457634360ba78" "sha256:e51705d0c14c97e9ca594374bfb54269a72c9b489e0e979598344c0189bfcb64"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version ~= '3.7'", "markers": "python_version ~= '3.8'",
"version": "==1.6.1" "version": "==1.7.0"
}, },
"orderedmultidict": { "orderedmultidict": {
"hashes": [ "hashes": [
@ -1244,49 +1252,49 @@
}, },
"black": { "black": {
"hashes": [ "hashes": [
"sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d", "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474",
"sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd", "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1",
"sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33", "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0",
"sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965", "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8",
"sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070", "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96",
"sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397", "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1",
"sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745", "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04",
"sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1", "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021",
"sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665", "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94",
"sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436", "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d",
"sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb", "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c",
"sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e", "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7",
"sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6", "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c",
"sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702", "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc",
"sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8", "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7",
"sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8", "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d",
"sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3", "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c",
"sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad", "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741",
"sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf", "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce",
"sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e", "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb",
"sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641", "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063",
"sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2" "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==24.4.0" "version": "==24.4.2"
}, },
"blinker": { "blinker": {
"hashes": [ "hashes": [
"sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9", "sha256:5f1cdeff423b77c31b89de0565cd03e5275a03028f44b2b15f912632a58cced6",
"sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182" "sha256:da44ec748222dcd0105ef975eed946da197d5bdf8bafb6aa92f5bc89da63fa25"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.7.0" "version": "==1.8.1"
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:2824e3dd18743ca50e5b10439d20e74647b1416e8a94509cb30beac92d27a18d", "sha256:decf52f8d5d8a1b10c9ff2a0e96ee207ed79e33d2e53fdf0880a5cbef70785e0",
"sha256:b2e5cb5b95efcc881e25a3bc872d7a24e75ff4e76f368138e4baf7b9d6ee3422" "sha256:e836b71d79671270fccac0a4d4c8ec239a6b82ea47c399b64675aa597d0ee63b"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.90" "version": "==1.34.95"
}, },
"boto3-mocking": { "boto3-mocking": {
"hashes": [ "hashes": [
@ -1299,28 +1307,28 @@
}, },
"boto3-stubs": { "boto3-stubs": {
"hashes": [ "hashes": [
"sha256:7361f162523168ddcfb3e0cc70e5208e78f95b9f1f2553032036a2b67ab33355", "sha256:412006b27ee707e9b51a084b02ac92b143af8a3b56727582afec2a76ce93c3b6",
"sha256:c82f3db8558e28f766361ba1eea7c77dff735f72fef2a0b9dffaa9c0d9ae76a3" "sha256:4fb5830626de42446c238ca72ca1a53e461281396007fb900edf50ceeb044a10"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.90" "version": "==1.34.95"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:113cd4c0cb63e13163ccbc2bb13d551be314ba7f8ba5bfab1c51a19ca01aa133", "sha256:6bd76a2eadb42b91fa3528392e981ad5b4dfdee3968fa5b904278acf6cbf15ff",
"sha256:d48f152498e2c60b43ce25b579d26642346a327b6fb2c632d57219e0a4f63392" "sha256:ead5823e0dd6751ece5498cb979fd9abf190e691c8833bcac6876fd6ca261fa7"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.90" "version": "==1.34.95"
}, },
"botocore-stubs": { "botocore-stubs": {
"hashes": [ "hashes": [
"sha256:b2d7416b524bce7325aa5fe09bb5e0b6bc9531d4136f4407fa39b6bc58507f34", "sha256:64d80a3467e3b19939e9c2750af33328b3087f8f524998dbdf7ed168227f507d",
"sha256:d9b66542cbb8fbe28eef3c22caf941ae22d36cc1ef55b93fc0b52239457cab57" "sha256:b0345f55babd8b901c53804fc5c326a4a0bd2e23e3b71f9ea5d9f7663466e6ba"
], ],
"markers": "python_version >= '3.8' and python_version < '4.0'", "markers": "python_version >= '3.8' and python_version < '4.0'",
"version": "==1.34.89" "version": "==1.34.94"
}, },
"click": { "click": {
"hashes": [ "hashes": [
@ -1357,20 +1365,20 @@
}, },
"django-stubs": { "django-stubs": {
"hashes": [ "hashes": [
"sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8", "sha256:084484cbe16a6d388e80ec687e46f529d67a232f3befaf55c936b3b476be289d",
"sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b" "sha256:b8a792bee526d6cab31e197cb414ee7fa218abd931a50948c66a80b3a2548621"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==4.2.7" "version": "==5.0.0"
}, },
"django-stubs-ext": { "django-stubs-ext": {
"hashes": [ "hashes": [
"sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c", "sha256:5bacfbb498a206d5938454222b843d81da79ea8b6fcd1a59003f529e775bc115",
"sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3" "sha256:8e1334fdf0c8bff87e25d593b33d4247487338aaed943037826244ff788b56a8"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==4.2.7" "version": "==5.0.0"
}, },
"django-webtest": { "django-webtest": {
"hashes": [ "hashes": [
@ -1423,37 +1431,37 @@
}, },
"mypy": { "mypy": {
"hashes": [ "hashes": [
"sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6", "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061",
"sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913", "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99",
"sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129", "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de",
"sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc", "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a",
"sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974", "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9",
"sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374", "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec",
"sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150", "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1",
"sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03", "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131",
"sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9", "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f",
"sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02", "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821",
"sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89", "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5",
"sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2", "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee",
"sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d", "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e",
"sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3", "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746",
"sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612", "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2",
"sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e", "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0",
"sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3", "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b",
"sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e", "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53",
"sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd", "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30",
"sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04", "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda",
"sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed", "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051",
"sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185", "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2",
"sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf", "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7",
"sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b", "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee",
"sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4", "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727",
"sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f", "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976",
"sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6" "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.9.0" "version": "==1.10.0"
}, },
"mypy-extensions": { "mypy-extensions": {
"hashes": [ "hashes": [
@ -1665,14 +1673,6 @@
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==5.3.0.7" "version": "==5.3.0.7"
}, },
"types-pytz": {
"hashes": [
"sha256:6810c8a1f68f21fdf0f4f374a432487c77645a0ac0b31de4bf4690cf21ad3981",
"sha256:8335d443310e2db7b74e007414e74c4f53b67452c0cb0d228ca359ccfba59659"
],
"markers": "python_version >= '3.8'",
"version": "==2024.1.0.20240417"
},
"types-pyyaml": { "types-pyyaml": {
"hashes": [ "hashes": [
"sha256:a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342", "sha256:a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342",

View file

@ -65,13 +65,31 @@ class OpenIdConnectBackend(ModelBackend):
return user return user
def update_existing_user(self, user, kwargs): def update_existing_user(self, user, kwargs):
"""Update other fields without overwriting first_name and last_name. """
Overwrite first_name and last_name if not empty string""" Update user fields without overwriting certain fields.
Args:
user: User object to be updated.
kwargs: Dictionary containing fields to update and their new values.
Note:
This method updates user fields while preserving the values of 'first_name',
'last_name', and 'phone' fields, unless specific conditions are met.
- 'first_name', 'last_name' or 'phone' will be updated if the provided value is not empty.
"""
fields_to_check = ["first_name", "last_name", "phone"]
# Iterate over fields to update
for key, value in kwargs.items(): for key, value in kwargs.items():
# Check if the key is not first_name or last_name or value is not empty string # Check if the field is not 'first_name', 'last_name', or 'phone',
if key not in ["first_name", "last_name"] or value: # or if it's 'first_name' or 'last_name' or 'phone' and the provided value is not empty
if key not in fields_to_check or (key in fields_to_check and value):
# Update the corresponding attribute of the user object
setattr(user, key, value) setattr(user, key, value)
# Save the user object with the updated fields
user.save() user.save()
def clean_username(self, username): def clean_username(self, username):

View file

@ -50,15 +50,21 @@ class OpenIdConnectBackendTestCase(TestCase):
self.assertEqual(user.email, "john.doe@example.com") self.assertEqual(user.email, "john.doe@example.com")
self.assertEqual(user.phone, "123456789") self.assertEqual(user.phone, "123456789")
def test_authenticate_with_existing_user_no_name(self): def test_authenticate_with_existing_user_with_existing_first_last_phone(self):
"""Test that authenticate updates an existing user if it finds one. """Test that authenticate updates an existing user if it finds one.
For this test, given_name and family_name are not supplied""" For this test, given_name and family_name are not supplied.
The existing user's first and last name are not overwritten.
The existing user's phone number is not overwritten"""
# Create an existing user with the same username and with first and last names # Create an existing user with the same username and with first and last names
existing_user = User.objects.create_user(username="test_user", first_name="John", last_name="Doe") existing_user = User.objects.create_user(
username="test_user", first_name="WillNotBe", last_name="Replaced", phone="9999999999"
)
# Remove given_name and family_name from the input, self.kwargs # Remove given_name and family_name from the input, self.kwargs
self.kwargs.pop("given_name", None) self.kwargs.pop("given_name", None)
self.kwargs.pop("family_name", None) self.kwargs.pop("family_name", None)
self.kwargs.pop("phone", None)
# Ensure that the authenticate method updates the existing user # Ensure that the authenticate method updates the existing user
# and preserves existing first and last names # and preserves existing first and last names
@ -68,16 +74,18 @@ class OpenIdConnectBackendTestCase(TestCase):
self.assertEqual(user, existing_user) # The same user instance should be returned self.assertEqual(user, existing_user) # The same user instance should be returned
# Verify that user fields are correctly updated # Verify that user fields are correctly updated
self.assertEqual(user.first_name, "John") self.assertEqual(user.first_name, "WillNotBe")
self.assertEqual(user.last_name, "Doe") self.assertEqual(user.last_name, "Replaced")
self.assertEqual(user.email, "john.doe@example.com") self.assertEqual(user.email, "john.doe@example.com")
self.assertEqual(user.phone, "123456789") self.assertEqual(user.phone, "9999999999")
def test_authenticate_with_existing_user_different_name(self): def test_authenticate_with_existing_user_different_name_phone(self):
"""Test that authenticate updates an existing user if it finds one. """Test that authenticate updates an existing user if it finds one.
For this test, given_name and family_name are supplied and overwrite""" For this test, given_name and family_name are supplied and overwrite"""
# Create an existing user with the same username and with first and last names # Create an existing user with the same username and with first and last names
existing_user = User.objects.create_user(username="test_user", first_name="WillBe", last_name="Replaced") existing_user = User.objects.create_user(
username="test_user", first_name="WillBe", last_name="Replaced", phone="987654321"
)
# Ensure that the authenticate method updates the existing user # Ensure that the authenticate method updates the existing user
# and preserves existing first and last names # and preserves existing first and last names

View file

@ -15,6 +15,8 @@ from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from dateutil.relativedelta import relativedelta # type: ignore from dateutil.relativedelta import relativedelta # type: ignore
from epplibwrapper.errors import ErrorCode, RegistryError from epplibwrapper.errors import ErrorCode, RegistryError
from waffle.admin import FlagAdmin
from waffle.models import Sample, Switch
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
from registrar.views.utility.mixins import OrderableFieldsMixin from registrar.views.utility.mixins import OrderableFieldsMixin
@ -1013,7 +1015,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
( (
"Show details", "Show details",
{ {
"classes": ["collapse--dotgov"], "classes": ["collapse--dgfieldset"],
"description": "Extends type of organization", "description": "Extends type of organization",
"fields": [ "fields": [
"federal_type", "federal_type",
@ -1037,7 +1039,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
( (
"Show details", "Show details",
{ {
"classes": ["collapse--dotgov"], "classes": ["collapse--dgfieldset"],
"description": "Extends organization name and mailing address", "description": "Extends organization name and mailing address",
"fields": [ "fields": [
"address_line1", "address_line1",
@ -1264,7 +1266,7 @@ class DomainRequestAdmin(ListHeaderAdmin):
( (
"Show details", "Show details",
{ {
"classes": ["collapse--dotgov"], "classes": ["collapse--dgfieldset"],
"description": "Extends type of organization", "description": "Extends type of organization",
"fields": [ "fields": [
"federal_type", "federal_type",
@ -1288,7 +1290,7 @@ class DomainRequestAdmin(ListHeaderAdmin):
( (
"Show details", "Show details",
{ {
"classes": ["collapse--dotgov"], "classes": ["collapse--dgfieldset"],
"description": "Extends organization name and mailing address", "description": "Extends organization name and mailing address",
"fields": [ "fields": [
"address_line1", "address_line1",
@ -2154,7 +2156,16 @@ class UserGroupAdmin(AuditedAdmin):
return obj.name return obj.name
class WaffleFlagAdmin(FlagAdmin):
class Meta:
"""Contains meta information about this class"""
model = models.WaffleFlag
fields = "__all__"
admin.site.unregister(LogEntry) # Unregister the default registration admin.site.unregister(LogEntry) # Unregister the default registration
admin.site.register(LogEntry, CustomLogEntryAdmin) admin.site.register(LogEntry, CustomLogEntryAdmin)
admin.site.register(models.User, MyUserAdmin) admin.site.register(models.User, MyUserAdmin)
# Unregister the built-in Group model # Unregister the built-in Group model
@ -2176,3 +2187,10 @@ admin.site.register(models.PublicContact, PublicContactAdmin)
admin.site.register(models.DomainRequest, DomainRequestAdmin) admin.site.register(models.DomainRequest, DomainRequestAdmin)
admin.site.register(models.TransitionDomain, TransitionDomainAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
# Register our custom waffle implementations
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)
# Unregister Sample and Switch from the waffle library
admin.site.unregister(Sample)
admin.site.unregister(Switch)

View file

@ -0,0 +1,76 @@
/*
* We will run our own version of
* https://github.com/django/django/blob/195d885ca01b14e3ce9a1881c3b8f7074f953736/django/contrib/admin/static/admin/js/collapse.js
* Works with our fieldset override
*/
'use strict';
{
window.addEventListener('load', function() {
// Add anchor tag for Show/Hide link
const fieldsets = document.querySelectorAll('fieldset.collapse--dgfieldset');
for (const [i, elem] of fieldsets.entries()) {
// Don't hide if fields in this fieldset have errors
if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) {
elem.classList.add('collapsed');
const button = elem.querySelector('button');
button.id = 'fieldsetcollapser' + i;
button.className = 'collapse-toggle--dgfieldset usa-button usa-button--unstyled';
}
}
// Add toggle to hide/show anchor tag
const toggleFuncDotgov = function(e) {
e.preventDefault();
e.stopPropagation();
const fieldset = this.closest('fieldset');
const spanElement = this.querySelector('span');
const useElement = this.querySelector('use');
if (fieldset.classList.contains('collapsed')) {
// Show
spanElement.textContent = 'Hide details';
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
fieldset.classList.remove('collapsed');
} else {
// Hide
spanElement.textContent = 'Show details';
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
fieldset.classList.add('collapsed');
}
};
document.querySelectorAll('.collapse-toggle--dgfieldset').forEach(function(el) {
el.addEventListener('click', toggleFuncDotgov);
});
});
}
'use strict';
{
window.addEventListener('load', function() {
// Add anchor tag for Show/Hide link
const collapsibleContent = document.querySelectorAll('fieldset.collapse--dgsimple');
for (const [i, elem] of collapsibleContent.entries()) {
const button = elem.closest('div').querySelector('button');
button.id = 'simplecollapser' + i;
}
// Add toggle to hide/show anchor tag
const toggleFuncDotgovSimple = function(e) {
const fieldset = this.closest('div').querySelector('.collapse--dgsimple');
const spanElement = this.querySelector('span');
const useElement = this.querySelector('use');
if (fieldset.classList.contains('collapsed')) {
// Show
spanElement.textContent = 'Hide details';
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
fieldset.classList.remove('collapsed');
} else {
// Hide
spanElement.textContent = 'Show details';
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
fieldset.classList.add('collapsed');
}
};
document.querySelectorAll('.collapse-toggle--dgsimple').forEach(function(el) {
el.addEventListener('click', toggleFuncDotgovSimple);
});
});
}

View file

@ -394,3 +394,38 @@ function initializeWidgetOnList(list, parentId) {
observer.observe(targetElement); observer.observe(targetElement);
} }
})(); })();
/** An IIFE for toggling the overflow styles on django-admin__model-description (the show more / show less button) */
(function () {
function handleShowMoreButton(toggleButton, descriptionDiv){
// Check the length of the text content in the description div
if (descriptionDiv.textContent.length < 200) {
// Hide the toggle button if text content is less than 200 characters
// This is a little over 160 characters to give us some wiggle room if we
// change the font size marginally.
toggleButton.classList.add('display-none');
} else {
toggleButton.addEventListener('click', function() {
toggleShowMoreButton(toggleButton, descriptionDiv, 'dja__model-description--no-overflow')
});
}
}
function toggleShowMoreButton(toggleButton, descriptionDiv, showMoreClassName){
// Toggle the class on the description div
descriptionDiv.classList.toggle(showMoreClassName);
// Change the button text based on the presence of the class
if (descriptionDiv.classList.contains(showMoreClassName)) {
toggleButton.textContent = 'Show less';
} else {
toggleButton.textContent = 'Show more';
}
}
let toggleButton = document.getElementById('dja-show-more-model-description');
let descriptionDiv = document.querySelector('.dja__model-description');
if (toggleButton && descriptionDiv) {
handleShowMoreButton(toggleButton, descriptionDiv)
}
})();

View file

@ -112,12 +112,20 @@ html[data-theme="light"] {
.change-list .usa-table--borderless thead th, .change-list .usa-table--borderless thead th,
.change-list .usa-table thead td, .change-list .usa-table thead td,
.change-list .usa-table thead th, .change-list .usa-table thead th,
.change-form .usa-table,
.change-form .usa-table--striped tbody tr:nth-child(odd) td,
.change-form .usa-table--borderless thead th,
.change-form .usa-table thead td,
.change-form .usa-table thead th,
body.dashboard, body.dashboard,
body.change-list, body.change-list,
body.change-form, body.change-form,
.analytics { .analytics {
color: var(--body-fg); color: var(--body-fg);
} }
.usa-table td {
background-color: transparent;
}
} }
// Firefox needs this to be specifically set // Firefox needs this to be specifically set
@ -127,11 +135,20 @@ html[data-theme="dark"] {
.change-list .usa-table--borderless thead th, .change-list .usa-table--borderless thead th,
.change-list .usa-table thead td, .change-list .usa-table thead td,
.change-list .usa-table thead th, .change-list .usa-table thead th,
.change-form .usa-table,
.change-form .usa-table--striped tbody tr:nth-child(odd) td,
.change-form .usa-table--borderless thead th,
.change-form .usa-table thead td,
.change-form .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);
} }
.usa-table td {
background-color: transparent;
}
} }
#branding h1 a:link, #branding h1 a:visited { #branding h1 a:link, #branding h1 a:visited {
@ -226,7 +243,7 @@ div#content > h2 {
// in the future // in the future
.object-tools li a, .object-tools li a,
.object-tools p a { .object-tools p a {
font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-family: family('sans');
text-transform: none!important; text-transform: none!important;
font-size: 14px!important; font-size: 14px!important;
} }
@ -276,7 +293,7 @@ div#content > h2 {
.messagelist_content-list--unstyled { .messagelist_content-list--unstyled {
padding-left: 0; padding-left: 0;
li { li {
font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-family: family('sans');
font-size: 13.92px!important; font-size: 13.92px!important;
background: none!important; background: none!important;
padding: 0!important; padding: 0!important;
@ -378,7 +395,6 @@ details.dja-detail-table {
border-top: none; border-top: none;
border-bottom: none; border-bottom: none;
} }
} }
@ -525,32 +541,42 @@ address.dja-address-contact-list {
} }
// Collapse button styles for fieldsets // Collapse button styles for fieldsets
.module.collapse--dotgov { .module.collapse--dgfieldset {
margin-top: -35px; margin-top: -35px;
padding-top: 0; padding-top: 0;
border: none; border: none;
button { }
background: none; .collapse-toggle--dgsimple,
text-transform: none; .module.collapse--dgfieldset button {
background: none;
text-transform: none;
color: var(--link-fg);
margin-top: 8px;
margin-left: 10px;
span {
text-decoration: underline;
font-size: 13px;
font-feature-settings: "kern";
font-kerning: normal;
line-height: 13px;
font-family: family('sans');
}
&:hover {
color: var(--link-fg); color: var(--link-fg);
margin-top: 8px; svg {
margin-left: 10px; color: var(--link-fg);
span {
text-decoration: underline;
font-size: 13px;
font-feature-settings: "kern";
font-kerning: normal;
line-height: 13px;
font-family: -apple-system, "system-ui", "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
} }
} }
} }
.collapse--dotgov.collapsed .collapse-toggle--dotgov { .collapse--dgfieldset.collapsed .collapse-toggle--dgfieldset {
display: inline-block!important; display: inline-block!important;
* { * {
display: inline-block; display: inline-block;
} }
} }
.collapse--dgsimple.collapsed {
display: none;
}
.dja-status-list { .dja-status-list {
border-top: solid 1px var(--border-color); border-top: solid 1px var(--border-color);
@ -559,7 +585,7 @@ address.dja-address-contact-list {
padding-top: 10px; padding-top: 10px;
li { li {
line-height: 1.5; line-height: 1.5;
font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif !important; font-family: family('sans');
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
} }
@ -616,13 +642,16 @@ address.dja-address-contact-list {
display: inline-flex; display: inline-flex;
padding-top: 4px; padding-top: 4px;
line-height: 14px; line-height: 14px;
color: var(--link-fg);
width: max-content; width: max-content;
font-size: unset; font-size: unset;
text-decoration: none !important; text-decoration: none !important;
} }
} }
button.usa-button__clipboard {
color: var(--link-fg);
}
.no-outline-on-click:focus { .no-outline-on-click:focus {
outline: none !important; outline: none !important;
} }
@ -659,3 +688,35 @@ form .aligned p.help, form .aligned div.help {
background: var(--primary); background: var(--primary);
color: var(--header-link-color); color: var(--header-link-color);
} }
div.dja__model-description{
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
p, li {
font-size: medium;
color: var(--secondary);
}
li {
list-style-type: disc;
font-family: Source Sans Pro Web,Helvetica Neue,Helvetica,Roboto,Arial,sans-serif;
}
a, a:link, a:visited {
font-size: medium;
color: #005288 !important;
}
&.dja__model-description--no-overflow {
display: block;
overflow: auto;
}
}
.text-underline {
text-decoration: underline !important;
}

View file

@ -22,7 +22,6 @@ from base64 import b64decode
from cfenv import AppEnv # type: ignore from cfenv import AppEnv # type: ignore
from pathlib import Path from pathlib import Path
from typing import Final from typing import Final
from botocore.config import Config from botocore.config import Config
# # # ### # # # ###
@ -148,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",
# Waffle feature flags
"waffle",
] ]
# Middleware are routines for processing web requests. # Middleware are routines for processing web requests.
@ -183,6 +184,8 @@ MIDDLEWARE = [
"csp.middleware.CSPMiddleware", "csp.middleware.CSPMiddleware",
# django-auditlog: obtain the request User for use in logging # django-auditlog: obtain the request User for use in logging
"auditlog.middleware.AuditlogMiddleware", "auditlog.middleware.AuditlogMiddleware",
# Used for waffle feature flags
"waffle.middleware.WaffleMiddleware",
] ]
# application object used by Djangos built-in servers (e.g. `runserver`) # application object used by Djangos built-in servers (e.g. `runserver`)
@ -319,6 +322,17 @@ EMAIL_TIMEOUT = 30
SERVER_EMAIL = "root@get.gov" SERVER_EMAIL = "root@get.gov"
# endregion # endregion
# region: Waffle feature flags-----------------------------------------------------------###
# If Waffle encounters a reference to a flag that is not in the database, should Waffle create the flag?
WAFFLE_CREATE_MISSING_FLAGS = True
# The model that will be used to keep track of flags. Extends AbstractUserFlag.
# Used to replace the default flag class (for customization purposes).
WAFFLE_FLAG_MODEL = "registrar.WaffleFlag"
# endregion
# region: Headers-----------------------------------------------------------### # region: Headers-----------------------------------------------------------###
# Content-Security-Policy configuration # Content-Security-Policy configuration

View file

@ -196,12 +196,12 @@ class UserFixture:
}, },
] ]
def load_users(cls, users, group_name): def load_users(cls, users, group_name, are_superusers=False):
logger.info(f"Going to load {len(users)} users in group {group_name}") logger.info(f"Going to load {len(users)} users in group {group_name}")
for user_data in users: for user_data in users:
try: try:
user, _ = User.objects.get_or_create(username=user_data["username"]) user, _ = User.objects.get_or_create(username=user_data["username"])
user.is_superuser = False user.is_superuser = are_superusers
user.first_name = user_data["first_name"] user.first_name = user_data["first_name"]
user.last_name = user_data["last_name"] user.last_name = user_data["last_name"]
if "email" in user_data: if "email" in user_data:
@ -229,5 +229,5 @@ class UserFixture:
# steps now do not need to close/reopen a db connection, # steps now do not need to close/reopen a db connection,
# instead they share one. # instead they share one.
with transaction.atomic(): with transaction.atomic():
cls.load_users(cls, cls.ADMINS, "full_access_group") cls.load_users(cls, cls.ADMINS, "full_access_group", are_superusers=True)
cls.load_users(cls, cls.STAFF, "cisa_analysts_group") cls.load_users(cls, cls.STAFF, "cisa_analysts_group")

View file

@ -0,0 +1,127 @@
# Generated by Django 4.2.10 on 2024-05-02 17:47
from django.conf import settings
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("registrar", "0089_user_verification_type"),
]
operations = [
migrations.CreateModel(
name="WaffleFlag",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
(
"name",
models.CharField(
help_text="The human/computer readable name.", max_length=100, unique=True, verbose_name="Name"
),
),
(
"everyone",
models.BooleanField(
blank=True,
help_text="Flip this flag on (Yes) or off (No) for everyone, overriding all other settings. Leave as Unknown to use normally.",
null=True,
verbose_name="Everyone",
),
),
(
"percent",
models.DecimalField(
blank=True,
decimal_places=1,
help_text="A number between 0.0 and 99.9 to indicate a percentage of users for whom this flag will be active.",
max_digits=3,
null=True,
verbose_name="Percent",
),
),
(
"testing",
models.BooleanField(
default=False,
help_text="Allow this flag to be set for a session for user testing",
verbose_name="Testing",
),
),
(
"superusers",
models.BooleanField(
default=True, help_text="Flag always active for superusers?", verbose_name="Superusers"
),
),
(
"staff",
models.BooleanField(default=False, help_text="Flag always active for staff?", verbose_name="Staff"),
),
(
"authenticated",
models.BooleanField(
default=False,
help_text="Flag always active for authenticated users?",
verbose_name="Authenticated",
),
),
(
"languages",
models.TextField(
blank=True,
default="",
help_text="Activate this flag for users with one of these languages (comma-separated list)",
verbose_name="Languages",
),
),
(
"rollout",
models.BooleanField(default=False, help_text="Activate roll-out mode?", verbose_name="Rollout"),
),
("note", models.TextField(blank=True, help_text="Note where this Flag is used.", verbose_name="Note")),
(
"created",
models.DateTimeField(
db_index=True,
default=django.utils.timezone.now,
help_text="Date when this Flag was created.",
verbose_name="Created",
),
),
(
"modified",
models.DateTimeField(
default=django.utils.timezone.now,
help_text="Date when this Flag was last modified.",
verbose_name="Modified",
),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="Activate this flag for these user groups.",
to="auth.group",
verbose_name="Groups",
),
),
(
"users",
models.ManyToManyField(
blank=True,
help_text="Activate this flag for these users.",
to=settings.AUTH_USER_MODEL,
verbose_name="Users",
),
),
],
options={
"verbose_name": "waffle flag",
"verbose_name_plural": "Waffle flags",
},
),
]

View file

@ -15,6 +15,8 @@ from .user_group import UserGroup
from .website import Website from .website import Website
from .transition_domain import TransitionDomain from .transition_domain import TransitionDomain
from .verified_by_staff import VerifiedByStaff from .verified_by_staff import VerifiedByStaff
from .waffle_flag import WaffleFlag
__all__ = [ __all__ = [
"Contact", "Contact",
@ -33,6 +35,7 @@ __all__ = [
"Website", "Website",
"TransitionDomain", "TransitionDomain",
"VerifiedByStaff", "VerifiedByStaff",
"WaffleFlag",
] ]
auditlog.register(Contact) auditlog.register(Contact)
@ -51,3 +54,4 @@ auditlog.register(UserGroup, m2m_fields=["permissions"])
auditlog.register(Website) auditlog.register(Website)
auditlog.register(TransitionDomain) auditlog.register(TransitionDomain)
auditlog.register(VerifiedByStaff) auditlog.register(VerifiedByStaff)
auditlog.register(WaffleFlag)

View file

@ -101,11 +101,23 @@ class Contact(TimeStampedModel):
# Call the parent class's save method to perform the actual save # Call the parent class's save method to perform the actual save
super().save(*args, **kwargs) super().save(*args, **kwargs)
# Update the related User object's first_name and last_name if self.user:
if self.user and (not self.user.first_name or not self.user.last_name): updated = False
self.user.first_name = self.first_name
self.user.last_name = self.last_name # Update first name and last name if necessary
self.user.save() if not self.user.first_name or not self.user.last_name:
self.user.first_name = self.first_name
self.user.last_name = self.last_name
updated = True
# Update phone if necessary
if not self.user.phone:
self.user.phone = self.phone
updated = True
# Save user if any updates were made
if updated:
self.user.save()
def __str__(self): def __str__(self):
if self.first_name or self.last_name: if self.first_name or self.last_name:

View file

@ -16,12 +16,21 @@ from .utility.time_stamped_model import TimeStampedModel
from ..utility.email import send_templated_email, EmailSendingError from ..utility.email import send_templated_email, EmailSendingError
from itertools import chain from itertools import chain
from auditlog.models import AuditlogHistoryField # type: ignore
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DomainRequest(TimeStampedModel): class DomainRequest(TimeStampedModel):
"""A registrant's domain request for a new domain.""" """A registrant's domain request for a new domain."""
# https://django-auditlog.readthedocs.io/en/latest/usage.html#object-history
# If we note any performace degradation due to this addition,
# we can query the auditlogs table in admin.py and add the results to
# extra_context in the change_view method for DomainRequestAdmin.
# This is the more straightforward way so trying it first.
history = AuditlogHistoryField()
# Constants for choice fields # Constants for choice fields
class DomainRequestStatus(models.TextChoices): class DomainRequestStatus(models.TextChoices):
STARTED = "started", "Started" STARTED = "started", "Started"

View file

@ -0,0 +1,19 @@
from waffle.models import AbstractUserFlag
import logging
logger = logging.getLogger(__name__)
class WaffleFlag(AbstractUserFlag):
"""
Custom implementation of django-waffles 'Flag' object.
Read more here: https://waffle.readthedocs.io/en/stable/types/flag.html
Use this class when dealing with feature flags, such as profile_feature.
"""
class Meta:
"""Contains meta information about this class"""
verbose_name = "waffle flag"
verbose_name_plural = "Waffle flags"

View file

@ -2,6 +2,10 @@
{% block content_title %} {% block content_title %}
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
{# Adds a model description #}
{% include "admin/model_descriptions.html" %}
<h2> <h2>
{{ cl.result_count }} {{ cl.result_count }}
{% if cl.get_ordering_field_columns %} {% if cl.get_ordering_field_columns %}

View file

@ -8,7 +8,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
<fieldset class="module aligned {{ fieldset.classes }}"> <fieldset class="module aligned {{ fieldset.classes }}">
{% if fieldset.name %} {% if fieldset.name %}
{# Customize the markup for the collapse toggle #} {# Customize the markup for the collapse toggle #}
{% if 'collapse--dotgov' in fieldset.classes %} {% if 'collapse--dgfieldset' in fieldset.classes %}
<button type="button"> <button type="button">
<span>{{ fieldset.name }}</span> <span>{{ fieldset.name }}</span>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24"> <svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
@ -22,7 +22,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
{% endif %} {% endif %}
{# Customize the markup for the collapse toggle: Do not show a description for the collapse fieldsets, instead we're using the description as a screen reader only legend #} {# Customize the markup for the collapse toggle: Do not show a description for the collapse fieldsets, instead we're using the description as a screen reader only legend #}
{% if fieldset.description and 'collapse--dotgov' not in fieldset.classes %} {% if fieldset.description and 'collapse--dgfieldset' not in fieldset.classes %}
<div class="description">{{ fieldset.description|safe }}</div> <div class="description">{{ fieldset.description|safe }}</div>
{% endif %} {% endif %}

View file

@ -0,0 +1,37 @@
<div class="dja__model-description text-normal">
{% if opts.model_name == 'domainrequest' %}
{% include "django/admin/includes/descriptions/domain_request_description.html" %}
{% elif opts.model_name == 'domaininformation' %}
{% include "django/admin/includes/descriptions/domain_information_description.html" %}
{% elif opts.model_name == 'domaininvitation' %}
{% include "django/admin/includes/descriptions/domain_invitation_description.html" %}
{% elif opts.model_name == 'contact' %}
{% include "django/admin/includes/descriptions/contact_description.html" %}
{% elif opts.model_name == 'logentry' %}
{% include "django/admin/includes/descriptions/logentry_description.html" %}
{% elif opts.model_name == 'domain'%}
{% include "django/admin/includes/descriptions/domain_description.html" %}
{% elif opts.model_name == 'draftdomain' %}
{% include "django/admin/includes/descriptions/draft_domain_description.html" %}
{% elif opts.model_name == 'host' %}
{% include "django/admin/includes/descriptions/host_description.html" %}
{% elif opts.model_name == 'publiccontact' %}
{% include "django/admin/includes/descriptions/public_contact_description.html" %}
{% elif opts.model_name == 'transitiondomain' %}
{% include "django/admin/includes/descriptions/transition_domain_description.html" %}
{% elif opts.model_name == 'userdomainrole' %}
{% include "django/admin/includes/descriptions/user_domain_role_description.html" %}
{% elif opts.model_name == 'usergroup' %}
{% include "django/admin/includes/descriptions/user_group_description.html" %}
{% elif opts.model_name == 'user' %}
{% include "django/admin/includes/descriptions/user_description.html" %}
{% elif opts.model_name == 'verifiedbystaff' %}
{% include "django/admin/includes/descriptions/verified_by_staff_description.html" %}
{% elif opts.model_name == 'website' %}
{% include "django/admin/includes/descriptions/website_description.html" %}
{% else %}
<p>This table does not have a description yet.</p>
{% endif %}
</div>
<button id="dja-show-more-model-description" class="usa-button usa-button--unstyled text-no-underline margin-top-1" type="button">Show more</button>

View file

@ -0,0 +1,10 @@
<p>
Contacts include anyone who has access to the registrar (known as “users”) and anyone listed in a domain request,
including other employees and authorizing officials.
Only contacts who have access to the registrar will have
a corresponding record within the <a class="text-underline" href="{% url 'admin:registrar_user_changelist' %}">Users</a> table.
</p>
<p>
Updating someones contact information here will not affect that persons Login.gov information.
</p>

View file

@ -0,0 +1,25 @@
<p>
This table contains all approved domains in the .gov registrar.
In other words, with the exception of domains in the “Unknown”, ”On hold”, and “Deleted” states,
this table matches the list of domains in the .gov zone file.
</p>
<p>
Individual domain pages allow you to view registry-related details and edit the associated domain information.
</p>
<p>
Other actions available on the domain page include the ability to:
</p>
<ul>
<li>Extend a domains expiration date</li>
<li>Place a domain “On hold”</li>
<li>Remove a domain from the registry</li>
<li>View the domain from a domain managers perspective
</ul>
<p>
To view a domain from a domain managers perspective, click the "Manage domain" button.
That will allow you to make changes (e.g., update DNS settings, invite people to manage the domain)
directly inside the registrar.
</p>

View file

@ -0,0 +1,19 @@
<p>
Domain information represents the basic metadata for an approved domain and the organization that manages it.
It does not include any information specific to the registry (DNS name servers, security email).
Registry-related information
can be managed within the <a class="text-underline" href="{% url 'admin:registrar_domain_changelist' %}">Domains</a> table.
</p>
<p>
Updating values here will immediately update the corresponding values that users see in the registrar.
</p>
<p>
Domain information is similar to <a class="text-underline" href="{% url 'admin:registrar_domainrequest_changelist' %}">Domain requests</a>,
and the fields are nearly identical,
but edits made to one are not made to the other.
Domain information exists so we dont modify details of an approved request after adjudication
(since a domain request should be maintained as-adjudicated for records retention purposes).
Entries are created here upon approval of a domain request.
</p>

View file

@ -0,0 +1,16 @@
<p>
Domain invitations contain all individuals who have been invited to manage a .gov domain.
Invitations are sent via email, and the recipient must log in to the registrar to officially
accept and become a domain manager.
</p>
<p>
An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent.
A “received” status indicates that the recipient has logged in.
</p>
<p>
If an invitation is created in this table, an email will not be sent.
To have an email sent, go to the domain in <a class="text-underline" href="{% url 'admin:registrar_domain_changelist' %}">Domains</a>,
click the “Manage domain” button, and add a domain manager.
</p>

View file

@ -0,0 +1,11 @@
<p>
This table contains all domain requests that have been started within the registrar and the status of those requests.
Updating values here will immediately update the corresponding values that users see in the registrar.
</p>
<p>
Once a domain request has been adjudicated, the details of that request should not be modified.
To update attributes (like an organizations name) after a domains approval,
go to <a class="text-underline" href="{% url 'admin:registrar_domain_changelist' %}">Domains</a>.
Similar fields display on each Domain page, but edits made there will not affect the corresponding domain request.
</p>

View file

@ -0,0 +1,10 @@
<p>
This table represents all “requested domains” that have been saved within a domain request form.
If a registrant changes the requested domain in their form,
the original name they listed and the new name will appear as separate records.
</p>
<p>
This table does not include “alternative domains,”
which are housed in the <a class="text-underline" href="{% url 'admin:registrar_website_changelist' %}">Websites</a> table.
</p>

View file

@ -0,0 +1,11 @@
<p>
Entries in the Hosts table indicate the relationship between an approved domain and a name server address
(and, if applicable, the IP address for a name server address).
</p>
<p>
In general, you should not modify these values here. They should be updated directly inside the registrar.
To update a domains name servers and/or IP addresses,
in the registrar, go to the domain in <a class="text-underline" href="{% url 'admin:registrar_domain_changelist' %}">Domains</a>,
then click the "Manage domain" button.
</p>

View file

@ -0,0 +1,7 @@
<p>
Log entries represent instances where something was created, updated, or deleted within the registrar or /admin.
The table on this page is useful for searching actions across all records.
To understand what happened to an individual record (like a domain request),
its better to go to that specific page
(<a class="text-underline" href="{% url 'admin:registrar_domainrequest_changelist' %}">Domain requests</a>) and click on the “History” button.
</p>

View file

@ -0,0 +1,18 @@
<p>
Public contacts represent the three registry contact types (administrative, technical, and security)
and their fields that are exposed in WHOIS data.
</p>
<p>
We dont currently allow registrants to publish real contact information to WHOIS,
but we must publish something to WHOIS. For each of the contact types, we use default values that are
associated with the program instead of the real contact information,
which we then redact in whole at the registry/WHOIS.
We do allow registrants to set a security contact email address,
which is published to WHOIS when a user sets one.
</p>
<p>
The public contacts in this table are a reflection of the data in the registry and should not be updated.
This information is primarily used by developers for validation purposes.
</p>

View file

@ -0,0 +1,4 @@
<p>
This table represents the domains that were transitioned from the old registry in November 2023.
This data has been preserved for historical reference and should not be updated.
</p>

View file

@ -0,0 +1,16 @@
<p>
A user is anyone who has access to the registrar. This includes request creators, domain managers, and CISA administrators.
Within each user record, you can review that users permissions and user status.
</p>
<p>
If a user has a domain request in ineligible status, then their user status will be “restricted.”
Users who are in restricted status cannot create/edit domain requests or approved domains,
and their existing requests are locked. They will see a 403 error if they try to take action in the registrar.
</p>
<p>
Each user record displays the associated “contact” info for that user,
which is the same info found in the <a class="text-underline" href="{% url 'admin:registrar_contact_changelist' %}">Contacts</a> table.
Updating these details on the user record will also update the corresponding contact record for that user.
</p>

View file

@ -0,0 +1,10 @@
<p>
This table represents the managers who are assigned to each domain in the registrar.
There are separate records for each domain/manager combination.
Managers can update information related to a domain, such as DNS data and security contact.
</p>
<p>
The creator of an approved domain request automatically becomes a manager for that domain.
Anyone who retrieves a domain invitation is also assigned the manager role.
</p>

View file

@ -0,0 +1,10 @@
<p>
Groups are a way to bundle admin permissions so they can be easily assigned to multiple users.
Once a group is created, it can be assigned to people via the <a class="text-underline" href="{% url 'admin:registrar_user_changelist' %}">Users</a> table.
</p>
<p>
To maintain a single source of truth across all environments (stable, staging, etc.),
the groups and permissions are set and updated through the codebase.
<strong>Do not add, edit or delete user groups here.</strong>
</p>

View file

@ -0,0 +1,10 @@
<p>
This table contains users who have been allowed to bypass identity proofing through Login.gov after approval by a CISA representative.
Bypassing identity proofing means they will not be asked to provide a form of ID, PII, and so on.
</p>
<p>
Additions to this table should be rare, and only after obtaining confirmation of their identity directly (or from a trusted person).
Once a verified-by-staff user has been added as a domain manager, they can be removed from this list,
(However, if they are removed as a domain manager for all domains and they attempt to sign in again, they will be identity proofed by Login.gov).
</p>

View file

@ -0,0 +1,8 @@
<p>
This table lists all the “current websites” and “alternative domains” that users have submitted in domain requests since January 2024.
</p>
<p>
This does not include any “requested domains” that have appeared within the <a class="text-underline" href="{% url 'admin:registrar_domainrequest_changelist' %}">Domain requests</a> table.
Those names are managed in the <a class="text-underline" href="{% url 'admin:registrar_draftdomain_changelist' %}">Draft domains<a/> table.
</p>

View file

@ -1,4 +1,5 @@
{% extends "admin/fieldset.html" %} {% extends "admin/fieldset.html" %}
{% load custom_filters %}
{% load static url_helpers %} {% load static url_helpers %}
{% comment %} {% comment %}
@ -44,7 +45,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
<div class="readonly"> <div class="readonly">
{% with total_websites=field.contents|split:", " %} {% with total_websites=field.contents|split:", " %}
{% for website in total_websites %} {% for website in total_websites %}
<a href="{{ website }}" class="padding-top-1 current-website__{{forloop.counter}}">{{ website }}</a>{% if not forloop.last %}, {% endif %} <a href="{{ website }}" target="_blank" class="padding-top-1 current-website__{{forloop.counter}}">{{ website }}</a>{% if not forloop.last %}, {% endif %}
{# Acts as a <br> #} {# Acts as a <br> #}
{% if total_websites|length < 5 %} {% if total_websites|length < 5 %}
<div class="display-block margin-top-1"></div> <div class="display-block margin-top-1"></div>
@ -67,7 +68,43 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% endblock field_readonly %} {% endblock field_readonly %}
{% block after_help_text %} {% block after_help_text %}
{% if field.field.name == "creator" %} {% if field.field.name == "status" and original_object.history.count > 0 %}
<div class="flex-container">
<label aria-label="Submitter contact details"></label>
<div>
<div class="usa-table-container--scrollable collapse--dgsimple" tabindex="0">
<table class="usa-table usa-table--borderless">
<thead>
<tr>
<th>Status</th>
<th>User</th>
<th>Changed at</th>
</tr>
</thead>
<tbody>
{% for log_entry in original_object.history.all %}
{% for key, value in log_entry.changes_display_dict.items %}
{% if key == "status" %}
<tr>
<td>{{ value.1|default:"None" }}</td>
<td>{{ log_entry.actor|default:"None" }}</td>
<td>{{ log_entry.timestamp|default:"None" }}</td>
</tr>
{% endif %}
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-2 margin-bottom-1 margin-left-1">
<span>Hide details</span>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="/public/img/sprite.svg#expand_less"></use>
</svg>
</button>
</div>
</div>
{% elif field.field.name == "creator" %}
<div class="flex-container tablet:margin-top-2"> <div class="flex-container tablet:margin-top-2">
<label aria-label="Creator contact details"></label> <label aria-label="Creator contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly user_verification_type=original_object.creator.get_verification_type_display%} {% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly user_verification_type=original_object.creator.get_verification_type_display%}
@ -126,5 +163,16 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
</details> </details>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% elif field.field.name == "state_territory" %}
<div class="flex-container margin-top-2">
<span>
CISA region:
{% if original_object.generic_org_type and original_object.generic_org_type != original_object.OrganizationChoices.FEDERAL %}
{{ original_object.state_territory|get_region }}
{% else %}
N/A
{% endif %}
</span>
</div>
{% endif %} {% endif %}
{% endblock after_help_text %} {% endblock after_help_text %}

View file

@ -1,8 +1,10 @@
<div class="usa-site-alert--emergency margin-y-0 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert" <div class="usa-site-alert--emergency margin-y-0 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
>
<div class="usa-alert"> <div class="usa-alert">
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %}"> <div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %}">
<b>Attention:</b> You are on a test site. <b>Attention:</b> You are on a test site.
{% if has_profile_feature_flag %}
The profile_feature flag is active.
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View file

@ -67,3 +67,69 @@ def get_organization_long_name(generic_org_type):
@register.filter(name="has_permission") @register.filter(name="has_permission")
def has_permission(user, permission): def has_permission(user, permission):
return user.has_perm(permission) return user.has_perm(permission)
@register.filter
def get_region(state):
if state and isinstance(state, str):
regions = {
"CT": 1,
"ME": 1,
"MA": 1,
"NH": 1,
"RI": 1,
"VT": 1,
"NJ": 2,
"NY": 2,
"PR": 2,
"VI": 2,
"DE": 3,
"DC": 3,
"MD": 3,
"PA": 3,
"VA": 3,
"WV": 3,
"AL": 4,
"FL": 4,
"GA": 4,
"KY": 4,
"MS": 4,
"NC": 4,
"SC": 4,
"TN": 4,
"IL": 5,
"IN": 5,
"MI": 5,
"MN": 5,
"OH": 5,
"WI": 5,
"AR": 6,
"LA": 6,
"NM": 6,
"OK": 6,
"TX": 6,
"IA": 7,
"KS": 7,
"MO": 7,
"NE": 7,
"CO": 8,
"MT": 8,
"ND": 8,
"SD": 8,
"UT": 8,
"WY": 8,
"AZ": 9,
"CA": 9,
"HI": 9,
"NV": 9,
"GU": 9,
"AS": 9,
"MP": 9,
"AK": 10,
"ID": 10,
"OR": 10,
"WA": 10,
}
return regions.get(state.upper(), "N/A")
else:
return None

View file

@ -841,7 +841,7 @@ def completed_domain_request(
) )
if not investigator: if not investigator:
investigator, _ = User.objects.get_or_create( investigator, _ = User.objects.get_or_create(
username="incrediblyfakeinvestigator", username="incrediblyfakeinvestigator" + str(uuid.uuid4())[:8],
first_name="Joe", first_name="Joe",
last_name="Bob", last_name="Bob",
is_staff=True, is_staff=True,

View file

@ -21,6 +21,12 @@ from registrar.admin import (
MyHostAdmin, MyHostAdmin,
UserDomainRoleAdmin, UserDomainRoleAdmin,
VerifiedByStaffAdmin, VerifiedByStaffAdmin,
WebsiteAdmin,
DraftDomainAdmin,
FederalAgencyAdmin,
PublicContactAdmin,
TransitionDomainAdmin,
UserGroupAdmin,
) )
from registrar.models import ( from registrar.models import (
Domain, Domain,
@ -33,6 +39,9 @@ from registrar.models import (
PublicContact, PublicContact,
Host, Host,
Website, Website,
FederalAgency,
UserGroup,
TransitionDomain,
) )
from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_domain_role import UserDomainRole
from registrar.models.verified_by_staff import VerifiedByStaff from registrar.models.verified_by_staff import VerifiedByStaff
@ -95,6 +104,89 @@ class TestDomainAdmin(MockEppLib, WebTest):
) )
super().setUp() super().setUp()
@less_console_noise_decorator
def test_staff_can_see_cisa_region_federal(self):
"""Tests if staff can see CISA Region: N/A"""
# Create a fake domain request
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
_domain_request.approve()
domain = _domain_request.approved_domain
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
# Test if the page has the right CISA region
expected_html = '<div class="flex-container margin-top-2"><span>CISA region: N/A</span></div>'
# Remove whitespace from expected_html
expected_html = "".join(expected_html.split())
# Remove whitespace from response content
response_content = "".join(response.content.decode().split())
# Check if response contains expected_html
self.assertIn(expected_html, response_content)
@less_console_noise_decorator
def test_staff_can_see_cisa_region_non_federal(self):
"""Tests if staff can see the correct CISA region"""
# Create a fake domain request. State will be NY (2).
_domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate"
)
_domain_request.approve()
domain = _domain_request.approved_domain
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
# Test if the page has the right CISA region
expected_html = '<div class="flex-container margin-top-2"><span>CISA region: 2</span></div>'
# Remove whitespace from expected_html
expected_html = "".join(expected_html.split())
# Remove whitespace from response content
response_content = "".join(response.content.decode().split())
# Check if response contains expected_html
self.assertIn(expected_html, response_content)
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domain/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(response, "This table contains all approved domains in the .gov registrar.")
self.assertContains(response, "Show more")
@less_console_noise_decorator @less_console_noise_decorator
def test_contact_fields_on_domain_change_form_have_detail_table(self): def test_contact_fields_on_domain_change_form_have_detail_table(self):
"""Tests if the contact fields in the inlined Domain information have the detail table """Tests if the contact fields in the inlined Domain information have the detail table
@ -511,7 +603,7 @@ class TestDomainAdmin(MockEppLib, WebTest):
# 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=36) self.assertContains(response, "Federal", count=42)
# 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 +944,23 @@ class TestDomainRequestAdmin(MockEppLib):
) )
self.mock_client = MockSESClient() self.mock_client = MockSESClient()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domainrequest/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(response, "This table contains all domain requests")
self.assertContains(response, "Show more")
@less_console_noise_decorator @less_console_noise_decorator
def test_helper_text(self): def test_helper_text(self):
""" """
@ -888,6 +997,96 @@ class TestDomainRequestAdmin(MockEppLib):
self.test_helper.assert_response_contains_distinct_values(response, expected_values) self.test_helper.assert_response_contains_distinct_values(response, expected_values)
@less_console_noise_decorator @less_console_noise_decorator
def test_status_logs(self):
"""
Tests that the status changes are shown in a table on the domain request change form,
accurately and in chronological order.
"""
# Create a fake domain request and domain
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED)
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_request.requested_domain.name)
# Table will contain one row for Started
self.assertContains(response, "<td>Started</td>", count=1)
self.assertNotContains(response, "<td>Submitted</td>")
domain_request.submit()
domain_request.save()
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Table will contain and extra row for Submitted
self.assertContains(response, "<td>Started</td>", count=1)
self.assertContains(response, "<td>Submitted</td>", count=1)
domain_request.in_review()
domain_request.save()
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Table will contain and extra row for In review
self.assertContains(response, "<td>Started</td>", count=1)
self.assertContains(response, "<td>Submitted</td>", count=1)
self.assertContains(response, "<td>In review</td>", count=1)
domain_request.action_needed()
domain_request.save()
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Table will contain and extra row for Action needed
self.assertContains(response, "<td>Started</td>", count=1)
self.assertContains(response, "<td>Submitted</td>", count=1)
self.assertContains(response, "<td>In review</td>", count=1)
self.assertContains(response, "<td>Action needed</td>", count=1)
domain_request.in_review()
domain_request.save()
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
follow=True,
)
# Define the expected sequence of status changes
expected_status_changes = [
"<td>In review</td>",
"<td>Action needed</td>",
"<td>In review</td>",
"<td>Submitted</td>",
"<td>Started</td>",
]
# Test for the order of status changes
for status_change in expected_status_changes:
self.assertContains(response, status_change, html=True)
# Table now contains 2 rows for Approved
self.assertContains(response, "<td>Started</td>", count=1)
self.assertContains(response, "<td>Submitted</td>", count=1)
self.assertContains(response, "<td>In review</td>", count=2)
self.assertContains(response, "<td>Action needed</td>", count=1)
def test_collaspe_toggle_button_markup(self): def test_collaspe_toggle_button_markup(self):
""" """
Tests for the correct collapse toggle button markup Tests for the correct collapse toggle button markup
@ -906,7 +1105,6 @@ class TestDomainRequestAdmin(MockEppLib):
# Make sure the page loaded, and that we're on the right page # Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_request.requested_domain.name) self.assertContains(response, domain_request.requested_domain.name)
self.test_helper.assertContains(response, "<span>Show details</span>") self.test_helper.assertContains(response, "<span>Show details</span>")
@less_console_noise_decorator @less_console_noise_decorator
@ -1178,7 +1376,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=34) self.assertContains(response, "Federal", count=40)
# 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
@ -1797,7 +1995,7 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertContains(response, domain_request.requested_domain.name) self.assertContains(response, domain_request.requested_domain.name)
# Check that the page contains the link we expect. # Check that the page contains the link we expect.
expected_url = '<a href="city.com" class="padding-top-1 current-website__1">city.com</a>' expected_url = '<a href="city.com" target="_blank" class="padding-top-1 current-website__1">city.com</a>'
self.assertContains(response, expected_url) self.assertContains(response, expected_url)
@less_console_noise_decorator @less_console_noise_decorator
@ -2390,6 +2588,66 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertEqual(expected_list, actual_list) self.assertEqual(expected_list, actual_list)
@less_console_noise_decorator
def test_staff_can_see_cisa_region_federal(self):
"""Tests if staff can see CISA Region: N/A"""
# Create a fake 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,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, _domain_request.requested_domain.name)
# Test if the page has the right CISA region
expected_html = '<div class="flex-container margin-top-2"><span>CISA region: N/A</span></div>'
# Remove whitespace from expected_html
expected_html = "".join(expected_html.split())
# Remove whitespace from response content
response_content = "".join(response.content.decode().split())
# Check if response contains expected_html
self.assertIn(expected_html, response_content)
@less_console_noise_decorator
def test_staff_can_see_cisa_region_non_federal(self):
"""Tests if staff can see the correct CISA region"""
# Create a fake domain request. State will be NY (2).
_domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate"
)
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, _domain_request.requested_domain.name)
# Test if the page has the right CISA region
expected_html = '<div class="flex-container margin-top-2"><span>CISA region: 2</span></div>'
# Remove whitespace from expected_html
expected_html = "".join(expected_html.split())
# Remove whitespace from response content
response_content = "".join(response.content.decode().split())
# Check if response contains expected_html
self.assertIn(expected_html, response_content)
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
Domain.objects.all().delete() Domain.objects.all().delete()
@ -2401,7 +2659,7 @@ class TestDomainRequestAdmin(MockEppLib):
self.mock_client.EMAILS_SENT.clear() self.mock_client.EMAILS_SENT.clear()
class DomainInvitationAdminTest(TestCase): class TestDomainInvitationAdmin(TestCase):
"""Tests for the DomainInvitation page""" """Tests for the DomainInvitation page"""
def setUp(self): def setUp(self):
@ -2417,6 +2675,25 @@ class DomainInvitationAdminTest(TestCase):
User.objects.all().delete() User.objects.all().delete()
Contact.objects.all().delete() Contact.objects.all().delete()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domaininvitation/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(
response, "Domain invitations contain all individuals who have been invited to manage a .gov domain."
)
self.assertContains(response, "Show more")
def test_get_filters(self): def test_get_filters(self):
"""Ensures that our filters are displaying correctly""" """Ensures that our filters are displaying correctly"""
with less_console_noise(): with less_console_noise():
@ -2431,7 +2708,7 @@ class DomainInvitationAdminTest(TestCase):
) )
# Assert that the filters are added # Assert that the filters are added
self.assertContains(response, "invited", count=2) self.assertContains(response, "invited", count=4)
self.assertContains(response, "Invited", count=2) self.assertContains(response, "Invited", count=2)
self.assertContains(response, "retrieved", count=2) self.assertContains(response, "retrieved", count=2)
self.assertContains(response, "Retrieved", count=2) self.assertContains(response, "Retrieved", count=2)
@ -2466,6 +2743,23 @@ class TestHostAdmin(TestCase):
Host.objects.all().delete() Host.objects.all().delete()
Domain.objects.all().delete() Domain.objects.all().delete()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/host/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(response, "Entries in the Hosts table indicate the relationship between an approved domain")
self.assertContains(response, "Show more")
@less_console_noise_decorator @less_console_noise_decorator
def test_helper_text(self): def test_helper_text(self):
""" """
@ -2544,6 +2838,88 @@ class TestDomainInformationAdmin(TestCase):
Contact.objects.all().delete() Contact.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
@less_console_noise_decorator
def test_admin_can_see_cisa_region_federal(self):
"""Tests if admins can see CISA Region: N/A"""
# Create a fake domain request
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
_domain_request.approve()
domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get()
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domaininformation/{}/change/".format(domain_information.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_information.domain.name)
# Test if the page has the right CISA region
expected_html = '<div class="flex-container margin-top-2"><span>CISA region: N/A</span></div>'
# Remove whitespace from expected_html
expected_html = "".join(expected_html.split())
# Remove whitespace from response content
response_content = "".join(response.content.decode().split())
# Check if response contains expected_html
self.assertIn(expected_html, response_content)
@less_console_noise_decorator
def test_admin_can_see_cisa_region_non_federal(self):
"""Tests if admins can see the correct CISA region"""
# Create a fake domain request. State will be NY (2).
_domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate"
)
_domain_request.approve()
domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get()
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domaininformation/{}/change/".format(domain_information.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_information.domain.name)
# Test if the page has the right CISA region
expected_html = '<div class="flex-container margin-top-2"><span>CISA region: 2</span></div>'
# Remove whitespace from expected_html
expected_html = "".join(expected_html.split())
# Remove whitespace from response content
response_content = "".join(response.content.decode().split())
# Check if response contains expected_html
self.assertIn(expected_html, response_content)
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/domaininformation/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(response, "Domain information represents the basic metadata")
self.assertContains(response, "Show more")
@less_console_noise_decorator @less_console_noise_decorator
def test_helper_text(self): def test_helper_text(self):
""" """
@ -2787,7 +3163,7 @@ class TestDomainInformationAdmin(TestCase):
self.test_helper.assert_table_sorted("-4", ("-submitter__first_name", "-submitter__last_name")) self.test_helper.assert_table_sorted("-4", ("-submitter__first_name", "-submitter__last_name"))
class UserDomainRoleAdminTest(TestCase): class TestUserDomainRoleAdmin(TestCase):
def setUp(self): def setUp(self):
"""Setup environment for a mock admin user""" """Setup environment for a mock admin user"""
self.site = AdminSite() self.site = AdminSite()
@ -2809,6 +3185,25 @@ class UserDomainRoleAdminTest(TestCase):
Domain.objects.all().delete() Domain.objects.all().delete()
UserDomainRole.objects.all().delete() UserDomainRole.objects.all().delete()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/userdomainrole/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(
response, "This table represents the managers who are assigned to each domain in the registrar"
)
self.assertContains(response, "Show more")
def test_domain_sortable(self): def test_domain_sortable(self):
"""Tests if the UserDomainrole sorts by domain correctly""" """Tests if the UserDomainrole sorts by domain correctly"""
with less_console_noise(): with less_console_noise():
@ -3005,6 +3400,23 @@ class TestMyUserAdmin(TestCase):
super().tearDown() super().tearDown()
User.objects.all().delete() User.objects.all().delete()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/user/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(response, "A user is anyone who has access to the registrar.")
self.assertContains(response, "Show more")
@less_console_noise_decorator @less_console_noise_decorator
def test_helper_text(self): def test_helper_text(self):
""" """
@ -3439,7 +3851,7 @@ class DomainSessionVariableTest(TestCase):
) )
class ContactAdminTest(TestCase): class TestContactAdmin(TestCase):
def setUp(self): def setUp(self):
self.site = AdminSite() self.site = AdminSite()
self.factory = RequestFactory() self.factory = RequestFactory()
@ -3448,6 +3860,23 @@ class ContactAdminTest(TestCase):
self.superuser = create_superuser() self.superuser = create_superuser()
self.staffuser = create_user() self.staffuser = create_user()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/contact/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(response, "Contacts include anyone who has access to the registrar (known as “users”)")
self.assertContains(response, "Show more")
def test_readonly_when_restricted_staffuser(self): def test_readonly_when_restricted_staffuser(self):
with less_console_noise(): with less_console_noise():
request = self.factory.get("/") request = self.factory.get("/")
@ -3566,6 +3995,25 @@ class TestVerifiedByStaffAdmin(TestCase):
VerifiedByStaff.objects.all().delete() VerifiedByStaff.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/verifiedbystaff/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(
response, "This table contains users who have been allowed to bypass " "identity proofing through Login.gov"
)
self.assertContains(response, "Show more")
@less_console_noise_decorator @less_console_noise_decorator
def test_helper_text(self): def test_helper_text(self):
""" """
@ -3608,3 +4056,204 @@ class TestVerifiedByStaffAdmin(TestCase):
# Check that the user field is set to the request.user # Check that the user field is set to the request.user
self.assertEqual(vip_instance.requestor, self.superuser) self.assertEqual(vip_instance.requestor, self.superuser)
class TestWebsiteAdmin(TestCase):
def setUp(self):
super().setUp()
self.site = AdminSite()
self.superuser = create_superuser()
self.admin = WebsiteAdmin(model=Website, admin_site=self.site)
self.factory = RequestFactory()
self.client = Client(HTTP_HOST="localhost:8080")
self.test_helper = GenericTestHelper(admin=self.admin)
def tearDown(self):
super().tearDown()
Website.objects.all().delete()
User.objects.all().delete()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/website/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(response, "This table lists all the “current websites” and “alternative domains”")
self.assertContains(response, "Show more")
class TestDraftDomain(TestCase):
def setUp(self):
super().setUp()
self.site = AdminSite()
self.superuser = create_superuser()
self.admin = DraftDomainAdmin(model=DraftDomain, admin_site=self.site)
self.factory = RequestFactory()
self.client = Client(HTTP_HOST="localhost:8080")
self.test_helper = GenericTestHelper(admin=self.admin)
def tearDown(self):
super().tearDown()
DraftDomain.objects.all().delete()
User.objects.all().delete()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/draftdomain/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(
response, "This table represents all “requested domains” that have been saved within a domain"
)
self.assertContains(response, "Show more")
class TestFederalAgency(TestCase):
def setUp(self):
super().setUp()
self.site = AdminSite()
self.superuser = create_superuser()
self.admin = FederalAgencyAdmin(model=FederalAgency, admin_site=self.site)
self.factory = RequestFactory()
self.client = Client(HTTP_HOST="localhost:8080")
self.test_helper = GenericTestHelper(admin=self.admin)
def tearDown(self):
super().tearDown()
FederalAgency.objects.all().delete()
User.objects.all().delete()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/federalagency/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(response, "This table does not have a description yet.")
self.assertContains(response, "Show more")
class TestPublicContact(TestCase):
def setUp(self):
super().setUp()
self.site = AdminSite()
self.superuser = create_superuser()
self.admin = PublicContactAdmin(model=PublicContact, admin_site=self.site)
self.factory = RequestFactory()
self.client = Client(HTTP_HOST="localhost:8080")
self.test_helper = GenericTestHelper(admin=self.admin)
def tearDown(self):
super().tearDown()
PublicContact.objects.all().delete()
User.objects.all().delete()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/publiccontact/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(response, "Public contacts represent the three registry contact types")
self.assertContains(response, "Show more")
class TestTransitionDomain(TestCase):
def setUp(self):
super().setUp()
self.site = AdminSite()
self.superuser = create_superuser()
self.admin = TransitionDomainAdmin(model=TransitionDomain, admin_site=self.site)
self.factory = RequestFactory()
self.client = Client(HTTP_HOST="localhost:8080")
self.test_helper = GenericTestHelper(admin=self.admin)
def tearDown(self):
super().tearDown()
PublicContact.objects.all().delete()
User.objects.all().delete()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/transitiondomain/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(response, "This table represents the domains that were transitioned from the old registry")
self.assertContains(response, "Show more")
class TestUserGroup(TestCase):
def setUp(self):
super().setUp()
self.site = AdminSite()
self.superuser = create_superuser()
self.admin = UserGroupAdmin(model=UserGroup, admin_site=self.site)
self.factory = RequestFactory()
self.client = Client(HTTP_HOST="localhost:8080")
self.test_helper = GenericTestHelper(admin=self.admin)
def tearDown(self):
super().tearDown()
User.objects.all().delete()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(
"/admin/registrar/usergroup/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(
response, "Groups are a way to bundle admin permissions so they can be easily assigned to multiple users."
)
self.assertContains(response, "Show more")

View file

@ -1152,12 +1152,18 @@ class TestContact(TestCase):
def setUp(self): def setUp(self):
self.email_for_invalid = "intern@igorville.gov" self.email_for_invalid = "intern@igorville.gov"
self.invalid_user, _ = User.objects.get_or_create( self.invalid_user, _ = User.objects.get_or_create(
username=self.email_for_invalid, email=self.email_for_invalid, first_name="", last_name="" username=self.email_for_invalid,
email=self.email_for_invalid,
first_name="",
last_name="",
phone="",
) )
self.invalid_contact, _ = Contact.objects.get_or_create(user=self.invalid_user) self.invalid_contact, _ = Contact.objects.get_or_create(user=self.invalid_user)
self.email = "mayor@igorville.gov" self.email = "mayor@igorville.gov"
self.user, _ = User.objects.get_or_create(email=self.email, first_name="Jeff", last_name="Lebowski") self.user, _ = User.objects.get_or_create(
email=self.email, first_name="Jeff", last_name="Lebowski", phone="123456789"
)
self.contact, _ = Contact.objects.get_or_create(user=self.user) self.contact, _ = Contact.objects.get_or_create(user=self.user)
self.contact_as_ao, _ = Contact.objects.get_or_create(email="newguy@igorville.gov") self.contact_as_ao, _ = Contact.objects.get_or_create(email="newguy@igorville.gov")
@ -1169,19 +1175,22 @@ class TestContact(TestCase):
Contact.objects.all().delete() Contact.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
def test_saving_contact_updates_user_first_last_names(self): def test_saving_contact_updates_user_first_last_names_and_phone(self):
"""When a contact is updated, we propagate the changes to the linked user if it exists.""" """When a contact is updated, we propagate the changes to the linked user if it exists."""
# User and Contact are created and linked as expected. # User and Contact are created and linked as expected.
# An empty User object should create an empty contact. # An empty User object should create an empty contact.
self.assertEqual(self.invalid_contact.first_name, "") self.assertEqual(self.invalid_contact.first_name, "")
self.assertEqual(self.invalid_contact.last_name, "") self.assertEqual(self.invalid_contact.last_name, "")
self.assertEqual(self.invalid_contact.phone, "")
self.assertEqual(self.invalid_user.first_name, "") self.assertEqual(self.invalid_user.first_name, "")
self.assertEqual(self.invalid_user.last_name, "") self.assertEqual(self.invalid_user.last_name, "")
self.assertEqual(self.invalid_user.phone, "")
# Manually update the contact - mimicking production (pre-existing data) # Manually update the contact - mimicking production (pre-existing data)
self.invalid_contact.first_name = "Joey" self.invalid_contact.first_name = "Joey"
self.invalid_contact.last_name = "Baloney" self.invalid_contact.last_name = "Baloney"
self.invalid_contact.phone = "123456789"
self.invalid_contact.save() self.invalid_contact.save()
# Refresh the user object to reflect the changes made in the database # Refresh the user object to reflect the changes made in the database
@ -1190,20 +1199,25 @@ class TestContact(TestCase):
# Updating the contact's first and last names propagate to the user # Updating the contact's first and last names propagate to the user
self.assertEqual(self.invalid_contact.first_name, "Joey") self.assertEqual(self.invalid_contact.first_name, "Joey")
self.assertEqual(self.invalid_contact.last_name, "Baloney") self.assertEqual(self.invalid_contact.last_name, "Baloney")
self.assertEqual(self.invalid_contact.phone, "123456789")
self.assertEqual(self.invalid_user.first_name, "Joey") self.assertEqual(self.invalid_user.first_name, "Joey")
self.assertEqual(self.invalid_user.last_name, "Baloney") self.assertEqual(self.invalid_user.last_name, "Baloney")
self.assertEqual(self.invalid_user.phone, "123456789")
def test_saving_contact_does_not_update_user_first_last_names(self): def test_saving_contact_does_not_update_user_first_last_names_and_phone(self):
"""When a contact is updated, we avoid propagating the changes to the linked user if it already has a value""" """When a contact is updated, we avoid propagating the changes to the linked user if it already has a value"""
# User and Contact are created and linked as expected # User and Contact are created and linked as expected
self.assertEqual(self.contact.first_name, "Jeff") self.assertEqual(self.contact.first_name, "Jeff")
self.assertEqual(self.contact.last_name, "Lebowski") self.assertEqual(self.contact.last_name, "Lebowski")
self.assertEqual(self.contact.phone, "123456789")
self.assertEqual(self.user.first_name, "Jeff") self.assertEqual(self.user.first_name, "Jeff")
self.assertEqual(self.user.last_name, "Lebowski") self.assertEqual(self.user.last_name, "Lebowski")
self.assertEqual(self.user.phone, "123456789")
self.contact.first_name = "Joey" self.contact.first_name = "Joey"
self.contact.last_name = "Baloney" self.contact.last_name = "Baloney"
self.contact.phone = "987654321"
self.contact.save() self.contact.save()
# Refresh the user object to reflect the changes made in the database # Refresh the user object to reflect the changes made in the database
@ -1212,8 +1226,10 @@ class TestContact(TestCase):
# Updating the contact's first and last names propagate to the user # Updating the contact's first and last names propagate to the user
self.assertEqual(self.contact.first_name, "Joey") self.assertEqual(self.contact.first_name, "Joey")
self.assertEqual(self.contact.last_name, "Baloney") self.assertEqual(self.contact.last_name, "Baloney")
self.assertEqual(self.contact.phone, "987654321")
self.assertEqual(self.user.first_name, "Jeff") self.assertEqual(self.user.first_name, "Jeff")
self.assertEqual(self.user.last_name, "Lebowski") self.assertEqual(self.user.last_name, "Lebowski")
self.assertEqual(self.user.phone, "123456789")
def test_saving_contact_does_not_update_user_email(self): def test_saving_contact_does_not_update_user_email(self):
"""When a contact's email is updated, the change is not propagated to the user.""" """When a contact's email is updated, the change is not propagated to the user."""

View file

@ -1,6 +1,7 @@
from django.shortcuts import render from django.shortcuts import render
from registrar.models import DomainRequest, Domain, UserDomainRole from registrar.models import DomainRequest, Domain, UserDomainRole
from waffle.decorators import flag_is_active
def index(request): def index(request):
@ -20,6 +21,9 @@ def index(request):
has_deletable_domain_requests = deletable_domain_requests.exists() has_deletable_domain_requests = deletable_domain_requests.exists()
context["has_deletable_domain_requests"] = has_deletable_domain_requests context["has_deletable_domain_requests"] = has_deletable_domain_requests
# This is a django waffle flag which toggles features based off of the "flag" table
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
# If they can delete domain requests, add the delete button to the context # If they can delete domain requests, add the delete button to the context
if has_deletable_domain_requests: if has_deletable_domain_requests:
# Add the delete modal button to the context # Add the delete modal button to the context

View file

@ -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.8.1; python_version >= '3.8' asgiref==3.8.1; python_version >= '3.8'
boto3==1.34.90; python_version >= '3.8' boto3==1.34.95; python_version >= '3.8'
botocore==1.34.90; python_version >= '3.8' botocore==1.34.95; 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,9 +22,10 @@ django-csp==3.8
django-fsm==2.8.1 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-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'
faker==24.11.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
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'
@ -37,7 +38,7 @@ lxml==5.2.1; python_version >= '3.6'
mako==1.3.3; python_version >= '3.8' mako==1.3.3; python_version >= '3.8'
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'
oic==1.6.1; python_version ~= '3.7' oic==1.7.0; python_version ~= '3.8'
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