diff --git a/.github/workflows/loaddata.yaml b/.github/workflows/loaddata.yaml index 96385a7cb..e104a9f49 100644 --- a/.github/workflows/loaddata.yaml +++ b/.github/workflows/loaddata.yaml @@ -6,7 +6,7 @@ name: Reset database # cf run-task getgov-staging --wait \ # --command 'python manage.py flush' --name flush # cf run-task getgov-staging --wait \ -# --command 'python manage.py loaddata registrar/fixtures/*' --name loaddata +# --command 'python manage.py load' --name loaddata on: workflow_dispatch: inputs: @@ -24,8 +24,8 @@ jobs: - name: Delete existing data for staging uses: 18f/cg-deploy-action@main with: - cf_username: ${{ secrets.CF_USERNAME }} - cf_password: ${{ secrets.CF_PASSWORD }} + cf_username: ${{ secrets.CF_STAGING_USERNAME }} + cf_password: ${{ secrets.CF_STAGING_PASSWORD }} cf_org: cisa-getgov-prototyping cf_space: staging full_command: "cf run-task getgov-staging --wait --command 'python manage.py flush' --name flush" @@ -37,4 +37,4 @@ jobs: cf_password: ${{ secrets.CF_STAGING_PASSWORD }} cf_org: cisa-getgov-prototyping cf_space: staging - full_command: "cf run-task getgov-staging --wait --command 'python manage.py loaddata registrar/fixtures/*' --name loaddata" + full_command: "cf run-task getgov-staging --wait --command 'python manage.py load' --name loaddata" diff --git a/docs/developer/README.md b/docs/developer/README.md index 0d814250c..3e282caac 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -38,6 +38,12 @@ If you run via `docker-compose up -d`, you can get logs with `docker-compose log You can change the logging verbosity, if needed. Do a web search for "django log level". +## Mock data + +There is a `post_migrate` signal in [signals.py](../../src/registrar/signals.py) that will load the fixtures from [fixtures.py](../../src/registrar/fixtures.py), giving you some test data to play with while developing. + +See the [database-access README](./database-access.md) for information on how to pull data to update these fixtures. + ## Running tests Crash course on Docker's `run` vs `exec`: in order to run the tests inside of a container, a container must be running. If you already have a container running, you can use `exec`. If you do not, you can use `run`, which will attempt to start one. diff --git a/docs/developer/database-access.md b/docs/developer/database-access.md index 913f58590..33bc65b0e 100644 --- a/docs/developer/database-access.md +++ b/docs/developer/database-access.md @@ -24,7 +24,7 @@ cf run-task getgov-unstable --command 'python manage.py migrate' --name migrate Optionally, load data from fixtures as well ```shell -cf run-task getgov-unstable --wait --command 'python manage.py loaddata registrar/fixtures/*' --name loaddata +cf run-task getgov-unstable --wait --command 'python manage.py load' --name loaddata ``` For the `staging` environment, developers don't have credentials so we need to diff --git a/src/Pipfile b/src/Pipfile index e526ed68c..7116e723a 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -11,6 +11,7 @@ django-allow-cidr = "*" django-auditlog = "*" django-csp = "*" environs = {extras=["django"]} +Faker = "*" gunicorn = "*" oic = "*" pyjwkest = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index d4ed5e674..17a6baee2 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9e2fe58b6282514da5d054147426e561f451da29f3c743c0cd9dccf4d7dba0cc" + "sha256": "c9762342448f1a70dbe93cc496e5fb868c67a73f3e51c4440e726f492f7dc5ee" }, "pipfile-spec": 6, "requires": {}, @@ -16,11 +16,11 @@ "default": { "asgiref": { "hashes": [ - "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", - "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" + "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac", + "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506" ], "markers": "python_version >= '3.7'", - "version": "==3.5.2" + "version": "==3.6.0" }, "cachetools": { "hashes": [ @@ -125,35 +125,32 @@ }, "cryptography": { "hashes": [ - "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd", - "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db", - "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290", - "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744", - "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb", - "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d", - "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70", - "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b", - "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876", - "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083", - "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6", - "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1", - "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00", - "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b", - "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b", - "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285", - "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9", - "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0", - "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d", - "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2", - "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8", - "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee", - "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b", - "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7", - "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353", - "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c" + "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b", + "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f", + "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190", + "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f", + "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f", + "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb", + "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c", + "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773", + "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72", + "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8", + "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717", + "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9", + "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856", + "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96", + "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288", + "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39", + "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e", + "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce", + "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1", + "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de", + "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df", + "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf", + "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458" ], "markers": "python_version >= '3.6'", - "version": "==38.0.4" + "version": "==39.0.0" }, "defusedxml": { "hashes": [ @@ -179,11 +176,11 @@ }, "django": { "hashes": [ - "sha256:0b223bfa55511f950ff741983d408d78d772351284c75e9f77d2b830b6b4d148", - "sha256:d38a4e108d2386cb9637da66a82dc8d0733caede4c83c4afdbda78af4214211b" + "sha256:4b214a05fe4c99476e99e2445c8b978c8369c18d4dea8e22ec412862715ad763", + "sha256:ff56ebd7ead0fd5dbe06fe157b0024a7aaea2e0593bb3785fb594cf94dad58ef" ], "index": "pypi", - "version": "==4.1.4" + "version": "==4.1.5" }, "django-allow-cidr": { "hashes": [ @@ -262,6 +259,14 @@ "index": "pypi", "version": "==9.5.0" }, + "faker": { + "hashes": [ + "sha256:2d5443724f640ce07658ca8ca8bbd40d26b58914e63eec6549727869aa67e2cc", + "sha256:c2a2ff9dd8dfd991109b517ab98d5cb465e857acb45f6b643a0e284a9eb2cc76" + ], + "index": "pypi", + "version": "==15.3.4" + }, "furl": { "hashes": [ "sha256:5a6188fe2666c484a12159c18be97a1977a71d632ef5bb867ef15f54af39cc4e", @@ -379,10 +384,10 @@ }, "phonenumberslite": { "hashes": [ - "sha256:87218f3f85f67779007b6cfeeca59b1d370b96f9b3867347f0e6d094b1a3df64", - "sha256:8daa2fff393e291531aaa5ed4ddc123749b631d48ae7e504e8d7a4e818b3f799" + "sha256:2b7452fe69c907b7638ff4cf7f8a773a6dfce26bf32d67ebf4dc74d5e31abb79", + "sha256:b7f56b77711e6b99c7890f20f1aaa705d9fe2514215446b8426c8515c198775f" ], - "version": "==8.13.2" + "version": "==8.13.3" }, "psycopg2-binary": { "hashes": [ @@ -573,21 +578,21 @@ }, "whitenoise": { "hashes": [ - "sha256:8e9c600a5c18bd17655ef668ad55b5edf6c24ce9bdca5bf607649ca4b1e8e2c2", - "sha256:8fa943c6d4cd9e27673b70c21a07b0aa120873901e099cd46cab40f7cc96d567" + "sha256:cf8ecf56d86ba1c734fdb5ef6127312e39e92ad5947fef9033dc9e43ba2777d9", + "sha256:fe0af31504ab08faa1ec7fc02845432096e40cc1b27e6a7747263d7b30fb51fa" ], "index": "pypi", - "version": "==6.2.0" + "version": "==6.3.0" } }, "develop": { "asgiref": { "hashes": [ - "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", - "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" + "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac", + "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506" ], "markers": "python_version >= '3.7'", - "version": "==3.5.2" + "version": "==3.6.0" }, "bandit": { "hashes": [ @@ -602,7 +607,7 @@ "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30", "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==4.11.1" }, "black": { @@ -641,11 +646,11 @@ }, "django": { "hashes": [ - "sha256:0b223bfa55511f950ff741983d408d78d772351284c75e9f77d2b830b6b4d148", - "sha256:d38a4e108d2386cb9637da66a82dc8d0733caede4c83c4afdbda78af4214211b" + "sha256:4b214a05fe4c99476e99e2445c8b978c8369c18d4dea8e22ec412862715ad763", + "sha256:ff56ebd7ead0fd5dbe06fe157b0024a7aaea2e0593bb3785fb594cf94dad58ef" ], "index": "pypi", - "version": "==4.1.4" + "version": "==4.1.5" }, "django-debug-toolbar": { "hashes": [ @@ -697,18 +702,18 @@ }, "gitpython": { "hashes": [ - "sha256:41eea0deec2deea139b459ac03656f0dd28fc4a3387240ec1d3c259a2c47850f", - "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd" + "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8", + "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882" ], "markers": "python_version >= '3.7'", - "version": "==3.1.29" + "version": "==3.1.30" }, "mccabe": { "hashes": [ "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==0.7.0" }, "mypy": { @@ -780,18 +785,18 @@ }, "platformdirs": { "hashes": [ - "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca", - "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e" + "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490", + "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2" ], "markers": "python_version >= '3.7'", - "version": "==2.6.0" + "version": "==2.6.2" }, "pycodestyle": { "hashes": [ "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053", "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.10.0" }, "pyflakes": { @@ -799,7 +804,7 @@ "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf", "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==3.0.1" }, "pyyaml": { @@ -845,7 +850,7 @@ "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==6.0" }, "six": { @@ -861,7 +866,7 @@ "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94", "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==5.0.0" }, "soupsieve": { @@ -869,7 +874,7 @@ "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759", "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.3.2.post1" }, "sqlparse": { @@ -906,10 +911,10 @@ }, "types-pytz": { "hashes": [ - "sha256:bea605ce5d5a5d52a8e1afd7656c9b42476e18a0f888de6be91587355313ddf4", - "sha256:d078196374d1277e9f9984d49373ea043cf2c64d5d5c491fbc86c258557bd46f" + "sha256:1509f182f686ab76e9a8234f22b00b8f50d239974db0cf924b7ae8674bb31a6f", + "sha256:4f20c2953b3a3a0587e94489ec4c9e02c3d3aedb9ba5cd7e796e12f4cfa7027e" ], - "version": "==2022.6.0.1" + "version": "==2022.7.0.0" }, "types-pyyaml": { "hashes": [ @@ -920,11 +925,11 @@ }, "types-requests": { "hashes": [ - "sha256:091d4a5a33c1b4f20d8b1b952aa8fa27a6e767c44c3cf65e56580df0b05fd8a9", - "sha256:a7df37cc6fb6187a84097da951f8e21d335448aa2501a6b0a39cbd1d7ca9ee2a" + "sha256:0ae38633734990d019b80f5463dfa164ebd3581998ac8435f526da6fe4d598c3", + "sha256:b6a2fca8109f4fdba33052f11ed86102bddb2338519e1827387137fefc66a98b" ], "index": "pypi", - "version": "==2.28.11.5" + "version": "==2.28.11.7" }, "types-urllib3": { "hashes": [ @@ -962,7 +967,7 @@ "sha256:2a001a9efa40d2a7e5d9cd8d1527c75f41814eb6afce2c3d207402547b1e5ead", "sha256:54bd969725838d9861a9fa27f8d971f79d275d94ae255f5c501f53bb6d9929eb" ], - "markers": "python_version >= '3.6' and python_version < '4'", + "markers": "python_full_version >= '3.6.0' and python_version < '4'", "version": "==3.0.0" } } diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py new file mode 100644 index 000000000..58cfc633e --- /dev/null +++ b/src/registrar/fixtures.py @@ -0,0 +1,256 @@ +import logging +import random +import string +from faker import Faker + +from registrar.models import ( + User, + UserProfile, + DomainApplication, + Domain, + Contact, + Website, +) + +fake = Faker() +logger = logging.getLogger(__name__) + + +class UserFixture: + """ + Load users into the database. + + Make sure this class' `load` method is called from `handle` + in management/commands/load.py, then use `./manage.py load` + to run this code. + """ + + ADMINS = [ + { + "username": "c4a0e101-73b4-4d7d-9e5e-7f19a726a0fa", + "first_name": "Seamus", + "last_name": "Johnston", + }, + { + "username": "d4c3bd84-dc3a-48bc-a3c3-f53111df2ec6", + "first_name": "Igor", + "last_name": "", + }, + { + "username": "ee80bfe0-49ad-456d-8d82-e2b608a66517", + "first_name": "Logan", + "last_name": "", + }, + ] + + @classmethod + def load(cls): + logger.info("Going to load %s users" % str(len(cls.ADMINS))) + for admin in cls.ADMINS: + try: + user, _ = User.objects.get_or_create( + username=admin["username"], + ) + user.is_superuser = True + user.first_name = admin["first_name"] + user.last_name = admin["last_name"] + user.is_staff = True + user.is_active = True + user.save() + logger.debug("User object created for %s" % admin["first_name"]) + UserProfile.objects.get_or_create(user=user) + logger.debug("Profile object created for %s" % admin["first_name"]) + except Exception as e: + logger.warning(e) + logger.debug("All users loaded.") + + +class DomainApplicationFixture: + """ + Load domain applications into the database. + + Make sure this class' `load` method is called from `handle` + in management/commands/load.py, then use `./manage.py load` + to run this code. + """ + + # any fields not specified here will be filled in with fake data or defaults + # NOTE BENE: each fixture must have `organization_name` for uniqueness! + DA = [ + { + "status": "started", + "organization_name": "Example - Finished but not Submitted", + }, + { + "status": "started", + "organization_name": "Example - Just started", + "organization_type": "federal", + "federal_agency": None, + "federal_type": None, + "address_line1": None, + "address_line2": None, + "city": None, + "state_territory": None, + "zipcode": None, + "urbanization": None, + "purpose": None, + "security_email": None, + "anything_else": None, + "is_policy_acknowledged": None, + "authorizing_official": None, + "submitter": None, + "other_contacts": [], + "current_websites": [], + "alternative_domains": [], + }, + { + "status": "submitted", + "organization_name": "Example - Submitted but pending Investigation", + }, + { + "status": "investigating", + "organization_name": "Example - In Investigation", + }, + ] + + @classmethod + def fake_contact(cls): + return { + "first_name": fake.first_name(), + "middle_name": None, + "last_name": fake.last_name(), + "title": fake.job(), + "email": fake.ascii_safe_email(), + "phone": fake.phone_number(), + } + + @classmethod + def fake_dot_gov(cls): + return "".join(random.choices(string.ascii_lowercase, k=16)) + ".gov" # nosec + + @classmethod + def _set_non_foreign_key_fields(cls, da: DomainApplication, app: dict): + """Helper method used by `load`.""" + da.status = app["status"] if "status" in app else "started" + da.organization_type = ( + app["organization_type"] if "organization_type" in app else "federal" + ) + da.federal_agency = app["federal_agency"] if "federal_agency" in app else "" + da.federal_type = ( + app["federal_type"] + if "federal_type" in app + else random.choice(["executive", "judicial", "legislative"]) # nosec + ) + da.address_line1 = ( + app["address_line1"] if "address_line1" in app else fake.street_address() + ) + da.address_line2 = app["address_line2"] if "address_line2" in app else None + da.city = app["city"] if "city" in app else fake.city() + da.state_territory = ( + app["state_territory"] if "state_territory" in app else fake.state_abbr() + ) + da.zipcode = app["zipcode"] if "zipcode" in app else fake.postalcode() + da.urbanization = app["urbanization"] if "urbanization" in app else None + da.purpose = app["purpose"] if "purpose" in app else fake.paragraph() + da.security_email = app["security_email"] if "security_email" in app else None + da.anything_else = app["anything_else"] if "anything_else" in app else None + da.is_policy_acknowledged = ( + app["is_policy_acknowledged"] if "is_policy_acknowledged" in app else True + ) + + @classmethod + def _set_foreign_key_fields(cls, da: DomainApplication, app: dict, user: User): + """Helper method used by `load`.""" + if not da.investigator: + da.investigator = ( + User.objects.get(username=user.username) + if "investigator" in app + else None + ) + + if not da.authorizing_official: + if ( + "authorizing_official" in app + and app["authorizing_official"] is not None + ): + da.authorizing_official, _ = Contact.objects.get_or_create( + **app["authorizing_official"] + ) + else: + da.authorizing_official = Contact.objects.create(**cls.fake_contact()) + + if not da.submitter: + if "submitter" in app and app["submitter"] is not None: + da.submitter, _ = Contact.objects.get_or_create(**app["submitter"]) + else: + da.submitter = Contact.objects.create(**cls.fake_contact()) + + if not da.requested_domain: + if "requested_domain" in app and app["requested_domain"] is not None: + da.requested_domain, _ = Domain.objects.get_or_create( + name=app["requested_domain"] + ) + else: + da.requested_domain = Domain.objects.create(name=cls.fake_dot_gov()) + + @classmethod + def _set_many_to_many_relations(cls, da: DomainApplication, app: dict): + """Helper method used by `load`.""" + if "other_contacts" in app: + for contact in app["other_contacts"]: + da.other_contacts.add(Contact.objects.get_or_create(**contact)[0]) + else: + other_contacts = [ + Contact.objects.create(**cls.fake_contact()) + for _ in range(random.randint(0, 3)) # nosec + ] + da.other_contacts.add(*other_contacts) + + if "current_websites" in app: + for website in app["current_websites"]: + da.current_websites.add( + Website.objects.get_or_create(website=website)[0] + ) + else: + current_websites = [ + Website.objects.create(website=fake.uri()) + for _ in range(random.randint(0, 3)) # nosec + ] + da.current_websites.add(*current_websites) + + if "alternative_domains" in app: + for domain in app["alternative_domains"]: + da.alternative_domains.add( + Website.objects.get_or_create(website=domain)[0] + ) + else: + alternative_domains = [ + Website.objects.create(website=cls.fake_dot_gov()) + for _ in range(random.randint(0, 3)) # nosec + ] + da.alternative_domains.add(*alternative_domains) + + @classmethod + def load(cls): + """Creates domain applications for each user in the database.""" + logger.info("Going to load %s domain applications" % len(cls.DA)) + try: + users = list(User.objects.all()) # force evaluation to catch db errors + except Exception as e: + logger.warning(e) + return + + for user in users: + logger.debug("Loading domain applications for %s" % user) + for app in cls.DA: + try: + da, _ = DomainApplication.objects.get_or_create( + creator=user, + organization_name=app["organization_name"], + ) + cls._set_non_foreign_key_fields(da, app) + cls._set_foreign_key_fields(da, app, user) + da.save() + cls._set_many_to_many_relations(da, app) + except Exception as e: + logger.warning(e) diff --git a/src/registrar/fixtures/users.json b/src/registrar/fixtures/users.json deleted file mode 100644 index 712fde168..000000000 --- a/src/registrar/fixtures/users.json +++ /dev/null @@ -1,107 +0,0 @@ -[ - { - "model": "registrar.user", - "pk": 1, - "fields": { - "password": "", - "last_login": "2022-10-04T15:07:34.590Z", - "is_superuser": true, - "username": "c4a0e101-73b4-4d7d-9e5e-7f19a726a0fa", - "first_name": "Seamus", - "last_name": "Johnston", - "email": "", - "is_staff": true, - "is_active": true, - "date_joined": "2022-09-30T14:14:02.280Z", - "groups": [], - "user_permissions": [] - } - }, - { - "model": "registrar.userprofile", - "pk": 1, - "fields": { - "created_at": "2022-09-30T14:14:02.284Z", - "updated_at": "2022-10-04T15:07:34.593Z", - "street1": "", - "street2": "", - "street3": "", - "city": "", - "sp": "", - "pc": "", - "cc": "", - "user": 1, - "display_name": "" - } - }, - { - "model": "registrar.user", - "pk": 2, - "fields": { - "password": "", - "last_login": "2022-10-04T16:43:07.738Z", - "is_superuser": true, - "username": "d4c3bd84-dc3a-48bc-a3c3-f53111df2ec6", - "first_name": "Igor", - "last_name": "", - "email": "", - "is_staff": true, - "is_active": true, - "date_joined": "2022-10-04T16:39:30.529Z", - "groups": [], - "user_permissions": [] - } - }, - { - "model": "registrar.userprofile", - "pk": 2, - "fields": { - "created_at": "2022-10-04T16:39:30.531Z", - "updated_at": "2022-10-04T16:43:07.740Z", - "street1": "", - "street2": "", - "street3": "", - "city": "", - "sp": "", - "pc": "", - "cc": "", - "user": 2, - "display_name": "" - } - }, - { - "model": "registrar.user", - "pk": 3, - "fields": { - "password": "", - "last_login": "2022-10-13T13:57:52.133Z", - "is_superuser": true, - "username": "ee80bfe0-49ad-456d-8d82-e2b608a66517", - "first_name": "Logan", - "last_name": "", - "email": "", - "is_staff": true, - "is_active": true, - "date_joined": "2022-10-13T13:57:52.124Z", - "groups": [], - "user_permissions": [] - } - }, - { - "model": "registrar.userprofile", - "pk": 3, - "fields": { - "created_at": "2022-10-04T16:39:30.531Z", - "updated_at": "2022-10-04T16:43:07.740Z", - "street1": "", - "street2": "", - "street3": "", - "city": "", - "sp": "", - "pc": "", - "cc": "", - "user": 3, - "display_name": "" - } - } -] diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index a4a36cda2..0cfbed76c 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -12,6 +12,8 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import resolve from formtools.wizard.views import NamedUrlSessionWizardView # type: ignore +from formtools.wizard.storage.session import SessionStorage # type: ignore + from phonenumber_field.formfields import PhoneNumberField # type: ignore from registrar.models import Contact, DomainApplication, Domain @@ -232,7 +234,7 @@ class DotGovDomainForm(RegistrarForm): requested = requested[:-4] if "." in requested: raise forms.ValidationError( - "Please enter a top-level domain name without any periods.", + "Please enter a domain without any periods.", code="invalid", ) if not Domain.string_could_be_domain(requested + ".gov"): @@ -334,7 +336,10 @@ class AnythingElseForm(RegistrarForm): class RequirementsForm(RegistrarForm): is_policy_acknowledged = forms.BooleanField( - label="I read and agree to the .gov domain requirements.", + label=( + "I read and agree to the requirements for registering " + "and operating .gov domains." + ), required=False, # use field validation to enforce this ) @@ -447,6 +452,19 @@ WIZARD_CONDITIONS = { } +class TrackingStorage(SessionStorage): + + """Storage subclass that keeps track of what the current_step has been.""" + + def _set_current_step(self, step): + super()._set_current_step(step) + + step_history = self.extra_data.setdefault("step_history", []) + # can't serialize a set, so keep list entries unique + if step not in step_history: + step_history.append(step) + + class ApplicationWizard(LoginRequiredMixin, NamedUrlSessionWizardView): """Multi-page form ("wizard") for new domain applications. @@ -462,6 +480,7 @@ class ApplicationWizard(LoginRequiredMixin, NamedUrlSessionWizardView): """ form_list = FORMS + storage_name = "registrar.forms.application_wizard.TrackingStorage" def get_template_names(self): """Template for the current step. @@ -483,6 +502,14 @@ class ApplicationWizard(LoginRequiredMixin, NamedUrlSessionWizardView): """Add title information to the context for all steps.""" context = super().get_context_data(form=form, **kwargs) context["form_titles"] = TITLES + + # Add information about which steps should be unlocked + # TODO: sometimes the first step doesn't get added to the step history + # so add it here + context["visited"] = self.storage.extra_data.get("step_history", []) + [ + self.steps.first + ] + if self.steps.current == Step.ORGANIZATION_CONTACT: context["is_federal"] = self._is_federal() if self.steps.current == Step.REVIEW: diff --git a/src/registrar/management/commands/load.py b/src/registrar/management/commands/load.py new file mode 100644 index 000000000..0203a2e75 --- /dev/null +++ b/src/registrar/management/commands/load.py @@ -0,0 +1,18 @@ +import logging + +from django.core.management.base import BaseCommand +from auditlog.context import disable_auditlog # type: ignore + +from registrar.fixtures import UserFixture, DomainApplicationFixture + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + def handle(self, *args, **options): + # django-auditlog has some bugs with fixtures + # https://github.com/jazzband/django-auditlog/issues/17 + with disable_auditlog(): + UserFixture.load() + DomainApplicationFixture.load() + logger.info("All fixtures loaded.") diff --git a/src/registrar/management/commands/loaddata.py b/src/registrar/management/commands/loaddata.py deleted file mode 100644 index 7bc340bd3..000000000 --- a/src/registrar/management/commands/loaddata.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.core.management.commands import loaddata -from auditlog.context import disable_auditlog # type: ignore - - -class Command(loaddata.Command): - def handle(self, *args, **options): - # django-auditlog has some bugs with fixtures - # https://github.com/jazzband/django-auditlog/issues/17 - with disable_auditlog(): - super(Command, self).handle(*args, **options) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 110f4fac2..4795d0195 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -93,21 +93,28 @@ class DomainApplication(TimeStampedModel): ARMED_FORCES_AP = "AP", "Armed Forces Pacific (AP)" class OrganizationChoices(models.TextChoices): - FEDERAL = "federal", "Federal: a federal agency" + FEDERAL = ( + "federal", + "Federal: an agency of the U.S. government's executive, legislative, " + "or judicial branches", + ) INTERSTATE = "interstate", "Interstate: an organization of two or more states" STATE_OR_TERRITORY = "state_or_territory", ( - "State or Territory: One of the 50 U.S. states, the District of " + "State or territory: one of the 50 U.S. states, the District of " "Columbia, American Samoa, Guam, Northern Mariana Islands, " "Puerto Rico, or the U.S. Virgin Islands" ) TRIBAL = "tribal", ( "Tribal: a tribal government recognized by the federal or " - "state government" + "a state government" ) COUNTY = "county", "County: a county, parish, or borough" CITY = "city", "City: a city, town, township, village, etc." SPECIAL_DISTRICT = "special_district", ( - "Special District: an independent organization within a single state" + "Special district: an independent organization within a single state" + ) + SCHOOL_DISTRICT = "school_district", ( + "School district: a school district that is not part of a local government" ) class BranchChoices(models.TextChoices): @@ -288,7 +295,7 @@ class DomainApplication(TimeStampedModel): federal_agency = models.TextField( null=True, blank=True, - help_text="Top level federal agency", + help_text="Federal agency", ) federal_type = models.CharField( @@ -296,7 +303,7 @@ class DomainApplication(TimeStampedModel): choices=BranchChoices.choices, null=True, blank=True, - help_text="Branch of federal government", + help_text="Federal government branch", ) is_election_board = models.BooleanField( @@ -314,13 +321,13 @@ class DomainApplication(TimeStampedModel): address_line1 = models.TextField( null=True, blank=True, - help_text="Address line 1", + help_text="Street address", ) address_line2 = models.CharField( max_length=15, null=True, blank=True, - help_text="Address line 2", + help_text="Street address line 2", ) city = models.TextField( null=True, @@ -331,19 +338,19 @@ class DomainApplication(TimeStampedModel): max_length=2, null=True, blank=True, - help_text="State/Territory", + help_text="State, territory, or military post", ) zipcode = models.CharField( max_length=10, null=True, blank=True, - help_text="ZIP code", + help_text="Zip code", db_index=True, ) urbanization = models.TextField( null=True, blank=True, - help_text="Urbanization", + help_text="Urbanization (Puerto Rico only)", ) authorizing_official = models.ForeignKey( @@ -388,7 +395,7 @@ class DomainApplication(TimeStampedModel): purpose = models.TextField( null=True, blank=True, - help_text="Purpose of the domain", + help_text="Purpose of your domain", ) other_contacts = models.ManyToManyField( diff --git a/src/registrar/signals.py b/src/registrar/signals.py index a39a90397..ee9d08612 100644 --- a/src/registrar/signals.py +++ b/src/registrar/signals.py @@ -1,9 +1,16 @@ -from django.db.models.signals import post_save +import logging + +from django.conf import settings +from django.core.management import call_command +from django.db.models.signals import post_save, post_migrate from django.dispatch import receiver from .models import User, UserProfile +logger = logging.getLogger(__name__) + + @receiver(post_save, sender=User) def handle_profile(sender, instance, **kwargs): @@ -21,3 +28,13 @@ def handle_profile(sender, instance, **kwargs): instance.userprofile.save() else: UserProfile.objects.create(user=instance) + + +@receiver(post_migrate) +def handle_loaddata(**kwargs): + """Attempt to load test fixtures when in DEBUG mode.""" + if settings.DEBUG: + try: + call_command("load") + except Exception as e: + logger.warning(e) diff --git a/src/registrar/templates/application_authorizing_official.html b/src/registrar/templates/application_authorizing_official.html index b26dfde75..6af18991a 100644 --- a/src/registrar/templates/application_authorizing_official.html +++ b/src/registrar/templates/application_authorizing_official.html @@ -6,7 +6,7 @@ {% block form_content %} -
Your authorizing official is the person within your organization who can authorize your domain request. This is generally the highest ranking or highest elected official in your organization. Read more about who can serve as an authorizing official. @@ -27,7 +27,7 @@