diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml
index d9d7cbe14..1c486bdf7 100644
--- a/.github/workflows/deploy-sandbox.yaml
+++ b/.github/workflows/deploy-sandbox.yaml
@@ -24,6 +24,7 @@ jobs:
|| startsWith(github.head_ref, 'backup/')
|| startsWith(github.head_ref, 'meoward/')
|| startsWith(github.head_ref, 'bob/')
+ || startsWith(github.head_ref, 'cb/')
outputs:
environment: ${{ steps.var.outputs.environment}}
runs-on: "ubuntu-latest"
diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml
index 825ab04d7..f5815012c 100644
--- a/.github/workflows/migrate.yaml
+++ b/.github/workflows/migrate.yaml
@@ -16,6 +16,7 @@ on:
- stable
- staging
- development
+ - cb
- bob
- meoward
- backup
diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml
index 05eb963c3..06638aa05 100644
--- a/.github/workflows/reset-db.yaml
+++ b/.github/workflows/reset-db.yaml
@@ -16,6 +16,7 @@ on:
options:
- staging
- development
+ - cb
- bob
- meoward
- backup
diff --git a/docs/developer/adding-feature-flags.md b/docs/developer/adding-feature-flags.md
new file mode 100644
index 000000000..dd97c7497
--- /dev/null
+++ b/docs/developer/adding-feature-flags.md
@@ -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.
diff --git a/ops/manifests/manifest-cb.yaml b/ops/manifests/manifest-cb.yaml
new file mode 100644
index 000000000..b9be98d27
--- /dev/null
+++ b/ops/manifests/manifest-cb.yaml
@@ -0,0 +1,32 @@
+---
+applications:
+- name: getgov-cb
+ buildpacks:
+ - python_buildpack
+ path: ../../src
+ instances: 1
+ memory: 512M
+ stack: cflinuxfs4
+ timeout: 180
+ command: ./run.sh
+ health-check-type: http
+ health-check-http-endpoint: /health
+ health-check-invocation-timeout: 40
+ env:
+ # Send stdout and stderr straight to the terminal without buffering
+ PYTHONUNBUFFERED: yup
+ # Tell Django where to find its configuration
+ DJANGO_SETTINGS_MODULE: registrar.config.settings
+ # Tell Django where it is being hosted
+ DJANGO_BASE_URL: https://getgov-cb.app.cloud.gov
+ # Tell Django how much stuff to log
+ DJANGO_LOG_LEVEL: INFO
+ # default public site location
+ GETGOV_PUBLIC_SITE_URL: https://get.gov
+ # Flag to disable/enable features in prod environments
+ IS_PRODUCTION: False
+ routes:
+ - route: getgov-cb.app.cloud.gov
+ services:
+ - getgov-credentials
+ - getgov-cb-database
diff --git a/src/Pipfile b/src/Pipfile
index 33abf0158..fdf127d7c 100644
--- a/src/Pipfile
+++ b/src/Pipfile
@@ -33,6 +33,7 @@ pyzipper="*"
tblib = "*"
django-admin-multiple-choice-list-filter = "*"
django-import-export = "*"
+django-waffle = "*"
[dev-packages]
django-debug-toolbar = "*"
diff --git a/src/Pipfile.lock b/src/Pipfile.lock
index f21b833ce..a42563c63 100644
--- a/src/Pipfile.lock
+++ b/src/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "16a0db98015509322cf1d27f06fced5b7635057c4eb98921a9419d63d51925ab"
+ "sha256": "9095c4f98f58a9502444584067a63f329d5a5fc4b49454c4e129bda09552d19d"
},
"pipfile-spec": 6,
"requires": {},
@@ -32,20 +32,20 @@
},
"boto3": {
"hashes": [
- "sha256:2824e3dd18743ca50e5b10439d20e74647b1416e8a94509cb30beac92d27a18d",
- "sha256:b2e5cb5b95efcc881e25a3bc872d7a24e75ff4e76f368138e4baf7b9d6ee3422"
+ "sha256:decf52f8d5d8a1b10c9ff2a0e96ee207ed79e33d2e53fdf0880a5cbef70785e0",
+ "sha256:e836b71d79671270fccac0a4d4c8ec239a6b82ea47c399b64675aa597d0ee63b"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==1.34.90"
+ "version": "==1.34.95"
},
"botocore": {
"hashes": [
- "sha256:113cd4c0cb63e13163ccbc2bb13d551be314ba7f8ba5bfab1c51a19ca01aa133",
- "sha256:d48f152498e2c60b43ce25b579d26642346a327b6fb2c632d57219e0a4f63392"
+ "sha256:6bd76a2eadb42b91fa3528392e981ad5b4dfdee3968fa5b904278acf6cbf15ff",
+ "sha256:ead5823e0dd6751ece5498cb979fd9abf190e691c8833bcac6876fd6ca261fa7"
],
"markers": "python_version >= '3.8'",
- "version": "==1.34.90"
+ "version": "==1.34.95"
},
"cachetools": {
"hashes": [
@@ -387,6 +387,15 @@
"markers": "python_version >= '3.8'",
"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": {
"hashes": [
"sha256:1c2180681ebb994e922c754804c7ffebbe1245014777ac47897a81f57cc629c7",
@@ -417,12 +426,12 @@
},
"faker": {
"hashes": [
- "sha256:34b947581c2bced340c39b35f89dbfac4f356932cfff8fe893bde854903f0e6e",
- "sha256:adb98e771073a06bdc5d2d6710d8af07ac5da64c8dc2ae3b17bb32319e66fd82"
+ "sha256:87ef41e24b39a5be66ecd874af86f77eebd26782a2681200e86c5326340a95d3",
+ "sha256:e23a2b74888885c3d23a9237bacb823041291c03d609a39acb9ebe6c123f3986"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==24.11.0"
+ "version": "==25.0.0"
},
"fred-epplib": {
"git": "https://github.com/cisagov/epplib.git",
@@ -613,7 +622,6 @@
"sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b",
"sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6",
"sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8",
- "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5",
"sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306",
"sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5",
"sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f",
@@ -839,12 +847,12 @@
},
"oic": {
"hashes": [
- "sha256:385a1f64bb59519df1e23840530921bf416740240f505ea6d161e331d3d39fad",
- "sha256:fcbf948a22e4d4df66f6bf57d327933f32a7b539640d9b42883457634360ba78"
+ "sha256:b74bd06c7de1ab4f8e798f714062e6a68f68ad9cdbed1f1c30a7fb887602f321",
+ "sha256:e51705d0c14c97e9ca594374bfb54269a72c9b489e0e979598344c0189bfcb64"
],
"index": "pypi",
- "markers": "python_version ~= '3.7'",
- "version": "==1.6.1"
+ "markers": "python_version ~= '3.8'",
+ "version": "==1.7.0"
},
"openpyxl": {
"hashes": [
@@ -1374,49 +1382,49 @@
},
"black": {
"hashes": [
- "sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d",
- "sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd",
- "sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33",
- "sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965",
- "sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070",
- "sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397",
- "sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745",
- "sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1",
- "sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665",
- "sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436",
- "sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb",
- "sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e",
- "sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6",
- "sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702",
- "sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8",
- "sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8",
- "sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3",
- "sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad",
- "sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf",
- "sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e",
- "sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641",
- "sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2"
+ "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474",
+ "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1",
+ "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0",
+ "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8",
+ "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96",
+ "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1",
+ "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04",
+ "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021",
+ "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94",
+ "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d",
+ "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c",
+ "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7",
+ "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c",
+ "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc",
+ "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7",
+ "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d",
+ "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c",
+ "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741",
+ "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce",
+ "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb",
+ "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063",
+ "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==24.4.0"
+ "version": "==24.4.2"
},
"blinker": {
"hashes": [
- "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9",
- "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"
+ "sha256:5f1cdeff423b77c31b89de0565cd03e5275a03028f44b2b15f912632a58cced6",
+ "sha256:da44ec748222dcd0105ef975eed946da197d5bdf8bafb6aa92f5bc89da63fa25"
],
"markers": "python_version >= '3.8'",
- "version": "==1.7.0"
+ "version": "==1.8.1"
},
"boto3": {
"hashes": [
- "sha256:2824e3dd18743ca50e5b10439d20e74647b1416e8a94509cb30beac92d27a18d",
- "sha256:b2e5cb5b95efcc881e25a3bc872d7a24e75ff4e76f368138e4baf7b9d6ee3422"
+ "sha256:decf52f8d5d8a1b10c9ff2a0e96ee207ed79e33d2e53fdf0880a5cbef70785e0",
+ "sha256:e836b71d79671270fccac0a4d4c8ec239a6b82ea47c399b64675aa597d0ee63b"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==1.34.90"
+ "version": "==1.34.95"
},
"boto3-mocking": {
"hashes": [
@@ -1429,28 +1437,28 @@
},
"boto3-stubs": {
"hashes": [
- "sha256:7361f162523168ddcfb3e0cc70e5208e78f95b9f1f2553032036a2b67ab33355",
- "sha256:c82f3db8558e28f766361ba1eea7c77dff735f72fef2a0b9dffaa9c0d9ae76a3"
+ "sha256:412006b27ee707e9b51a084b02ac92b143af8a3b56727582afec2a76ce93c3b6",
+ "sha256:4fb5830626de42446c238ca72ca1a53e461281396007fb900edf50ceeb044a10"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==1.34.90"
+ "version": "==1.34.95"
},
"botocore": {
"hashes": [
- "sha256:113cd4c0cb63e13163ccbc2bb13d551be314ba7f8ba5bfab1c51a19ca01aa133",
- "sha256:d48f152498e2c60b43ce25b579d26642346a327b6fb2c632d57219e0a4f63392"
+ "sha256:6bd76a2eadb42b91fa3528392e981ad5b4dfdee3968fa5b904278acf6cbf15ff",
+ "sha256:ead5823e0dd6751ece5498cb979fd9abf190e691c8833bcac6876fd6ca261fa7"
],
"markers": "python_version >= '3.8'",
- "version": "==1.34.90"
+ "version": "==1.34.95"
},
"botocore-stubs": {
"hashes": [
- "sha256:b2d7416b524bce7325aa5fe09bb5e0b6bc9531d4136f4407fa39b6bc58507f34",
- "sha256:d9b66542cbb8fbe28eef3c22caf941ae22d36cc1ef55b93fc0b52239457cab57"
+ "sha256:64d80a3467e3b19939e9c2750af33328b3087f8f524998dbdf7ed168227f507d",
+ "sha256:b0345f55babd8b901c53804fc5c326a4a0bd2e23e3b71f9ea5d9f7663466e6ba"
],
"markers": "python_version >= '3.8' and python_version < '4.0'",
- "version": "==1.34.89"
+ "version": "==1.34.94"
},
"click": {
"hashes": [
@@ -1487,20 +1495,20 @@
},
"django-stubs": {
"hashes": [
- "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8",
- "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b"
+ "sha256:084484cbe16a6d388e80ec687e46f529d67a232f3befaf55c936b3b476be289d",
+ "sha256:b8a792bee526d6cab31e197cb414ee7fa218abd931a50948c66a80b3a2548621"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==4.2.7"
+ "version": "==5.0.0"
},
"django-stubs-ext": {
"hashes": [
- "sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c",
- "sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3"
+ "sha256:5bacfbb498a206d5938454222b843d81da79ea8b6fcd1a59003f529e775bc115",
+ "sha256:8e1334fdf0c8bff87e25d593b33d4247487338aaed943037826244ff788b56a8"
],
"markers": "python_version >= '3.8'",
- "version": "==4.2.7"
+ "version": "==5.0.0"
},
"django-webtest": {
"hashes": [
@@ -1553,37 +1561,37 @@
},
"mypy": {
"hashes": [
- "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6",
- "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913",
- "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129",
- "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc",
- "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974",
- "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374",
- "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150",
- "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03",
- "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9",
- "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02",
- "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89",
- "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2",
- "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d",
- "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3",
- "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612",
- "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e",
- "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3",
- "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e",
- "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd",
- "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04",
- "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed",
- "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185",
- "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf",
- "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b",
- "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4",
- "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f",
- "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"
+ "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061",
+ "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99",
+ "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de",
+ "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a",
+ "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9",
+ "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec",
+ "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1",
+ "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131",
+ "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f",
+ "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821",
+ "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5",
+ "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee",
+ "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e",
+ "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746",
+ "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2",
+ "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0",
+ "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b",
+ "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53",
+ "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30",
+ "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda",
+ "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051",
+ "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2",
+ "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7",
+ "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee",
+ "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727",
+ "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976",
+ "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==1.9.0"
+ "version": "==1.10.0"
},
"mypy-extensions": {
"hashes": [
@@ -1794,14 +1802,6 @@
"markers": "python_version >= '3.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": {
"hashes": [
"sha256:a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342",
diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py
index b0e6417db..41e442f2d 100644
--- a/src/djangooidc/backends.py
+++ b/src/djangooidc/backends.py
@@ -65,13 +65,31 @@ class OpenIdConnectBackend(ModelBackend):
return user
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():
- # Check if the key is not first_name or last_name or value is not empty string
- if key not in ["first_name", "last_name"] or value:
+ # Check if the field is not 'first_name', 'last_name', or 'phone',
+ # 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)
+
+ # Save the user object with the updated fields
user.save()
def clean_username(self, username):
diff --git a/src/djangooidc/tests/test_backends.py b/src/djangooidc/tests/test_backends.py
index ac7f74903..c15106fa9 100644
--- a/src/djangooidc/tests/test_backends.py
+++ b/src/djangooidc/tests/test_backends.py
@@ -50,15 +50,21 @@ class OpenIdConnectBackendTestCase(TestCase):
self.assertEqual(user.email, "john.doe@example.com")
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.
- 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
- 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
self.kwargs.pop("given_name", None)
self.kwargs.pop("family_name", None)
+ self.kwargs.pop("phone", None)
# Ensure that the authenticate method updates the existing user
# 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
# Verify that user fields are correctly updated
- self.assertEqual(user.first_name, "John")
- self.assertEqual(user.last_name, "Doe")
+ self.assertEqual(user.first_name, "WillNotBe")
+ self.assertEqual(user.last_name, "Replaced")
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.
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
- 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
# and preserves existing first and last names
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 89f8391b3..356286936 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -15,6 +15,8 @@ from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from dateutil.relativedelta import relativedelta # type: ignore
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.utility.errors import FSMDomainRequestError, FSMErrorCodes
from registrar.views.utility.mixins import OrderableFieldsMixin
@@ -1091,7 +1093,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
(
"Show details",
{
- "classes": ["collapse--dotgov"],
+ "classes": ["collapse--dgfieldset"],
"description": "Extends type of organization",
"fields": [
"federal_type",
@@ -1117,7 +1119,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
(
"Show details",
{
- "classes": ["collapse--dotgov"],
+ "classes": ["collapse--dgfieldset"],
"description": "Extends organization name and mailing address",
"fields": [
"address_line1",
@@ -1352,7 +1354,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
(
"Show details",
{
- "classes": ["collapse--dotgov"],
+ "classes": ["collapse--dgfieldset"],
"description": "Extends type of organization",
"fields": [
"federal_type",
@@ -1378,7 +1380,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
(
"Show details",
{
- "classes": ["collapse--dotgov"],
+ "classes": ["collapse--dgfieldset"],
"description": "Extends organization name and mailing address",
"fields": [
"address_line1",
@@ -2252,7 +2254,16 @@ class UserGroupAdmin(AuditedAdmin):
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.register(LogEntry, CustomLogEntryAdmin)
admin.site.register(models.User, MyUserAdmin)
# Unregister the built-in Group model
@@ -2274,3 +2285,10 @@ admin.site.register(models.PublicContact, PublicContactAdmin)
admin.site.register(models.DomainRequest, DomainRequestAdmin)
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
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)
diff --git a/src/registrar/assets/js/dja-collapse.js b/src/registrar/assets/js/dja-collapse.js
index c33954192..6fd838e2e 100644
--- a/src/registrar/assets/js/dja-collapse.js
+++ b/src/registrar/assets/js/dja-collapse.js
@@ -4,19 +4,18 @@
* Works with our fieldset override
*/
-/*global gettext*/
'use strict';
{
window.addEventListener('load', function() {
// Add anchor tag for Show/Hide link
- const fieldsets = document.querySelectorAll('fieldset.collapse--dotgov');
+ 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--dotgov usa-button usa-button--unstyled';
+ button.className = 'collapse-toggle--dgfieldset usa-button usa-button--unstyled';
}
}
// Add toggle to hide/show anchor tag
@@ -38,8 +37,40 @@
fieldset.classList.add('collapsed');
}
};
- document.querySelectorAll('.collapse-toggle--dotgov').forEach(function(el) {
+ 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);
+ });
+ });
+}
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index f38afd252..702364cba 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -394,3 +394,38 @@ function initializeWidgetOnList(list, parentId) {
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)
+ }
+})();
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index b4b590acb..9f5ea7a97 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -243,7 +243,7 @@ div#content > h2 {
// in the future
.object-tools li 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;
font-size: 14px!important;
}
@@ -293,7 +293,7 @@ div#content > h2 {
.messagelist_content-list--unstyled {
padding-left: 0;
li {
- font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
+ font-family: family('sans');
font-size: 13.92px!important;
background: none!important;
padding: 0!important;
@@ -395,7 +395,6 @@ details.dja-detail-table {
border-top: none;
border-bottom: none;
}
-
}
@@ -542,32 +541,42 @@ address.dja-address-contact-list {
}
// Collapse button styles for fieldsets
-.module.collapse--dotgov {
+.module.collapse--dgfieldset {
margin-top: -35px;
padding-top: 0;
border: none;
- button {
- background: none;
- text-transform: none;
+}
+.collapse-toggle--dgsimple,
+.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);
- 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: -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";
+ svg {
+ color: var(--link-fg);
}
}
}
-.collapse--dotgov.collapsed .collapse-toggle--dotgov {
+.collapse--dgfieldset.collapsed .collapse-toggle--dgfieldset {
display: inline-block!important;
* {
display: inline-block;
}
}
+.collapse--dgsimple.collapsed {
+ display: none;
+}
.dja-status-list {
border-top: solid 1px var(--border-color);
@@ -576,7 +585,7 @@ address.dja-address-contact-list {
padding-top: 10px;
li {
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-bottom: 0;
}
@@ -633,13 +642,16 @@ address.dja-address-contact-list {
display: inline-flex;
padding-top: 4px;
line-height: 14px;
- color: var(--link-fg);
width: max-content;
font-size: unset;
text-decoration: none !important;
}
}
+button.usa-button__clipboard {
+ color: var(--link-fg);
+}
+
.no-outline-on-click:focus {
outline: none !important;
}
@@ -676,3 +688,35 @@ form .aligned p.help, form .aligned div.help {
background: var(--primary);
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;
+}
diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py
index a18adabde..f2890cc20 100644
--- a/src/registrar/config/settings.py
+++ b/src/registrar/config/settings.py
@@ -22,7 +22,6 @@ from base64 import b64decode
from cfenv import AppEnv # type: ignore
from pathlib import Path
from typing import Final
-
from botocore.config import Config
# # # ###
@@ -150,6 +149,8 @@ INSTALLED_APPS = [
"django_admin_multiple_choice_list_filter",
# library for export and import of data
"import_export",
+ # Waffle feature flags
+ "waffle",
]
# Middleware are routines for processing web requests.
@@ -185,6 +186,8 @@ MIDDLEWARE = [
"csp.middleware.CSPMiddleware",
# django-auditlog: obtain the request User for use in logging
"auditlog.middleware.AuditlogMiddleware",
+ # Used for waffle feature flags
+ "waffle.middleware.WaffleMiddleware",
]
# application object used by Django’s built-in servers (e.g. `runserver`)
@@ -321,6 +324,17 @@ EMAIL_TIMEOUT = 30
SERVER_EMAIL = "root@get.gov"
# 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-----------------------------------------------------------###
# Content-Security-Policy configuration
@@ -644,6 +658,7 @@ ALLOWED_HOSTS = [
"getgov-stable.app.cloud.gov",
"getgov-staging.app.cloud.gov",
"getgov-development.app.cloud.gov",
+ "getgov-cb.app.cloud.gov",
"getgov-bob.app.cloud.gov",
"getgov-meoward.app.cloud.gov",
"getgov-backup.app.cloud.gov",
diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py
index 7f991fa0e..c31acacfd 100644
--- a/src/registrar/fixtures_users.py
+++ b/src/registrar/fixtures_users.py
@@ -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}")
for user_data in users:
try:
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.last_name = user_data["last_name"]
if "email" in user_data:
@@ -229,5 +229,5 @@ class UserFixture:
# steps now do not need to close/reopen a db connection,
# instead they share one.
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")
diff --git a/src/registrar/migrations/0090_waffleflag.py b/src/registrar/migrations/0090_waffleflag.py
new file mode 100644
index 000000000..3edef9a7e
--- /dev/null
+++ b/src/registrar/migrations/0090_waffleflag.py
@@ -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",
+ },
+ ),
+ ]
diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py
index d3bbb3ae5..f084a5d8b 100644
--- a/src/registrar/models/__init__.py
+++ b/src/registrar/models/__init__.py
@@ -15,6 +15,8 @@ from .user_group import UserGroup
from .website import Website
from .transition_domain import TransitionDomain
from .verified_by_staff import VerifiedByStaff
+from .waffle_flag import WaffleFlag
+
__all__ = [
"Contact",
@@ -33,6 +35,7 @@ __all__ = [
"Website",
"TransitionDomain",
"VerifiedByStaff",
+ "WaffleFlag",
]
auditlog.register(Contact)
@@ -51,3 +54,4 @@ auditlog.register(UserGroup, m2m_fields=["permissions"])
auditlog.register(Website)
auditlog.register(TransitionDomain)
auditlog.register(VerifiedByStaff)
+auditlog.register(WaffleFlag)
diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py
index 9deb22641..3ebd8bc3e 100644
--- a/src/registrar/models/contact.py
+++ b/src/registrar/models/contact.py
@@ -101,11 +101,23 @@ class Contact(TimeStampedModel):
# Call the parent class's save method to perform the actual save
super().save(*args, **kwargs)
- # Update the related User object's first_name and last_name
- if self.user and (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
- self.user.save()
+ if self.user:
+ updated = False
+
+ # Update first name and last name if necessary
+ 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):
if self.first_name or self.last_name:
diff --git a/src/registrar/models/waffle_flag.py b/src/registrar/models/waffle_flag.py
new file mode 100644
index 000000000..d185c2a82
--- /dev/null
+++ b/src/registrar/models/waffle_flag.py
@@ -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"
diff --git a/src/registrar/templates/admin/change_list.html b/src/registrar/templates/admin/change_list.html
index 05c2d4e64..43abf0861 100644
--- a/src/registrar/templates/admin/change_list.html
+++ b/src/registrar/templates/admin/change_list.html
@@ -2,6 +2,10 @@
{% block content_title %}
{{ title }}
+
+ {# Adds a model description #}
+ {% include "admin/model_descriptions.html" %}
+
{{ cl.result_count }}
{% if cl.get_ordering_field_columns %}
diff --git a/src/registrar/templates/admin/fieldset.html b/src/registrar/templates/admin/fieldset.html
index 19c5db294..89537b098 100644
--- a/src/registrar/templates/admin/fieldset.html
+++ b/src/registrar/templates/admin/fieldset.html
@@ -8,7 +8,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/