Merge branch 'main' of https://github.com/cisagov/getgov into jon/594

This commit is contained in:
Jon Roberts 2023-06-02 11:25:16 -06:00
commit 20c66de5dd
No known key found for this signature in database
GPG key ID: EED093582198B041
353 changed files with 1080 additions and 19637 deletions

View file

@ -23,6 +23,9 @@ requests = "*"
django-fsm = "*"
django-phonenumber-field = {extras = ["phonenumberslite"], version = "*"}
boto3 = "*"
typing-extensions ='*'
fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"}
[dev-packages]
django-debug-toolbar = "*"

394
src/Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "b6c1a957da6c715c734906059a81da21cb0eb4c4ab04f204eb58a48ddb8f7234"
"sha256": "fd7d0efa9a87dfe4b2bb228ee0e7978fba16c7cfdd3c443870900cfe899e2cfd"
},
"pipfile-spec": 6,
"requires": {},
@ -16,35 +16,35 @@
"default": {
"asgiref": {
"hashes": [
"sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac",
"sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"
"sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e",
"sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"
],
"markers": "python_version >= '3.7'",
"version": "==3.6.0"
"version": "==3.7.2"
},
"boto3": {
"hashes": [
"sha256:62285ecee7629a4388d55ae369536f759622d68d5b9a0ced7c58a0c1a409c0f7",
"sha256:8ff0af0b25266a01616396abc19eb34dc3d44bd867fa4158985924128b9034fb"
"sha256:30f8ab1cf89d5864a80ba2d5eb5316dbd2a63c9469877e0cffb522630438aa85",
"sha256:77e8fa7c257f9ed8bfe0c3ffc2ccc47b1cfa27058f99415b6003699d1202e0c0"
],
"index": "pypi",
"version": "==1.26.133"
"version": "==1.26.145"
},
"botocore": {
"hashes": [
"sha256:7b38e540f73c921d8cb0ac72794072000af9e10758c04ba7f53d5629cc52fa87",
"sha256:b266185d7414a559952569005009a400de50af91fd3da44f05cf05b00946c4a7"
"sha256:264a3f19ed280d80711b7e278be09acff7ed379a96432fdf179b4e6e3a687e6a",
"sha256:65e2a2b1cc70583225f87d6d63736215f93c6234721967bdab872270ba7a1f45"
],
"markers": "python_version >= '3.7'",
"version": "==1.29.133"
"version": "==1.29.145"
},
"cachetools": {
"hashes": [
"sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14",
"sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"
"sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590",
"sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"
],
"index": "pypi",
"version": "==5.3.0"
"version": "==5.3.1"
},
"certifi": {
"hashes": [
@ -214,28 +214,28 @@
},
"cryptography": {
"hashes": [
"sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440",
"sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288",
"sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b",
"sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958",
"sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b",
"sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d",
"sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a",
"sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404",
"sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b",
"sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e",
"sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2",
"sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c",
"sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b",
"sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9",
"sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b",
"sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636",
"sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99",
"sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e",
"sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9"
"sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db",
"sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a",
"sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039",
"sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c",
"sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3",
"sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485",
"sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c",
"sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca",
"sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5",
"sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5",
"sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3",
"sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb",
"sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43",
"sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31",
"sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc",
"sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b",
"sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006",
"sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a",
"sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699"
],
"markers": "python_version >= '3.6'",
"version": "==40.0.2"
"markers": "python_version >= '3.7'",
"version": "==41.0.1"
},
"defusedxml": {
"hashes": [
@ -277,11 +277,11 @@
},
"django-auditlog": {
"hashes": [
"sha256:189bcc486e5c5720fac3070ac9bf1c3911f45618ed85a30dfa7d272324ef5e9a",
"sha256:39008937a7161ff0fce75f5e3c028240049c64fb1b20f4caa325c5c835e28802"
"sha256:7bc2c87e4aff62dec9785d1b2359a2b27148f8c286f8a52b9114fc7876c5a9f7",
"sha256:b9d3acebb64f3f2785157efe3f2f802e0929aafc579d85bbfb9827db4adab532"
],
"index": "pypi",
"version": "==2.2.2"
"version": "==2.3.0"
},
"django-cache-url": {
"hashes": [
@ -338,11 +338,15 @@
},
"faker": {
"hashes": [
"sha256:38dbc3b80e655d7301e190426ab30f04b6b7f6ca4764c5dd02772ffde0fa6dcd",
"sha256:f02c6d3fdb5bc781f80b440cf2bdec336ed47ecfb8d620b20c3d4188ed051831"
"sha256:a70de9ec7a14a02d278755a11134baa5a297bb82600f115022d0d07080a9e77a",
"sha256:dd15fa165ced55f668fbb0ad20ece98ab78ddacd58dc056950d66980ff61fa79"
],
"index": "pypi",
"version": "==18.7.0"
"version": "==18.10.0"
},
"fred-epplib": {
"git": "https://github.com/cisagov/epplib.git",
"ref": "f818cbf0b069a12f03e1d72e4b9f4900924b832d"
},
"furl": {
"hashes": [
@ -382,6 +386,89 @@
"markers": "python_version >= '3.7'",
"version": "==1.0.1"
},
"lxml": {
"hashes": [
"sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7",
"sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726",
"sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03",
"sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140",
"sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a",
"sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05",
"sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03",
"sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419",
"sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4",
"sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e",
"sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67",
"sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50",
"sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894",
"sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf",
"sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947",
"sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1",
"sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd",
"sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3",
"sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92",
"sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3",
"sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457",
"sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74",
"sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf",
"sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1",
"sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4",
"sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975",
"sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5",
"sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe",
"sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7",
"sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1",
"sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2",
"sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409",
"sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f",
"sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f",
"sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5",
"sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24",
"sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e",
"sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4",
"sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a",
"sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c",
"sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de",
"sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f",
"sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b",
"sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5",
"sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7",
"sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a",
"sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c",
"sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9",
"sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e",
"sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab",
"sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941",
"sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5",
"sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45",
"sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7",
"sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892",
"sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746",
"sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c",
"sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53",
"sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe",
"sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184",
"sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38",
"sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df",
"sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9",
"sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b",
"sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2",
"sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0",
"sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda",
"sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b",
"sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5",
"sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380",
"sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33",
"sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8",
"sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1",
"sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889",
"sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9",
"sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f",
"sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.9.2"
},
"mako": {
"hashes": [
"sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818",
@ -456,11 +543,11 @@
},
"oic": {
"hashes": [
"sha256:1bb80d7717faed750f1d8d482c06616a45f1fa5b2295d0620049040b4b48b97e",
"sha256:6cd66c5203baa8cd90dd2b73d0cc83785e96c91fab67fa7b587549fcadb3c092"
"sha256:2de3b83f1299dda8ed0460baad8bb2d4c6ac8bfc08a220c768b7e6e754caf9e7",
"sha256:f82e087e0ffaba2194ebd24694721a25167d5d467fb06bf4f4da9f48d43e0cc6"
],
"index": "pypi",
"version": "==1.5.0"
"version": "==1.6.0"
},
"orderedmultidict": {
"hashes": [
@ -479,10 +566,10 @@
},
"phonenumberslite": {
"hashes": [
"sha256:90e7ad011dc571c9ba76a0816d7fc92a7de8944dcb273f074edfb48f20f18f75",
"sha256:9d8162427baa4a0fdcb4902c5ca5936d2c165374c0dc6693227c68e5852d5c88"
"sha256:670a3e1ea775a9fad89d843d2d18a0e2bb0c3719e1a6bb2b96fb12f7ddd579a0",
"sha256:d1d23707dbde2b8b6940f80b181638a3e10334ff2f122c7165ce43e91c6639fe"
],
"version": "==8.13.11"
"version": "==8.13.13"
},
"psycopg2-binary": {
"hashes": [
@ -561,42 +648,83 @@
},
"pycryptodomex": {
"hashes": [
"sha256:0af93aad8d62e810247beedef0261c148790c52f3cd33643791cc6396dd217c1",
"sha256:12056c38e49d972f9c553a3d598425f8a1c1d35b2e4330f89d5ff1ffb70de041",
"sha256:23d83b610bd97704f0cd3acc48d99b76a15c8c1540d8665c94d514a49905bad7",
"sha256:2d4d395f109faba34067a08de36304e846c791808524614c731431ee048fe70a",
"sha256:32e764322e902bbfac49ca1446604d2839381bbbdd5a57920c9daaf2e0b778df",
"sha256:3c2516b42437ae6c7a29ef3ddc73c8d4714e7b6df995b76be4695bbe4b3b5cd2",
"sha256:40e8a11f578bd0851b02719c862d55d3ee18d906c8b68a9c09f8c564d6bb5b92",
"sha256:4b51e826f0a04d832eda0790bbd0665d9bfe73e5a4d8ea93b6a9b38beeebe935",
"sha256:4c4674f4b040321055c596aac926d12f7f6859dfe98cd12f4d9453b43ab6adc8",
"sha256:55eed98b4150a744920597c81b3965b632038781bab8a08a12ea1d004213c600",
"sha256:599bb4ae4bbd614ca05f49bd4e672b7a250b80b13ae1238f05fd0f09d87ed80a",
"sha256:5c23482860302d0d9883404eaaa54b0615eefa5274f70529703e2c43cc571827",
"sha256:64b876d57cb894b31056ad8dd6a6ae1099b117ae07a3d39707221133490e5715",
"sha256:67a3648025e4ddb72d43addab764336ba2e670c8377dba5dd752e42285440d31",
"sha256:6feedf4b0e36b395329b4186a805f60f900129cdf0170e120ecabbfcb763995d",
"sha256:78f0ddd4adc64baa39b416f3637aaf99f45acb0bcdc16706f0cc7ebfc6f10109",
"sha256:7a6651a07f67c28b6e978d63aa3a3fccea0feefed9a8453af3f7421a758461b7",
"sha256:7a8dc3ee7a99aae202a4db52de5a08aa4d01831eb403c4d21da04ec2f79810db",
"sha256:7cc28dd33f1f3662d6da28ead4f9891035f63f49d30267d3b41194c8778997c8",
"sha256:7fa0b52df90343fafe319257b31d909be1d2e8852277fb0376ba89d26d2921db",
"sha256:88b0d5bb87eaf2a31e8a759302b89cf30c97f2f8ca7d83b8c9208abe8acb447a",
"sha256:a4fa037078e92c7cc49f6789a8bac3de06856740bb2038d05f2d9a2e4b165d59",
"sha256:a57e3257bacd719769110f1f70dd901c5b6955e9596ad403af11a3e6e7e3311c",
"sha256:ab33c2d9f275e05e235dbca1063753b5346af4a5cac34a51fa0da0d4edfb21d7",
"sha256:c84689c73358dfc23f9fdcff2cb9e7856e65e2ce3b5ed8ff630d4c9bdeb1867b",
"sha256:c92537b596bd5bffb82f8964cabb9fef1bca8a28a9e0a69ffd3ec92a4a7ad41b",
"sha256:caa937ff29d07a665dfcfd7a84f0d4207b2ebf483362fa9054041d67fdfacc20",
"sha256:d38ab9e53b1c09608ba2d9b8b888f1e75d6f66e2787e437adb1fecbffec6b112",
"sha256:d4cf0128da167562c49b0e034f09e9cedd733997354f2314837c2fa461c87bb1",
"sha256:db23d7341e21b273d2440ec6faf6c8b1ca95c8894da612e165be0b89a8688340",
"sha256:ee8bf4fdcad7d66beb744957db8717afc12d176e3fd9c5d106835133881a049b",
"sha256:f854c8476512cebe6a8681cc4789e4fcff6019c17baa0fd72b459155dc605ab4",
"sha256:fd29d35ac80755e5c0a99d96b44fb9abbd7e871849581ea6a4cb826d24267537"
"sha256:160a39a708c36fa0b168ab79386dede588e62aec06eb505add870739329aecc6",
"sha256:192306cf881fe3467dda0e174a4f47bb3a8bb24b90c9cdfbdc248eec5fc0578c",
"sha256:1949e09ea49b09c36d11a951b16ff2a05a0ffe969dda1846e4686ee342fe8646",
"sha256:215be2980a6b70704c10796dd7003eb4390e7be138ac6fb8344bf47e71a8d470",
"sha256:27072a494ce621cc7a9096bbf60ed66826bb94db24b49b7359509e7951033e74",
"sha256:2dc4eab20f4f04a2d00220fdc9258717b82d31913552e766d5f00282c031b70a",
"sha256:302a8f37c224e7b5d72017d462a2be058e28f7be627bdd854066e16722d0fc0c",
"sha256:3d9314ac785a5b75d5aaf924c5f21d6ca7e8df442e5cf4f0fefad4f6e284d422",
"sha256:3e3ecb5fe979e7c1bb0027e518340acf7ee60415d79295e5251d13c68dde576e",
"sha256:4d9379c684efea80fdab02a3eb0169372bca7db13f9332cb67483b8dc8b67c37",
"sha256:50308fcdbf8345e5ec224a5502b4215178bdb5e95456ead8ab1a69ffd94779cb",
"sha256:5594a125dae30d60e94f37797fc67ce3c744522de7992c7c360d02fdb34918f8",
"sha256:58fc0aceb9c961b9897facec9da24c6a94c5db04597ec832060f53d4d6a07196",
"sha256:6421d23d6a648e83ba2670a352bcd978542dad86829209f59d17a3f087f4afef",
"sha256:6875eb8666f68ddbd39097867325bd22771f595b4e2b0149739b5623c8bf899b",
"sha256:6ed3606832987018615f68e8ed716a7065c09a0fe94afd7c9ca1b6777f0ac6eb",
"sha256:71687eed47df7e965f6e0bf3cadef98f368d5221f0fb89d2132effe1a3e6a194",
"sha256:73d64b32d84cf48d9ec62106aa277dbe99ab5fbfd38c5100bc7bddd3beb569f7",
"sha256:75672205148bdea34669173366df005dbd52be05115e919551ee97171083423d",
"sha256:76f0a46bee539dae4b3dfe37216f678769349576b0080fdbe431d19a02da42ff",
"sha256:8ff129a5a0eb5ff16e45ca4fa70a6051da7f3de303c33b259063c19be0c43d35",
"sha256:ac614363a86cc53d8ba44b6c469831d1555947e69ab3276ae8d6edc219f570f7",
"sha256:ba95abd563b0d1b88401658665a260852a8e6c647026ee6a0a65589287681df8",
"sha256:bbdcce0a226d9205560a5936b05208c709b01d493ed8307792075dedfaaffa5f",
"sha256:bec6c80994d4e7a38312072f89458903b65ec99bed2d65aa4de96d997a53ea7a",
"sha256:c2953afebf282a444c51bf4effe751706b4d0d63d7ca2cc51db21f902aa5b84e",
"sha256:d35a8ffdc8b05e4b353ba281217c8437f02c57d7233363824e9d794cf753c419",
"sha256:d56c9ec41258fd3734db9f5e4d2faeabe48644ba9ca23b18e1839b3bdf093222",
"sha256:d84e105787f5e5d36ec6a581ff37a1048d12e638688074b2a00bcf402f9aa1c2",
"sha256:e00a4bacb83a2627e8210cb353a2e31f04befc1155db2976e5e239dd66482278",
"sha256:f237278836dda412a325e9340ba2e6a84cb0f56b9244781e5b61f10b3905de88",
"sha256:f9ab5ef0718f6a8716695dea16d83b671b22c45e9c0c78fd807c32c0192e54b5"
],
"index": "pypi",
"version": "==3.17"
"version": "==3.18.0"
},
"pydantic": {
"hashes": [
"sha256:052d8654cb65174d6f9490cc9b9a200083a82cf5c3c5d3985db765757eb3b375",
"sha256:0c6fafa0965b539d7aab0a673a046466d23b86e4b0e8019d25fd53f4df62c277",
"sha256:1243d28e9b05003a89d72e7915fdb26ffd1d39bdd39b00b7dbe4afae4b557f9d",
"sha256:12f7b0bf8553e310e530e9f3a2f5734c68699f42218bf3568ef49cd9b0e44df4",
"sha256:1410275520dfa70effadf4c21811d755e7ef9bb1f1d077a21958153a92c8d9ca",
"sha256:16f8c3e33af1e9bb16c7a91fc7d5fa9fe27298e9f299cff6cb744d89d573d62c",
"sha256:17aef11cc1b997f9d574b91909fed40761e13fac438d72b81f902226a69dac01",
"sha256:191ba419b605f897ede9892f6c56fb182f40a15d309ef0142212200a10af4c18",
"sha256:1952526ba40b220b912cdc43c1c32bcf4a58e3f192fa313ee665916b26befb68",
"sha256:1ced8375969673929809d7f36ad322934c35de4af3b5e5b09ec967c21f9f7887",
"sha256:2e4148e635994d57d834be1182a44bdb07dd867fa3c2d1b37002000646cc5459",
"sha256:34d327c81e68a1ecb52fe9c8d50c8a9b3e90d3c8ad991bfc8f953fb477d42fb4",
"sha256:35db5301b82e8661fa9c505c800d0990bc14e9f36f98932bb1d248c0ac5cada5",
"sha256:3e59417ba8a17265e632af99cc5f35ec309de5980c440c255ab1ca3ae96a3e0e",
"sha256:42aa0c4b5c3025483240a25b09f3c09a189481ddda2ea3a831a9d25f444e03c1",
"sha256:666bdf6066bf6dbc107b30d034615d2627e2121506c555f73f90b54a463d1f33",
"sha256:66a703d1983c675a6e0fed8953b0971c44dba48a929a2000a493c3772eb61a5a",
"sha256:6a82d6cda82258efca32b40040228ecf43a548671cb174a1e81477195ed3ed56",
"sha256:6f2e754d5566f050954727c77f094e01793bcb5725b663bf628fa6743a5a9108",
"sha256:7456eb22ed9aaa24ff3e7b4757da20d9e5ce2a81018c1b3ebd81a0b88a18f3b2",
"sha256:7b1f6cb446470b7ddf86c2e57cd119a24959af2b01e552f60705910663af09a4",
"sha256:7d5b8641c24886d764a74ec541d2fc2c7fb19f6da2a4001e6d580ba4a38f7878",
"sha256:84d80219c3f8d4cad44575e18404099c76851bc924ce5ab1c4c8bb5e2a2227d0",
"sha256:88f195f582851e8db960b4a94c3e3ad25692c1c1539e2552f3df7a9e972ef60e",
"sha256:93e6bcfccbd831894a6a434b0aeb1947f9e70b7468f274154d03d71fabb1d7c6",
"sha256:93e766b4a8226e0708ef243e843105bf124e21331694367f95f4e3b4a92bbb3f",
"sha256:ab523c31e22943713d80d8d342d23b6f6ac4b792a1e54064a8d0cf78fd64e800",
"sha256:bb14388ec45a7a0dc429e87def6396f9e73c8c77818c927b6a60706603d5f2ea",
"sha256:c0ab53b609c11dfc0c060d94335993cc2b95b2150e25583bec37a49b2d6c6c3f",
"sha256:c33b60054b2136aef8cf190cd4c52a3daa20b2263917c49adad20eaf381e823b",
"sha256:ceb6a23bf1ba4b837d0cfe378329ad3f351b5897c8d4914ce95b85fba96da5a1",
"sha256:d532bf00f381bd6bc62cabc7d1372096b75a33bc197a312b03f5838b4fb84edd",
"sha256:df7800cb1984d8f6e249351139667a8c50a379009271ee6236138a22a0c0f319",
"sha256:e82d4566fcd527eae8b244fa952d99f2ca3172b7e97add0b43e2d97ee77f81ab",
"sha256:f90c1e29f447557e9e26afb1c4dbf8768a10cc676e3781b6a577841ade126b85",
"sha256:f9613fadad06b4f3bc5db2653ce2f22e0de84a7c6c293909b48f6ed37b83c61f"
],
"markers": "python_version >= '3.7'",
"version": "==1.10.8"
},
"pyjwkest": {
"hashes": [
@ -623,11 +751,11 @@
},
"requests": {
"hashes": [
"sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294",
"sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4"
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
"sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
],
"index": "pypi",
"version": "==2.30.0"
"version": "==2.31.0"
},
"s3transfer": {
"hashes": [
@ -639,11 +767,11 @@
},
"setuptools": {
"hashes": [
"sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b",
"sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990"
"sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f",
"sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"
],
"markers": "python_version >= '3.7'",
"version": "==67.7.2"
"version": "==67.8.0"
},
"six": {
"hashes": [
@ -663,19 +791,19 @@
},
"typing-extensions": {
"hashes": [
"sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb",
"sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"
"sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c",
"sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"
],
"markers": "python_version >= '3.7'",
"version": "==4.5.0"
"index": "pypi",
"version": "==4.6.2"
},
"urllib3": {
"hashes": [
"sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305",
"sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"
"sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f",
"sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.26.15"
"version": "==1.26.16"
},
"whitenoise": {
"hashes": [
@ -689,11 +817,11 @@
"develop": {
"asgiref": {
"hashes": [
"sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac",
"sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"
"sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e",
"sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"
],
"markers": "python_version >= '3.7'",
"version": "==3.6.0"
"version": "==3.7.2"
},
"bandit": {
"hashes": [
@ -752,11 +880,11 @@
},
"boto3": {
"hashes": [
"sha256:62285ecee7629a4388d55ae369536f759622d68d5b9a0ced7c58a0c1a409c0f7",
"sha256:8ff0af0b25266a01616396abc19eb34dc3d44bd867fa4158985924128b9034fb"
"sha256:30f8ab1cf89d5864a80ba2d5eb5316dbd2a63c9469877e0cffb522630438aa85",
"sha256:77e8fa7c257f9ed8bfe0c3ffc2ccc47b1cfa27058f99415b6003699d1202e0c0"
],
"index": "pypi",
"version": "==1.26.133"
"version": "==1.26.145"
},
"boto3-mocking": {
"hashes": [
@ -768,27 +896,27 @@
},
"boto3-stubs": {
"hashes": [
"sha256:a921814574761842073822dc5e9fc7ca4f1c5fdeaa53d83cd8831e060dae09c8",
"sha256:cc6a662700e755c1e3dec2383c146b89cd8c70b5921033504bfb8367d03a538f"
"sha256:9413cb395c803d5b85e9ec7b16fba855a613ecd78b2e0011e2f6b62cf0b4fc1e",
"sha256:be2007f92138781288c7a22eba30b7d60742466fc28edd04637b31fabee854a5"
],
"index": "pypi",
"version": "==1.26.133"
"version": "==1.26.145"
},
"botocore": {
"hashes": [
"sha256:7b38e540f73c921d8cb0ac72794072000af9e10758c04ba7f53d5629cc52fa87",
"sha256:b266185d7414a559952569005009a400de50af91fd3da44f05cf05b00946c4a7"
"sha256:264a3f19ed280d80711b7e278be09acff7ed379a96432fdf179b4e6e3a687e6a",
"sha256:65e2a2b1cc70583225f87d6d63736215f93c6234721967bdab872270ba7a1f45"
],
"markers": "python_version >= '3.7'",
"version": "==1.29.133"
"version": "==1.29.145"
},
"botocore-stubs": {
"hashes": [
"sha256:5f6f1967d23c45834858a055cbf65b66863f9f28d05f32f57bf52864a13512d9",
"sha256:622c4a5cd740498439008d81c5ded612146f4f0d575341c12591f978edbbe733"
"sha256:80ffab72ad428d20cb1cf538ee55fcd94f7d81315b77d84fec99e218c3974e8b",
"sha256:928c58a434dd83bef956e3b5bb1e96278fff5eee9f8b8ab08d916cef1e9a2014"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==1.29.130"
"version": "==1.29.145"
},
"click": {
"hashes": [
@ -808,11 +936,11 @@
},
"django-debug-toolbar": {
"hashes": [
"sha256:89619f6e0ea1057dca47bfc429ed99b237ef70074dabc065a7faa5f00e1459cf",
"sha256:bad339d68520652ddc1580c76f136fcbc3e020fd5ed96510a89a02ec81bb3fb1"
"sha256:a0b532ef5d52544fd745d1dcfc0557fa75f6f0d1962a8298bd568427ef2fa436",
"sha256:f57882e335593cb8e74c2bda9f1116bbb9ca8fc0d81b50a75ace0f83de5173c7"
],
"index": "pypi",
"version": "==4.0.0"
"version": "==4.1.0"
},
"django-model2puml": {
"hashes": [
@ -1061,11 +1189,11 @@
},
"rich": {
"hashes": [
"sha256:2d11b9b8dd03868f09b4fffadc84a6a8cda574e40dc90821bd845720ebb8e89c",
"sha256:69cdf53799e63f38b95b9bf9c875f8c90e78dd62b2f00c13a911c7a3b9fa4704"
"sha256:76f6b65ea7e5c5d924ba80e322231d7cb5b5981aa60bfc1e694f1bc097fe6fe1",
"sha256:d204aadb50b936bf6b1a695385429d192bc1fdaf3e8b907e8e26f4c4e4b5bf75"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==13.3.5"
"version": "==13.4.1"
},
"s3transfer": {
"hashes": [
@ -1109,11 +1237,11 @@
},
"stevedore": {
"hashes": [
"sha256:2c428d2338976279e8eb2196f7a94910960d9f7ba2f41f3988511e95ca447021",
"sha256:bd5a71ff5e5e5f5ea983880e4a1dd1bb47f8feebbb3d95b592398e2f02194771"
"sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d",
"sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c"
],
"markers": "python_version >= '3.8'",
"version": "==5.0.0"
"version": "==5.1.0"
},
"tomli": {
"hashes": [
@ -1125,11 +1253,11 @@
},
"types-awscrt": {
"hashes": [
"sha256:9e447df3ad46767887d14fa9c856df94f80e8a0a7f0169577ab23b52ee37bcdf",
"sha256:e28fb3f20568ce9e96e33e01e0b87b891822f36b8f368adb582553b016d4aa08"
"sha256:50fe7610aa40550a23d79d6167b2b8536281f038f42846eda0e520d6e3e01787",
"sha256:763d8d543f145d51cd16ea407d079608b126941d24525e16cb1de31d949ff563"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==0.16.17"
"version": "==0.16.19"
},
"types-cachetools": {
"hashes": [
@ -1148,18 +1276,18 @@
},
"types-pyyaml": {
"hashes": [
"sha256:5aed5aa66bd2d2e158f75dda22b059570ede988559f030cf294871d3b647e3e8",
"sha256:c51b1bd6d99ddf0aa2884a7a328810ebf70a4262c292195d3f4f9a0005f9eeb6"
"sha256:662fa444963eff9b68120d70cda1af5a5f2aa57900003c2006d7626450eaae5f",
"sha256:ebab3d0700b946553724ae6ca636ea932c1b0868701d4af121630e78d695fc97"
],
"version": "==6.0.12.9"
"version": "==6.0.12.10"
},
"types-requests": {
"hashes": [
"sha256:c6cf08e120ca9f0dc4fa4e32c3f953c3fba222bcc1db6b97695bce8da1ba9864",
"sha256:dec781054324a70ba64430ae9e62e7e9c8e4618c185a5cb3f87a6738251b5a31"
"sha256:3de667cffa123ce698591de0ad7db034a5317457a596eb0b4944e5a9d9e8d1ac",
"sha256:afb06ef8f25ba83d59a1d424bd7a5a939082f94b94e90ab5e6116bd2559deaa3"
],
"index": "pypi",
"version": "==2.30.0.0"
"version": "==2.31.0.1"
},
"types-s3transfer": {
"hashes": [
@ -1178,19 +1306,19 @@
},
"typing-extensions": {
"hashes": [
"sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb",
"sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"
"sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c",
"sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"
],
"markers": "python_version >= '3.7'",
"version": "==4.5.0"
"index": "pypi",
"version": "==4.6.2"
},
"urllib3": {
"hashes": [
"sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305",
"sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"
"sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f",
"sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.26.15"
"version": "==1.26.16"
},
"waitress": {
"hashes": [

View file

@ -27,6 +27,8 @@ services:
- DJANGO_DEBUG=True
# Tell Django where it is being hosted
- DJANGO_BASE_URL=http://localhost:8080
# Public site URL link
- GETGOV_PUBLIC_SITE_URL=https://beta.get.gov
# Set a username for accessing the registry
- REGISTRY_CL_ID=nothing
# Set a password for accessing the registry

View file

@ -77,6 +77,7 @@ h2 {
font-weight: font-weight('semibold');
line-height: line-height('heading', 3);
margin: units(4) 0 units(1);
color: color('primary-darker');
}
.register-form-step > h1 {
@ -431,4 +432,4 @@ abbr[title] {
@include at-media('tablet') {
height: units('mobile');
}
}
}

View file

@ -62,6 +62,8 @@ secret_registry_key = b64decode(secret("REGISTRY_KEY", ""))
secret_registry_key_passphrase = secret("REGISTRY_KEY_PASSPHRASE", "")
secret_registry_hostname = secret("REGISTRY_HOSTNAME")
secret_getgov_public_site_url = secret("GETGOV_PUBLIC_SITE_URL", "")
# region: Basic Django Config-----------------------------------------------###
# Build paths inside the project like this: BASE_DIR / "subdir".
@ -505,6 +507,10 @@ ROOT_URLCONF = "registrar.config.urls"
# Must be relative and end with "/"
STATIC_URL = "public/"
# Base URL of our separate static public website. Used by the
# {% public_site_url subdir/path %} template tag
GETGOV_PUBLIC_SITE_URL = secret_getgov_public_site_url
# endregion
# region: Registry----------------------------------------------------------###

View file

@ -83,6 +83,16 @@ urlpatterns = [
views.DomainNameserversView.as_view(),
name="domain-nameservers",
),
path(
"domain/<int:pk>/your-contact-information",
views.DomainYourContactInformationView.as_view(),
name="domain-your-contact-information",
),
path(
"domain/<int:pk>/authorizing-official",
views.DomainAuthorizingOfficialView.as_view(),
name="domain-authorizing-official",
),
path(
"domain/<int:pk>/security-email",
views.DomainSecurityEmailView.as_view(),

View file

@ -1,2 +1,7 @@
from .application_wizard import *
from .domain import DomainAddUserForm, NameserverFormset, DomainSecurityEmailForm
from .domain import (
DomainAddUserForm,
NameserverFormset,
DomainSecurityEmailForm,
ContactForm,
)

View file

@ -3,6 +3,10 @@
from django import forms
from django.forms import formset_factory
from phonenumber_field.widgets import RegionalPhoneNumberWidget
from ..models import Contact
class DomainAddUserForm(forms.Form):
@ -24,6 +28,37 @@ NameserverFormset = formset_factory(
)
class ContactForm(forms.ModelForm):
"""Form for updating contacts."""
class Meta:
model = Contact
fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"]
widgets = {
"first_name": forms.TextInput,
"middle_name": forms.TextInput,
"last_name": forms.TextInput,
"title": forms.TextInput,
"email": forms.EmailInput,
"phone": RegionalPhoneNumberWidget,
}
# the database fields have blank=True so ModelForm doesn't create
# required fields by default. Use this list in __init__ to mark each
# of these fields as required
required = ["first_name", "last_name", "title", "email", "phone"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# take off maxlength attribute for the phone number field
# which interferes with out input_with_errors template tag
self.fields["phone"].widget.attrs.pop("maxlength", None)
for field_name in self.required:
self.fields[field_name].required = True
class DomainSecurityEmailForm(forms.Form):
"""Form for adding or editing a security email to a domain."""

View file

@ -0,0 +1,44 @@
# Generated by Django 4.2.1 on 2023-05-31 23:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0022_draftdomain_domainapplication_approved_domain_and_more"),
]
operations = [
migrations.AlterField(
model_name="contact",
name="first_name",
field=models.TextField(
blank=True,
db_index=True,
help_text="First name",
null=True,
verbose_name="first name / given name",
),
),
migrations.AlterField(
model_name="contact",
name="last_name",
field=models.TextField(
blank=True,
db_index=True,
help_text="Last name",
null=True,
verbose_name="last name / family name",
),
),
migrations.AlterField(
model_name="contact",
name="title",
field=models.TextField(
blank=True,
help_text="Title",
null=True,
verbose_name="title or role in your organization",
),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 4.2.1 on 2023-06-01 19:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0023_alter_contact_first_name_alter_contact_last_name_and_more"),
]
operations = [
migrations.AlterField(
model_name="contact",
name="email",
field=models.EmailField(
blank=True, db_index=True, help_text="Email", max_length=254, null=True
),
),
]

View file

@ -0,0 +1,47 @@
# Generated by Django 4.2.1 on 2023-06-01 21:47
from django.db import migrations
import django_fsm # type: ignore
import registrar.models.utility.domain_field
class Migration(migrations.Migration):
dependencies = [
("registrar", "0024_alter_contact_email"),
]
operations = [
migrations.RemoveConstraint(
model_name="domain",
name="unique_domain_name_in_registry",
),
migrations.RemoveField(
model_name="domain",
name="is_active",
),
migrations.AddField(
model_name="domain",
name="state",
field=django_fsm.FSMField(
choices=[
("created", "Created"),
("deleted", "Deleted"),
("unknown", "Unknown"),
],
default="unknown",
help_text="Very basic info about the lifecycle of this domain object",
max_length=21,
protected=True,
),
),
migrations.AlterField(
model_name="domain",
name="name",
field=registrar.models.utility.domain_field.DomainField(
default=None,
help_text="Fully qualified domain name",
max_length=253,
unique=True,
),
),
]

View file

@ -20,6 +20,7 @@ class Contact(TimeStampedModel):
null=True,
blank=True,
help_text="First name",
verbose_name="first name / given name",
db_index=True,
)
middle_name = models.TextField(
@ -31,14 +32,16 @@ class Contact(TimeStampedModel):
null=True,
blank=True,
help_text="Last name",
verbose_name="last name / family name",
db_index=True,
)
title = models.TextField(
null=True,
blank=True,
help_text="Title",
verbose_name="title or role in your organization",
)
email = models.TextField(
email = models.EmailField(
null=True,
blank=True,
help_text="Email",

View file

@ -1,42 +1,45 @@
import logging
import re
from typing import List
from datetime import date
from django_fsm import FSMField # type: ignore
from django.apps import apps
from django.core.exceptions import ValidationError
from django.db import models
from django_fsm import FSMField, transition # type: ignore
from api.views import in_domains
from registrar.utility import errors
from epplibwrapper import (
CLIENT as registry,
commands,
)
from .utility.domain_field import DomainField
from .utility.domain_helper import DomainHelper
from .utility.time_stamped_model import TimeStampedModel
from .public_contact import PublicContact
logger = logging.getLogger(__name__)
class Domain(TimeStampedModel):
class Domain(TimeStampedModel, DomainHelper):
"""
Manage the lifecycle of domain names.
The registry is the source of truth for this data and this model exists:
1. To tie ownership information in the registrar to
DNS entries in the registry; and
2. To allow a new registrant to draft DNS entries before their
application is approved
"""
DNS entries in the registry
class Meta:
constraints = [
# draft domains may share the same name, but
# once approved, they must be globally unique
models.UniqueConstraint(
fields=["name"],
condition=models.Q(is_active=True),
name="unique_domain_name_in_registry",
),
]
~~~ HOW TO USE THIS CLASS ~~~
A) You can create a Domain object with just a name. `Domain(name="something.gov")`.
B) Saving the Domain object will not contact the registry, as it may be useful
to have Domain objects in an `UNKNOWN` pre-created state.
C) Domain properties are lazily loaded. Accessing `my_domain.expiration_date` will
contact the registry, if a cached copy does not exist.
D) Domain creation is lazy. If `my_domain.expiration_date` finds that `my_domain`
does not exist in the registry, it will ask the registry to create it.
F) Created is _not_ the same as active aka live on the internet.
G) Activation is controlled by the registry. It will happen automatically when the
domain meets the required checks.
"""
class Status(models.TextChoices):
"""
@ -91,221 +94,202 @@ class Domain(TimeStampedModel):
PENDING_TRANSFER = "pendingTransfer"
PENDING_UPDATE = "pendingUpdate"
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}$")
class State(models.TextChoices):
"""These capture (some of) the states a domain object can be in."""
@classmethod
def string_could_be_domain(cls, domain: str | None) -> bool:
"""Return True if the string could be a domain name, otherwise False."""
if not isinstance(domain, str):
return False
return bool(cls.DOMAIN_REGEX.match(domain))
# the normal state of a domain object -- may or may not be active!
CREATED = "created"
@classmethod
def validate(cls, domain: str | None, blank_ok=False) -> str:
"""Attempt to determine if a domain name could be requested."""
if domain is None:
raise errors.BlankValueError()
if not isinstance(domain, str):
raise ValueError("Domain name must be a string")
domain = domain.lower().strip()
if domain == "":
if blank_ok:
return domain
else:
raise errors.BlankValueError()
if domain.endswith(".gov"):
domain = domain[:-4]
if "." in domain:
raise errors.ExtraDotsError()
if not Domain.string_could_be_domain(domain + ".gov"):
raise ValueError()
if in_domains(domain):
raise errors.DomainUnavailableError()
return domain
# previously existed but has been deleted from the registry
DELETED = "deleted"
# the state is indeterminate
UNKNOWN = "unknown"
@classmethod
def available(cls, domain: str) -> bool:
"""Check if a domain is available.
"""Check if a domain is available."""
if not cls.string_could_be_domain(domain):
raise ValueError("Not a valid domain: %s" % str(domain))
req = commands.CheckDomain([domain])
return registry.send(req).res_data[0].avail
Not implemented. Returns a dummy value for testing."""
return False # domain_check(domain)
@classmethod
def registered(cls, domain: str) -> bool:
"""Check if a domain is _not_ available."""
return not cls.available(domain)
@property
def contacts(self) -> dict[str, str]:
"""
Get a dictionary of registry IDs for the contacts for this domain.
IDs are provided as strings, e.g.
{ PublicContact.ContactTypeChoices.REGISTRANT: "jd1234",
PublicContact.ContactTypeChoices.ADMINISTRATIVE: "sh8013",...}
"""
raise NotImplementedError()
@property
def creation_date(self) -> date:
"""Get the `cr_date` element from the registry."""
raise NotImplementedError()
@property
def last_transferred_date(self) -> date:
"""Get the `tr_date` element from the registry."""
raise NotImplementedError()
@property
def last_updated_date(self) -> date:
"""Get the `up_date` element from the registry."""
raise NotImplementedError()
@property
def expiration_date(self) -> date:
"""Get or set the `ex_date` element from the registry."""
raise NotImplementedError()
@expiration_date.setter # type: ignore
def expiration_date(self, ex_date: date):
raise NotImplementedError()
@property
def password(self) -> str:
"""
Get the `auth_info.pw` element from the registry. Not a real password.
This `auth_info` element is required by the EPP protocol, but the registry is
using a different mechanism to ensure unauthorized clients cannot perform
actions on domains they do not own. This field provides no security features.
It is not a secret.
"""
raise NotImplementedError()
@property
def nameservers(self) -> list[tuple[str]]:
"""
Get or set a complete list of nameservers for this domain.
Hosts are provided as a list of tuples, e.g.
[("ns1.example.com",), ("ns1.example.gov", "0.0.0.0")]
Subordinate hosts (something.your-domain.gov) MUST have IP addresses,
while non-subordinate hosts MUST NOT.
"""
# TODO: call EPP to get this info instead of returning fake data.
return [
("ns1.example.com",),
("ns2.example.com",),
("ns3.example.com",),
]
@nameservers.setter # type: ignore
def nameservers(self, hosts: list[tuple[str]]):
# TODO: call EPP to set this info.
pass
@property
def statuses(self) -> list[str]:
"""
Get or set the domain `status` elements from the registry.
A domain's status indicates various properties. See Domain.Status.
"""
# implementation note: the Status object from EPP stores the string in
# a dataclass property `state`, not to be confused with the `state` field here
raise NotImplementedError()
@statuses.setter # type: ignore
def statuses(self, statuses: list[str]):
# TODO: there are a long list of rules in the RFC about which statuses
# can be combined; check that here and raise errors for invalid combinations -
# some statuses cannot be set by the client at all
raise NotImplementedError()
@property
def registrant_contact(self) -> PublicContact:
"""Get or set the registrant for this domain."""
raise NotImplementedError()
@registrant_contact.setter # type: ignore
def registrant_contact(self, contact: PublicContact):
raise NotImplementedError()
@property
def administrative_contact(self) -> PublicContact:
"""Get or set the admin contact for this domain."""
raise NotImplementedError()
@administrative_contact.setter # type: ignore
def administrative_contact(self, contact: PublicContact):
raise NotImplementedError()
@property
def security_contact(self) -> PublicContact:
"""Get or set the security contact for this domain."""
# TODO: replace this with a real implementation
contact = PublicContact.get_default_security()
contact.domain = self
contact.email = "mayor@igorville.gov"
return contact
@security_contact.setter # type: ignore
def security_contact(self, contact: PublicContact):
# TODO: replace this with a real implementation
pass
@property
def technical_contact(self) -> PublicContact:
"""Get or set the tech contact for this domain."""
raise NotImplementedError()
@technical_contact.setter # type: ignore
def technical_contact(self, contact: PublicContact):
raise NotImplementedError()
def is_active(self) -> bool:
"""Is the domain live on the inter webs?"""
# TODO: implement a check -- should be performant so it can be called for
# any number of domains on a status page
# this is NOT as simple as checking if Domain.Status.OK is in self.statuses
return False
def transfer(self):
"""Going somewhere. Not implemented."""
pass
raise NotImplementedError()
def renew(self):
"""Time to renew. Not implemented."""
pass
raise NotImplementedError()
def _get_property(self, property):
"""Get some info about a domain."""
if not self.is_active:
return None
if not hasattr(self, "info"):
try:
# get info from registry
self.info = {} # domain_info(self.name)
except Exception as e:
logger.error(e)
# TODO: back off error handling
return None
if hasattr(self, "info"):
if property in self.info:
return self.info[property]
else:
raise KeyError(
"Requested key %s was not found in registry data." % str(property)
)
else:
# TODO: return an error if registry cannot be contacted
return None
def place_client_hold(self):
"""This domain should not be active."""
raise NotImplementedError()
@transition(field="is_active", source="*", target=True)
def activate(self):
"""This domain should be made live."""
DomainApplication = apps.get_model("registrar.DomainApplication")
if hasattr(self, "domain_application"):
if self.domain_application.status != DomainApplication.APPROVED:
raise ValueError("Cannot activate. Application must be approved.")
if Domain.objects.filter(name=self.name, is_active=True).exists():
raise ValueError("Cannot activate. Domain name is already in use.")
# TODO: depending on the details of our registry integration
# we will either contact the registry and deploy the domain
# in this function OR we will verify that it has already been
# activated and reject this state transition if it has not
pass
@transition(field="is_active", source="*", target=False)
def deactivate(self):
"""This domain should not be live."""
# there are security concerns to having this function exist
# within the codebase; discuss these with the project lead
# if there is a feature request to implement this
raise Exception("Cannot revoke, contact registry.")
@property
def sld(self):
"""Get or set the second level domain string."""
return self.name.split(".")[0]
@sld.setter
def sld(self, value: str):
parts = self.name.split(".")
tld = parts[1] if len(parts) > 1 else ""
if Domain.string_could_be_domain(f"{value}.{tld}"):
self.name = f"{value}.{tld}"
else:
raise ValidationError("%s is not a valid second level domain" % value)
@property
def tld(self):
"""Get or set the top level domain string."""
parts = self.name.split(".")
return parts[1] if len(parts) > 1 else ""
@tld.setter
def tld(self, value: str):
sld = self.name.split(".")[0]
if Domain.string_could_be_domain(f"{sld}.{value}"):
self.name = f"{sld}.{value}"
else:
raise ValidationError("%s is not a valid top level domain" % value)
def remove_client_hold(self):
"""This domain is okay to be active."""
raise NotImplementedError()
def __str__(self) -> str:
return self.name
def nameservers(self) -> List[str]:
"""A list of the nameservers for this domain.
TODO: call EPP to get this info instead of returning fake data.
"""
return [
# reserved example domain
"ns1.example.com",
"ns2.example.com",
"ns3.example.com",
]
def set_nameservers(self, new_nameservers: List[str]):
"""Set the nameservers for this domain."""
# TODO: call EPP to set these values in the registry instead of doing
# nothing.
logger.warn("TODO: Fake setting nameservers to %s", new_nameservers)
def security_email(self) -> str:
"""Get the security email for this domain.
TODO: call EPP to get this info instead of returning fake data.
"""
return "mayor@igorville.gov"
def set_security_email(self, new_security_email: str):
"""Set the security email for this domain."""
# TODO: call EPP to set these values in the registry instead of doing
# nothing.
logger.warn("TODO: Fake setting security email to %s", new_security_email)
@property
def roid(self):
return self._get_property("roid")
@property
def status(self):
return self._get_property("status")
@property
def registrant(self):
return self._get_property("registrant")
@property
def sponsor(self):
return self._get_property("sponsor")
@property
def creator(self):
return self._get_property("creator")
@property
def creation_date(self):
return self._get_property("creation_date")
@property
def updator(self):
return self._get_property("updator")
@property
def last_update_date(self):
return self._get_property("last_update_date")
@property
def expiration_date(self):
return self._get_property("expiration_date")
@property
def last_transfer_date(self):
return self._get_property("last_transfer_date")
name = models.CharField(
name = DomainField(
max_length=253,
blank=False,
default=None, # prevent saving without a value
unique=True,
help_text="Fully qualified domain name",
)
# we use `is_active` rather than `domain_application.status`
# because domains may exist without associated applications
is_active = FSMField(
choices=[
(True, "Yes"),
(False, "No"),
],
default=False,
# TODO: how to edit models in Django admin if protected = True
protected=False,
help_text="Domain is live in the registry",
state = FSMField(
max_length=21,
choices=State.choices,
default=State.UNKNOWN,
protected=True, # cannot change state directly, particularly in Django admin
help_text="Very basic info about the lifecycle of this domain object",
)
# ForeignKey on UserDomainRole creates a "permissions" member for

View file

@ -32,7 +32,7 @@ class PublicContact(TimeStampedModel):
if hasattr(self, "domain"):
match self.contact_type:
case PublicContact.ContactTypeChoices.REGISTRANT:
self.domain.registrant = self
self.domain.registrant_contact = self
case PublicContact.ContactTypeChoices.ADMINISTRATIVE:
self.domain.administrative_contact = self
case PublicContact.ContactTypeChoices.TECHNICAL:

View file

@ -0,0 +1,13 @@
from django.db import models
class DomainField(models.CharField):
"""Subclass of CharField to enforce domain name specific requirements."""
def to_python(self, value):
"""Convert to lowercase during deserialization and during form `clean`."""
if value is None:
return value
if isinstance(value, str):
return value.lower()
return str(value).lower()

View file

@ -1,5 +1,5 @@
{% extends 'application_form.html' %}
{% load field_helpers %}
{% load field_helpers url_helpers %}
{% block form_instructions %}
<p>.Gov domain names are for use on the internet. Dont register a .gov to simply reserve a
@ -8,7 +8,7 @@ domain name or for mainly internal use.</p>
<p>Describe the reason for your domain request. Explain how you plan to use this domain.
Who is your intended audience? Will you use it for a website and/or email? Are you moving
your website from another top-level domain (like .com or .org)?
Read about <a href="{% url 'todo' %}">activities that are prohibited on .gov domains.</a></p>
Read about <a href="{% public_site_url 'domains/requirements/' %}">activities that are prohibited on .gov domains.</a></p>
{% endblock %}

View file

@ -0,0 +1,43 @@
{% extends "domain_base.html" %}
{% load static field_helpers%}
{% block title %}Domain authorizing official | {{ domain.name }} | {% endblock %}
{% block domain_content %}
{# this is right after the messages block in the parent template #}
{% include "includes/form_errors.html" with form=form %}
<h1>Authorizing official</h1>
<p>Your authorizing official is the person within your organization who can
authorize domain requests. This is generally the highest-ranking or
highest-elected official in your organization. <a class="usa-link"
href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization">Read more about who can serve
as an authorizing official.</a></p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% csrf_token %}
{% input_with_errors form.first_name %}
{% input_with_errors form.middle_name %}
{% input_with_errors form.last_name %}
{% input_with_errors form.title %}
{% input_with_errors form.email %}
{% input_with_errors form.phone %}
<button
type="submit"
class="usa-button"
>Save</button>
</form>
{% endblock %} {# domain_content #}

View file

@ -1,6 +1,33 @@
{% extends "domain_base.html" %}
{% load static url_helpers %}
{% block domain_content %}
{{ block.super }}
<p>Active: {% if domain.is_active %}Yes{% else %}No{% endif %}</p>
<div class="margin-top-4 tablet:grid-col-10">
{% url 'domain-nameservers' pk=domain.id as url %}
{% if domain.nameservers %}
{% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %}
{% else %}
<h2 class="margin-top-neg-1"> DNS name servers </h2>
<p> No DNS name servers have been added yet. Before your domain can be used well need information about your domain name servers.</p>
<a class="usa-button margin-bottom-1" href="{{url}}"> Add DNS name servers </a>
{% endif %}
{% url 'todo' as url %}
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url %}
{% url 'domain-authorizing-official' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Authorizing official' value=domain.domain_info.authorizing_official contact='true' edit_link=url %}
{% url 'domain-your-contact-information' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url %}
{% url 'domain-security-email' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Security email' value=domain.security_email edit_link=url %}
{% url 'domain-users' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='User management' users='true' list=True value=domain.permissions.all edit_link=url %}
</div>
{% endblock %} {# domain_content #}

View file

@ -8,7 +8,7 @@
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
Domain Overview
Domain overview
</a>
</li>
@ -31,7 +31,7 @@
</li>
<li class="usa-sidenav__item">
{% url 'todo' as url %}
{% url 'domain-authorizing-official' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
@ -40,7 +40,7 @@
</li>
<li class="usa-sidenav__item">
{% url 'todo' as url %}
{% url 'domain-your-contact-information' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>

View file

@ -0,0 +1,35 @@
{% extends "domain_base.html" %}
{% load static field_helpers %}
{% block title %}Domain contact information | {{ domain.name }} | {% endblock %}
{% block domain_content %}
<h1>Domain contact information</h1>
<p>If youd like us to use a different name, email, or phone number you can make those changes below. Changing your contact information here wont affect your Login.gov account information.</p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% csrf_token %}
{% input_with_errors form.first_name %}
{% input_with_errors form.middle_name %}
{% input_with_errors form.last_name %}
{% input_with_errors form.title %}
{% input_with_errors form.email %}
{% input_with_errors form.phone %}
<button
type="submit"
class="usa-button"
>Save</button>
</form>
{% endblock %} {# domain_content #}

View file

@ -1,11 +1,13 @@
{% load static url_helpers %}
<section class="summary-item margin-top-3">
<hr class="" />
<p class="summary-item__title
<hr class="" aria-hidden="true" />
<h2 class="summary-item__title
text-primary-dark text-semibold
margin-top-0 margin-bottom-05"
>
{{ title }}
</p>
>
{{ title }}
</h2>
{% if address %}
{% include "includes/organization_address.html" with organization=value %}
{% elif contact %}
@ -30,11 +32,19 @@
{% endif %}
{% elif list %}
{% if value|length == 1 %}
<p class="margin-top-0">{{ value | first }} </p>
{% if users %}
<p class="margin-top-0">{{ value.0.user.email }} </p>
{% else %}
<p class="margin-top-0">{{ value | first }} </p>
{% endif %}
{% else %}
<ul class="usa-list margin-top-0">
{% for item in value %}
<li>{{ item }}</li>
{% if users %}
<li>{{ item.user.email }}</li>
{% else %}
<li>{{ item }}</li>
{% endif %}
{% empty %}
<li>None</li>
{% endfor %}</ul></p>
@ -45,5 +55,13 @@
{{ value }}
</p>
{% endif %}
{% if edit_link %}
<a
href="{{ edit_link }}"
>
Edit<span class="sr-only"> {{ title }}</span>
</a>
{% endif %}
</section>

View file

@ -1,6 +1,8 @@
from django import template
from django.urls import reverse
from django.conf import settings
register = template.Library()
@ -15,3 +17,16 @@ def startswith(text, starts):
if isinstance(text, str):
return text.startswith(starts)
return False
@register.simple_tag
def public_site_url(url_path):
"""Make a full URL for this path at our public site.
The public site base url is set by a GETGOV_PUBLIC_SITE_URL environment
variable.
"""
base_url = settings.GETGOV_PUBLIC_SITE_URL
# join the two halves with a single slash
public_url = "/".join([base_url.rstrip("/"), url_path.lstrip("/")])
return public_url

View file

@ -15,6 +15,7 @@ from registrar.forms.application_wizard import (
AnythingElseForm,
TypeOfWorkForm,
)
from registrar.forms.domain import ContactForm
class TestFormValidation(TestCase):
@ -277,3 +278,13 @@ class TestFormValidation(TestCase):
for error in form.non_field_errors()
)
)
class TestContactForm(TestCase):
def test_contact_form_email_invalid(self):
form = ContactForm(data={"email": "example.net"})
self.assertEqual(form.errors["email"], ["Enter a valid email address."])
def test_contact_form_email_invalid2(self):
form = ContactForm(data={"email": "@"})
self.assertEqual(form.errors["email"], ["Enter a valid email address."])

View file

@ -0,0 +1,31 @@
"""Test template tags."""
from django.conf import settings
from django.test import TestCase
from django.template import Context, Template
class TestTemplateTags(TestCase):
def _render_template(self, string, context=None):
"""Helper method to render a template given as a string.
Originally from https://stackoverflow.com/a/1690879
"""
context = context or {}
context = Context(context)
return Template(string).render(context)
def test_public_site_url(self):
result = self._render_template(
"{% load url_helpers %}{% public_site_url 'directory/page' %}"
)
self.assertTrue(result.startswith(settings.GETGOV_PUBLIC_SITE_URL))
self.assertTrue(result.endswith("/directory/page"))
def test_public_site_url_leading_slash(self):
result = self._render_template(
"{% load url_helpers %}{% public_site_url '/directory/page' %}"
)
self.assertTrue(result.startswith(settings.GETGOV_PUBLIC_SITE_URL))
# slash-slash host slash directory slash page
self.assertEqual(result.count("/"), 4)

View file

@ -13,6 +13,7 @@ import boto3_mocking # type: ignore
from registrar.models import (
DomainApplication,
Domain,
DomainInformation,
DraftDomain,
DomainInvitation,
Contact,
@ -1030,12 +1031,16 @@ class TestWithDomainPermissions(TestWithUser):
def setUp(self):
super().setUp()
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
self.domain_information, _ = DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain
)
self.role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN
)
def tearDown(self):
try:
self.domain_information.delete()
if hasattr(self.domain, "contacts"):
self.domain.contacts.all().delete()
self.domain.delete()
@ -1048,61 +1053,41 @@ class TestWithDomainPermissions(TestWithUser):
class TestDomainPermissions(TestWithDomainPermissions):
def test_not_logged_in(self):
"""Not logged in gets a redirect to Login."""
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("domain-users", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("domain-users-add", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("domain-security-email", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
for view_name in [
"domain",
"domain-users",
"domain-users-add",
"domain-nameservers",
"domain-authorizing-official",
"domain-your-contact-information",
"domain-security-email",
]:
with self.subTest(view_name=view_name):
response = self.client.get(
reverse(view_name, kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
def test_no_domain_role(self):
"""Logged in but no role gets 403 Forbidden."""
self.client.force_login(self.user)
self.role.delete() # user no longer has a role on this domain
with less_console_noise():
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
reverse("domain-users", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
reverse("domain-users-add", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
reverse("domain-security-email", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
for view_name in [
"domain",
"domain-users",
"domain-users-add",
"domain-nameservers",
"domain-authorizing-official",
"domain-your-contact-information",
"domain-security-email",
]:
with self.subTest(view_name=view_name):
with less_console_noise():
response = self.client.get(
reverse(view_name, kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
class TestDomainDetail(TestWithDomainPermissions, WebTest):
@ -1312,6 +1297,40 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
# the field.
self.assertContains(result, "This field is required", count=2, status_code=200)
def test_domain_authorizing_official(self):
"""Can load domain's authorizing official page."""
page = self.client.get(
reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})
)
# once on the sidebar, once in the title
self.assertContains(page, "Authorizing official", count=2)
def test_domain_authorizing_official_content(self):
"""Authorizing official information appears on the page."""
self.domain_information.authorizing_official = Contact(first_name="Testy")
self.domain_information.authorizing_official.save()
self.domain_information.save()
page = self.app.get(
reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Testy")
def test_domain_your_contact_information(self):
"""Can load domain's your contact information page."""
page = self.client.get(
reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Domain contact information")
def test_domain_your_contact_information_content(self):
"""Logged-in user's contact information appears on the page."""
self.user.contact.first_name = "Testy"
self.user.contact.save()
page = self.app.get(
reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Testy")
def test_domain_security_email(self):
"""Can load domain's security email page."""
page = self.client.get(

View file

@ -1,7 +1,9 @@
from .application import *
from .domain import (
DomainView,
DomainAuthorizingOfficialView,
DomainNameserversView,
DomainYourContactInformationView,
DomainSecurityEmailView,
DomainUsersView,
DomainAddUserView,

View file

@ -14,9 +14,19 @@ from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic.edit import FormMixin
from registrar.models import DomainInvitation, User, UserDomainRole
from registrar.models import (
Domain,
DomainInvitation,
User,
UserDomainRole,
)
from ..forms import DomainAddUserForm, NameserverFormset, DomainSecurityEmailForm
from ..forms import (
DomainAddUserForm,
NameserverFormset,
DomainSecurityEmailForm,
ContactForm,
)
from ..utility.email import send_templated_email, EmailSendingError
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
@ -31,6 +41,48 @@ class DomainView(DomainPermissionView):
template_name = "domain_detail.html"
class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
"""Domain authorizing official editing view."""
model = Domain
template_name = "domain_authorizing_official.html"
context_object_name = "domain"
form_class = ContactForm
def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.authorizing_official instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs)
form_kwargs["instance"] = self.get_object().domain_info.authorizing_official
return form_kwargs
def get_success_url(self):
"""Redirect to the overview page for the domain."""
return reverse("domain-authorizing-official", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
"""Form submission posts to this view.
This post method harmonizes using DetailView and FormMixin together.
"""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
"""The form is valid, save the authorizing official."""
form.save()
messages.success(
self.request, "The authorizing official for this domain has been updated."
)
# superclass has the redirect
return super().form_valid(form)
class DomainNameserversView(DomainPermissionView, FormMixin):
"""Domain nameserver editing view."""
@ -41,10 +93,10 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
def get_initial(self):
"""The initial value for the form (which is a formset here)."""
domain = self.get_object()
return [{"server": server} for server in domain.nameservers()]
return [{"server": name} for name, *ip in domain.nameservers]
def get_success_url(self):
"""Redirect to the overview page for the domain."""
"""Redirect to the nameservers page for the domain."""
return reverse("domain-nameservers", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
@ -82,12 +134,13 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
nameservers = []
for form in formset:
try:
nameservers.append(form.cleaned_data["server"])
as_tuple = (form.cleaned_data["server"],)
nameservers.append(as_tuple)
except KeyError:
# no server information in this field, skip it
pass
domain = self.get_object()
domain.set_nameservers(nameservers)
domain.nameservers = nameservers
messages.success(
self.request, "The name servers for this domain have been updated."
@ -96,6 +149,46 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
return super().form_valid(formset)
class DomainYourContactInformationView(DomainPermissionView, FormMixin):
"""Domain your contact information editing view."""
template_name = "domain_your_contact_information.html"
form_class = ContactForm
def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.submitter instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs)
form_kwargs["instance"] = self.request.user.contact
return form_kwargs
def get_success_url(self):
"""Redirect to the your contact information for the domain."""
return reverse("domain-your-contact-information", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
"""Form submission posts to this view."""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
# there is a valid email address in the form
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
"""The form is valid, call setter in model."""
# Post to DB using values from the form
form.save()
messages.success(
self.request, "Your contact information for this domain has been updated."
)
# superclass has the redirect
return super().form_valid(form)
class DomainSecurityEmailView(DomainPermissionView, FormMixin):
"""Domain security email editing view."""
@ -107,15 +200,15 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
"""The initial value for the form."""
domain = self.get_object()
initial = super().get_initial()
initial["security_email"] = domain.security_email()
initial["security_email"] = domain.security_contact.email
return initial
def get_success_url(self):
"""Redirect to the overview page for the domain."""
"""Redirect to the security email page for the domain."""
return reverse("domain-security-email", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
"""Formset submission posts to this view."""
"""Form submission posts to this view."""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
@ -130,7 +223,9 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
# Set the security email from the form
new_email = form.cleaned_data.get("security_email", "")
domain = self.get_object()
domain.set_security_email(new_email)
contact = domain.security_contact
contact.email = new_email
contact.save()
messages.success(
self.request, "The security email for this domain have been updated."

View file

@ -1,49 +1,52 @@
-i https://pypi.python.org/simple
asgiref==3.6.0 ; python_version >= '3.7'
boto3==1.26.69
botocore==1.29.69 ; python_version >= '3.7'
cachetools==5.3.0
certifi==2022.12.7 ; python_version >= '3.6'
asgiref==3.7.2 ; python_version >= '3.7'
boto3==1.26.145
botocore==1.29.145 ; python_version >= '3.7'
cachetools==5.3.1
certifi==2023.5.7 ; python_version >= '3.6'
cfenv==0.5.3
cffi==1.15.1
charset-normalizer==3.0.1 ; python_version >= '3.6'
cryptography==39.0.1 ; python_version >= '3.6'
charset-normalizer==3.1.0 ; python_full_version >= '3.7.0'
cryptography==41.0.1 ; python_version >= '3.7'
defusedxml==0.7.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
dj-database-url==1.2.0
dj-database-url==2.0.0
dj-email-url==1.0.6
django==4.1.6
django==4.2.1
django-allow-cidr==0.6.0
django-auditlog==2.2.2
django-auditlog==2.3.0
django-cache-url==3.4.4
django-csp==3.7
django-fsm==2.8.1
django-phonenumber-field[phonenumberslite]==7.0.2
django-phonenumber-field[phonenumberslite]==7.1.0
django-widget-tweaks==1.4.12
environs[django]==9.5.0
faker==17.0.0
faker==18.10.0
git+https://github.com/cisagov/epplib.git@f818cbf0b069a12f03e1d72e4b9f4900924b832d#egg=fred-epplib
furl==2.1.3
future==0.18.3 ; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
gunicorn==20.1.0
idna==3.4 ; python_version >= '3.5'
jmespath==1.0.1 ; python_version >= '3.7'
lxml==4.9.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
mako==1.2.4 ; python_version >= '3.7'
markupsafe==2.1.2 ; python_version >= '3.7'
marshmallow==3.19.0 ; python_version >= '3.7'
oic==1.5.0
oic==1.6.0
orderedmultidict==1.0.1
packaging==23.0 ; python_version >= '3.7'
phonenumberslite==8.13.6
psycopg2-binary==2.9.5
packaging==23.1 ; python_version >= '3.7'
phonenumberslite==8.13.13
psycopg2-binary==2.9.6
pycparser==2.21
pycryptodomex==3.17
pycryptodomex==3.18.0
pydantic==1.10.8 ; python_version >= '3.7'
pyjwkest==1.4.2
python-dateutil==2.8.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-dotenv==0.21.1 ; python_version >= '3.7'
requests==2.28.2
s3transfer==0.6.0 ; python_version >= '3.7'
setuptools==67.2.0 ; python_version >= '3.7'
python-dotenv==1.0.0 ; python_version >= '3.8'
requests==2.31.0
s3transfer==0.6.1 ; python_version >= '3.7'
setuptools==67.8.0 ; python_version >= '3.7'
six==1.16.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlparse==0.4.3 ; python_version >= '3.5'
typing-extensions==4.4.0 ; python_version >= '3.7'
urllib3==1.26.14 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
whitenoise==6.3.0
sqlparse==0.4.4 ; python_version >= '3.5'
typing-extensions==4.6.2
urllib3==1.26.16 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
whitenoise==6.4.0

View file

@ -52,6 +52,7 @@
10038 OUTOFSCOPE http://app:8080/users
10038 OUTOFSCOPE http://app:8080/users/add
10038 OUTOFSCOPE http://app:8080/nameservers
10038 OUTOFSCOPE http://app:8080/your-contact-information
10038 OUTOFSCOPE http://app:8080/security-email
10038 OUTOFSCOPE http://app:8080/delete
10038 OUTOFSCOPE http://app:8080/withdraw