Add domain application data model

This commit is contained in:
Neil Martinsen-Burrell 2022-11-02 09:42:15 -05:00
parent d3b9a993cd
commit 6263b69c34
No known key found for this signature in database
GPG key ID: 6A3C818CC10D0184
8 changed files with 799 additions and 80 deletions

View file

@ -19,6 +19,7 @@ django-formtools = "*"
django-widget-tweaks = "*" django-widget-tweaks = "*"
cachetools = "*" cachetools = "*"
requests = "*" requests = "*"
django-fsm = "*"
[dev-packages] [dev-packages]
django-debug-toolbar = "*" django-debug-toolbar = "*"
@ -31,3 +32,4 @@ types-requests = "*"
django-stubs = "*" django-stubs = "*"
django-webtest = "*" django-webtest = "*"
types-cachetools = "*" types-cachetools = "*"
graphviz = ">=0.4"

174
src/Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "1b7689dae771eaeee047ef75ed1da344ebc9d40fbb9ade689e9dba885e20ec59" "sha256": "80ebab4c3aa382d11cc102ff541e23fd3a43e7943eb66ad58b2805304dbaf4fe"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@ -131,35 +131,35 @@
}, },
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a", "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d",
"sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f", "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd",
"sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0", "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146",
"sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407", "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7",
"sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7", "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436",
"sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6", "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0",
"sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153", "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828",
"sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750", "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b",
"sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad", "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55",
"sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6", "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36",
"sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b", "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50",
"sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5", "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2",
"sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a", "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a",
"sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d", "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8",
"sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d", "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0",
"sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294", "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548",
"sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0", "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320",
"sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a", "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748",
"sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac", "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249",
"sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61", "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959",
"sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013", "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f",
"sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e", "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0",
"sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb", "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd",
"sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9", "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220",
"sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c",
"sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==38.0.1" "version": "==38.0.3"
}, },
"defusedxml": { "defusedxml": {
"hashes": [ "hashes": [
@ -185,11 +185,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793", "sha256:678bbfc8604eb246ed54e2063f0765f13b321a50526bdc8cb1f943eda7fa31f1",
"sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f" "sha256:6b1de6886cae14c7c44d188f580f8ba8da05750f544c80ae5ad43375ab293cd5"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.1.2" "version": "==4.1.3"
}, },
"django-allow-cidr": { "django-allow-cidr": {
"hashes": [ "hashes": [
@ -222,6 +222,14 @@
"index": "pypi", "index": "pypi",
"version": "==2.4" "version": "==2.4"
}, },
"django-fsm": {
"hashes": [
"sha256:e2c02cbf273fb9691aa9a907c29990afdd21a4adea09c5640344c93fbe03f8d9",
"sha256:fd9f8de9f33188e50f876ce53908fbd7289e5031a44ffdb97d43909e56699ef8"
],
"index": "pypi",
"version": "==2.8.1"
},
"django-widget-tweaks": { "django-widget-tweaks": {
"hashes": [ "hashes": [
"sha256:9bfc5c705684754a83cc81da328b39ad1b80f32bd0f4340e2a810cbab4b0c00e", "sha256:9bfc5c705684754a83cc81da328b39ad1b80f32bd0f4340e2a810cbab4b0c00e",
@ -507,11 +515,11 @@
}, },
"setuptools": { "setuptools": {
"hashes": [ "hashes": [
"sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17", "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31",
"sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356" "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==65.5.0" "version": "==65.5.1"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@ -624,11 +632,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793", "sha256:678bbfc8604eb246ed54e2063f0765f13b321a50526bdc8cb1f943eda7fa31f1",
"sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f" "sha256:6b1de6886cae14c7c44d188f580f8ba8da05750f544c80ae5ad43375ab293cd5"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.1.2" "version": "==4.1.3"
}, },
"django-debug-toolbar": { "django-debug-toolbar": {
"hashes": [ "hashes": [
@ -640,19 +648,19 @@
}, },
"django-stubs": { "django-stubs": {
"hashes": [ "hashes": [
"sha256:0dff8ec0ba3abe046450b3d8a29ce9e72629893d2c1ef679189cc2bfdb6d2f64", "sha256:424fdd1935f859a802365056f9ccf4db12d1d93a5ab3de6d5633dddba0c5fc76",
"sha256:ea8b35d0da49f7b2ee99a79125f1943e033431dd114726d6643cc35de619230e" "sha256:eaecc1fc71532c1148f0c9687556651d880165476d7629bf318ff86a903a150c"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.12.0" "version": "==1.13.0"
}, },
"django-stubs-ext": { "django-stubs-ext": {
"hashes": [ "hashes": [
"sha256:9bd7418376ab00b7f88d6d56be9fece85bfa0c7c348ac621155fa4d7a91146f2", "sha256:4fd8cdbc68d1a421f21bb7e0d9e76d50f6a4b504d350ba786405daf536e90c21",
"sha256:c5d8db53d29c756e7e3d0820a5a079a43bc38d8fab0e1b8bd5df2f3366c54b5a" "sha256:d729fbc7fe8970a7e26b35956c35b48502516f011d523c0577bdfb02ed956284"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.7'",
"version": "==0.5.0" "version": "==0.7.0"
}, },
"django-webtest": { "django-webtest": {
"hashes": [ "hashes": [
@ -686,6 +694,14 @@
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==3.1.29" "version": "==3.1.29"
}, },
"graphviz": {
"hashes": [
"sha256:587c58a223b51611c0cf461132da386edd896a029524ca61a1462b880bf97977",
"sha256:8c58f14adaa3b947daf26c19bc1e98c4e0702cdc31cf99153e6f06904d492bf8"
],
"index": "pypi",
"version": "==0.20.1"
},
"mccabe": { "mccabe": {
"hashes": [ "hashes": [
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
@ -696,33 +712,39 @@
}, },
"mypy": { "mypy": {
"hashes": [ "hashes": [
"sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d", "sha256:0680389c34284287fe00e82fc8bccdea9aff318f7e7d55b90d967a13a9606013",
"sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24", "sha256:1767830da2d1afa4e62b684647af0ff79b401f004d7fa08bc5b0ce2d45bcd5ec",
"sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046", "sha256:1ee5f99817ee70254e7eb5cf97c1b11dda29c6893d846c8b07bce449184e9466",
"sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e", "sha256:262c543ef24deb10470a3c1c254bb986714e2b6b1a67d66daf836a548a9f316c",
"sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3", "sha256:269f0dfb6463b8780333310ff4b5134425157ef0d2b1d614015adaf6d6a7eabd",
"sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5", "sha256:2a3150d409609a775c8cb65dbe305c4edd7fe576c22ea79d77d1454acd9aeda8",
"sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20", "sha256:2b6f85c2ad378e3224e017904a051b26660087b3b76490d533b7344f1546d3ff",
"sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda", "sha256:3227f14fe943524f5794679156488f18bf8d34bfecd4623cf76bc55958d229c5",
"sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1", "sha256:3ff201a0c6d3ea029d73b1648943387d75aa052491365b101f6edd5570d018ea",
"sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146", "sha256:46897755f944176fbc504178422a5a2875bbf3f7436727374724842c0987b5af",
"sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206", "sha256:47a9955214615108c3480a500cfda8513a0b1cd3c09a1ed42764ca0dd7b931dd",
"sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746", "sha256:49082382f571c3186ce9ea0bd627cb1345d4da8d44a8377870f4442401f0a706",
"sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6", "sha256:4a8a6c10f4c63fbf6ad6c03eba22c9331b3946a4cec97f008e9ffb4d3b31e8e2",
"sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e", "sha256:6826d9c4d85bbf6d68cb279b561de6a4d8d778ca8e9ab2d00ee768ab501a9852",
"sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc", "sha256:72382cb609142dba3f04140d016c94b4092bc7b4d98ca718740dc989e5271b8d",
"sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a", "sha256:7da0005e47975287a92b43276e460ac1831af3d23032c34e67d003388a0ce8d0",
"sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8", "sha256:8798c8ed83aa809f053abff08664bdca056038f5a02af3660de00b7290b64c47",
"sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763", "sha256:8f1940325a8ed460ba03d19ab83742260fa9534804c317224e5d4e5aa588e2d6",
"sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2", "sha256:8f694d6d09a460b117dccb6857dda269188e3437c880d7b60fa0014fa872d1e9",
"sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947", "sha256:9b8f4a8213b1fd4b751e26b59ae0e0c12896568d7e805861035c7a15ed6dc9eb",
"sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40", "sha256:9d851c09b981a65d9d283a8ccb5b1d0b698e580493416a10942ef1a04b19fd37",
"sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b", "sha256:aaf1be63e0207d7d17be942dcf9a6b641745581fe6c64df9a38deb562a7dbafa",
"sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795", "sha256:aba38e3dd66bdbafbbfe9c6e79637841928ea4c79b32e334099463c17b0d90ef",
"sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c" "sha256:b08541a06eed35b543ae1a6b301590eb61826a1eb099417676ddc5a42aa151c5",
"sha256:be88d665e76b452c26fb2bdc3d54555c01226fba062b004ede780b190a50f9db",
"sha256:c76c769c46a1e6062a84837badcb2a7b0cdb153d68601a61f60739c37d41cc74",
"sha256:cc6019808580565040cd2a561b593d7c3c646badd7e580e07d875eb1bf35c695",
"sha256:cd2dd3730ba894ec2a2082cc703fbf3e95a08479f7be84912e3131fc68809d46",
"sha256:d555aa7f44cecb7ea3c0ac69d58b1a5afb92caa017285a8e9c4efbf0518b61b4",
"sha256:d847dd23540e2912d9667602271e5ebf25e5788e7da46da5ffd98e7872616e8e"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.982" "version": "==0.990"
}, },
"mypy-extensions": { "mypy-extensions": {
"hashes": [ "hashes": [
@ -757,11 +779,11 @@
}, },
"platformdirs": { "platformdirs": {
"hashes": [ "hashes": [
"sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb",
"sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==2.5.2" "version": "==2.5.3"
}, },
"pycodestyle": { "pycodestyle": {
"hashes": [ "hashes": [
@ -870,7 +892,7 @@
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
], ],
"markers": "python_version >= '3.7'", "markers": "python_full_version < '3.11.0a7'",
"version": "==2.0.1" "version": "==2.0.1"
}, },
"types-cachetools": { "types-cachetools": {
@ -883,10 +905,10 @@
}, },
"types-pytz": { "types-pytz": {
"hashes": [ "hashes": [
"sha256:0c163b15d3e598e6cc7074a99ca9ec72b25dc1b446acc133b827667af0b7b09a", "sha256:bea605ce5d5a5d52a8e1afd7656c9b42476e18a0f888de6be91587355313ddf4",
"sha256:a8e1fe6a1b270fbfaf2553b20ad0f1316707cc320e596da903bb17d7373fed2d" "sha256:d078196374d1277e9f9984d49373ea043cf2c64d5d5c491fbc86c258557bd46f"
], ],
"version": "==2022.5.0.0" "version": "==2022.6.0.1"
}, },
"types-pyyaml": { "types-pyyaml": {
"hashes": [ "hashes": [

View file

@ -90,6 +90,7 @@ INSTALLED_APPS = [
"registrar", "registrar",
# Our internal API application # Our internal API application
"api", "api",
"django_fsm",
] ]
# Middleware are routines for processing web requests. # Middleware are routines for processing web requests.

View file

@ -0,0 +1,237 @@
# -*- coding: utf-8; mode: django -*-
import graphviz
from optparse import make_option
from itertools import chain
from django.core.management.base import BaseCommand
try:
from django.utils.encoding import force_text
_requires_system_checks = True
except ImportError: # Django >= 4.0
from django.utils.encoding import force_str as force_text
from django.core.management.base import ALL_CHECKS
_requires_system_checks = ALL_CHECKS
from django_fsm import FSMFieldMixin, GET_STATE, RETURN_VALUE
try:
from django.db.models import get_apps, get_app, get_models, get_model
NEW_META_API = False
except ImportError:
from django.apps import apps
NEW_META_API = True
from django import VERSION
HAS_ARGPARSE = VERSION >= (1, 10)
def all_fsm_fields_data(model):
if NEW_META_API:
return [(field, model) for field in model._meta.get_fields() if isinstance(field, FSMFieldMixin)]
else:
return [(field, model) for field in model._meta.fields if isinstance(field, FSMFieldMixin)]
def node_name(field, state):
opts = field.model._meta
return "%s.%s.%s.%s" % (opts.app_label, opts.verbose_name.replace(" ", "_"), field.name, state)
def node_label(field, state):
if type(state) == int or (type(state) == bool and hasattr(field, "choices")):
return force_text(dict(field.choices).get(state))
else:
return state
def generate_dot(fields_data):
result = graphviz.Digraph()
for field, model in fields_data:
sources, targets, edges, any_targets, any_except_targets = set(), set(), set(), set(), set()
# dump nodes and edges
for transition in field.get_all_transitions(model):
if transition.source == "*":
any_targets.add((transition.target, transition.name))
elif transition.source == "+":
any_except_targets.add((transition.target, transition.name))
else:
_targets = (
(state for state in transition.target.allowed_states)
if isinstance(transition.target, (GET_STATE, RETURN_VALUE))
else (transition.target,)
)
source_name_pair = (
((state, node_name(field, state)) for state in transition.source.allowed_states)
if isinstance(transition.source, (GET_STATE, RETURN_VALUE))
else ((transition.source, node_name(field, transition.source)),)
)
for source, source_name in source_name_pair:
if transition.on_error:
on_error_name = node_name(field, transition.on_error)
targets.add((on_error_name, node_label(field, transition.on_error)))
edges.add((source_name, on_error_name, (("style", "dotted"),)))
for target in _targets:
add_transition(source, target, transition.name, source_name, field, sources, targets, edges)
targets.update(
set((node_name(field, target), node_label(field, target)) for target, _ in chain(any_targets, any_except_targets))
)
for target, name in any_targets:
target_name = node_name(field, target)
all_nodes = sources | targets
for source_name, label in all_nodes:
sources.add((source_name, label))
edges.add((source_name, target_name, (("label", name),)))
for target, name in any_except_targets:
target_name = node_name(field, target)
all_nodes = sources | targets
all_nodes.remove(((target_name, node_label(field, target))))
for source_name, label in all_nodes:
sources.add((source_name, label))
edges.add((source_name, target_name, (("label", name),)))
# construct subgraph
opts = field.model._meta
subgraph = graphviz.Digraph(
name="cluster_%s_%s_%s" % (opts.app_label, opts.object_name, field.name),
graph_attr={"label": "%s.%s.%s" % (opts.app_label, opts.object_name, field.name)},
)
final_states = targets - sources
for name, label in final_states:
subgraph.node(name, label=label, shape="doublecircle")
for name, label in (sources | targets) - final_states:
subgraph.node(name, label=label, shape="circle")
if field.default: # Adding initial state notation
if label == field.default:
initial_name = node_name(field, "_initial")
subgraph.node(name=initial_name, label="", shape="point")
subgraph.edge(initial_name, name)
for source_name, target_name, attrs in edges:
subgraph.edge(source_name, target_name, **dict(attrs))
result.subgraph(subgraph)
return result
def add_transition(transition_source, transition_target, transition_name, source_name, field, sources, targets, edges):
target_name = node_name(field, transition_target)
sources.add((source_name, node_label(field, transition_source)))
targets.add((target_name, node_label(field, transition_target)))
edges.add((source_name, target_name, (("label", transition_name),)))
def get_graphviz_layouts():
try:
import graphviz
return graphviz.backend.ENGINES
except Exception:
return {"sfdp", "circo", "twopi", "dot", "neato", "fdp", "osage", "patchwork"}
class Command(BaseCommand):
requires_system_checks = _requires_system_checks
if not HAS_ARGPARSE:
option_list = BaseCommand.option_list + (
make_option(
"--output",
"-o",
action="store",
dest="outputfile",
help=(
"Render output file. Type of output dependent on file extensions. " "Use png or jpg to render graph to image."
),
),
# NOQA
make_option(
"--layout",
"-l",
action="store",
dest="layout",
default="dot",
help=("Layout to be used by GraphViz for visualization. " "Layouts: %s." % " ".join(get_graphviz_layouts())),
),
)
args = "[appname[.model[.field]]]"
else:
def add_arguments(self, parser):
parser.add_argument(
"--output",
"-o",
action="store",
dest="outputfile",
help=(
"Render output file. Type of output dependent on file extensions. " "Use png or jpg to render graph to image."
),
)
parser.add_argument(
"--layout",
"-l",
action="store",
dest="layout",
default="dot",
help=("Layout to be used by GraphViz for visualization. " "Layouts: %s." % " ".join(get_graphviz_layouts())),
)
parser.add_argument("args", nargs="*", help=("[appname[.model[.field]]]"))
help = "Creates a GraphViz dot file with transitions for selected fields"
def render_output(self, graph, **options):
filename, format = options["outputfile"].rsplit(".", 1)
graph.engine = options["layout"]
graph.format = format
graph.render(filename)
def handle(self, *args, **options):
fields_data = []
if len(args) != 0:
for arg in args:
field_spec = arg.split(".")
if len(field_spec) == 1:
if NEW_META_API:
app = apps.get_app(field_spec[0])
models = apps.get_models(app)
else:
app = get_app(field_spec[0])
models = get_models(app)
for model in models:
fields_data += all_fsm_fields_data(model)
elif len(field_spec) == 2:
if NEW_META_API:
model = apps.get_model(field_spec[0], field_spec[1])
else:
model = get_model(field_spec[0], field_spec[1])
fields_data += all_fsm_fields_data(model)
elif len(field_spec) == 3:
if NEW_META_API:
model = apps.get_model(field_spec[0], field_spec[1])
else:
model = get_model(field_spec[0], field_spec[1])
fields_data += all_fsm_fields_data(model)
else:
if NEW_META_API:
for model in apps.get_models():
fields_data += all_fsm_fields_data(model)
else:
for app in get_apps():
for model in get_models(app):
fields_data += all_fsm_fields_data(model)
dotdata = generate_dot(fields_data)
if options["outputfile"]:
self.render_output(dotdata, **options)
else:
print(dotdata)

View file

@ -0,0 +1,243 @@
# Generated by Django 4.1.3 on 2022-11-07 19:39
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django_fsm
class Migration(migrations.Migration):
dependencies = [
("registrar", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="Contact",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("first_name", models.TextField(help_text="First name", null=True)),
("middle_name", models.TextField(help_text="Middle name", null=True)),
("last_name", models.TextField(help_text="Last name", null=True)),
("title", models.TextField(help_text="Title", null=True)),
("email", models.TextField(help_text="Email", null=True)),
("phone", models.TextField(help_text="Phone", null=True)),
],
),
migrations.CreateModel(
name="Website",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("website", models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name="DomainApplication",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"status",
django_fsm.FSMField(
choices=[
("started", "started"),
("submitted", "submitted"),
("investigating", "investigating"),
("approved", "approved"),
],
default="started",
max_length=50,
protected=True,
),
),
(
"organization_type",
models.CharField(
choices=[
("federal", "a federal agency"),
("interstate", "an organization of two or more states"),
(
"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",
"a tribal government recognized by the federal or state government",
),
("county", "a county, parish, or borough"),
("city", "a city, town, township, village, etc."),
(
"special_district",
"an independent organization within a single state",
),
],
help_text="Type of Organization",
max_length=255,
),
),
(
"federal_branch",
models.CharField(
choices=[
("Executive", "Executive"),
("Judicial", "Judicial"),
("Legislative", "Legislative"),
],
help_text="Branch of federal government",
max_length=50,
null=True,
),
),
(
"is_election_office",
models.BooleanField(
help_text="Is your ogranization an election office?", null=True
),
),
(
"organization_name",
models.TextField(help_text="Organization name", null=True),
),
(
"street_address",
models.TextField(help_text="Street Address", null=True),
),
(
"unit_type",
models.CharField(help_text="Unit type", max_length=15, null=True),
),
(
"unit_number",
models.CharField(
help_text="Unit number", max_length=255, null=True
),
),
(
"state_territory",
models.CharField(
help_text="State/Territory", max_length=2, null=True
),
),
(
"zip_code",
models.CharField(help_text="ZIP code", max_length=10, null=True),
),
(
"purpose",
models.TextField(help_text="Purpose of the domain", null=True),
),
(
"security_email",
models.CharField(
help_text="Security email for public use",
max_length=320,
null=True,
),
),
(
"anything_else",
models.TextField(
help_text="Anything else we should know?", null=True
),
),
(
"acknowledged_policy",
models.BooleanField(
help_text="Acknowledged .gov acceptable use policy", null=True
),
),
(
"alternative_domains",
models.ManyToManyField(
related_name="alternatives+", to="registrar.website"
),
),
(
"authorizing_official",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="authorizing_official",
to="registrar.contact",
),
),
(
"creator",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="applications_created",
to=settings.AUTH_USER_MODEL,
),
),
(
"current_websites",
models.ManyToManyField(
related_name="current+", to="registrar.website"
),
),
(
"investigator",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="applications_investigating",
to=settings.AUTH_USER_MODEL,
),
),
(
"other_contacts",
models.ManyToManyField(
related_name="contact_applications", to="registrar.contact"
),
),
(
"requested_domain",
models.ForeignKey(
help_text="The requested domain",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="requested+",
to="registrar.website",
),
),
(
"submitter",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="submitted_applications",
to="registrar.contact",
),
),
],
options={
"abstract": False,
},
),
]

View file

@ -1,3 +1,3 @@
from .models import User, UserProfile from .models import User, UserProfile, Contact, Website, DomainApplication
__all__ = ["User", "UserProfile"] __all__ = ["User", "UserProfile", "Contact", "Website", "DomainApplication"]

View file

@ -2,6 +2,10 @@ from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django_fsm import FSMField, transition
from ..api.views.available import string_could_be_domain
class User(AbstractUser): class User(AbstractUser):
""" """
@ -56,7 +60,7 @@ class AddressModel(models.Model):
# don't put anything else here, it will be ignored # don't put anything else here, it will be ignored
class ContactModel(models.Model): class ContactInfo(models.Model):
""" """
An abstract base model that provides common fields An abstract base model that provides common fields
for contact information. for contact information.
@ -71,7 +75,7 @@ class ContactModel(models.Model):
# don't put anything else here, it will be ignored # don't put anything else here, it will be ignored
class UserProfile(TimeStampedModel, ContactModel, AddressModel): class UserProfile(TimeStampedModel, ContactInfo, AddressModel):
user = models.OneToOneField(User, null=True, on_delete=models.CASCADE) user = models.OneToOneField(User, null=True, on_delete=models.CASCADE)
display_name = models.TextField() display_name = models.TextField()
@ -83,3 +87,165 @@ class UserProfile(TimeStampedModel, ContactModel, AddressModel):
return self.user.username return self.user.username
except ObjectDoesNotExist: except ObjectDoesNotExist:
return "No username" return "No username"
class Website(models.Model):
"""Keep domain names in their own table so that applications can refer to
many of them."""
# domain names have strictly limited lengths, 255 characters is more than
# enough.
website = models.CharField(max_length=255, null=False, help_text="")
class Contact(models.Model):
"""Contact information follows a similar pattern for each contact."""
first_name = models.TextField(null=True, help_text="First name")
middle_name = models.TextField(null=True, help_text="Middle name")
last_name = models.TextField(null=True, help_text="Last name")
title = models.TextField(null=True, help_text="Title")
email = models.TextField(null=True, help_text="Email")
phone = models.TextField(null=True, help_text="Phone")
class DomainApplication(TimeStampedModel):
STARTED = "started"
SUBMITTED = "submitted"
INVESTIGATING = "investigating"
APPROVED = "approved"
STATUS_CHOICES = [
(STARTED, STARTED),
(SUBMITTED, SUBMITTED),
(INVESTIGATING, INVESTIGATING),
(APPROVED, APPROVED),
]
status = FSMField(
choices=STATUS_CHOICES, # possible states as an array of constants
default=STARTED, # sensible default
protected=True, # cannot change state directly, must use methods!
)
creator = models.ForeignKey(
User, on_delete=models.PROTECT, related_name="applications_created"
)
investigator = models.ForeignKey(
User,
null=True,
on_delete=models.SET_NULL,
related_name="applications_investigating",
)
# data fields from the initial form
FEDERAL = "federal"
INTERSTATE = "interstate"
STATE_OR_TERRITORY = "state_or_territory"
TRIBAL = "tribal"
COUNTY = "county"
CITY = "city"
SPECIAL_DISTRICT = "special_district"
ORGANIZATION_CHOICES = [
(FEDERAL, "a federal agency"),
(INTERSTATE, "an organization of two or more states"),
(
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,
"a tribal government recognized by the federal or " "state government",
),
(COUNTY, "a county, parish, or borough"),
(CITY, "a city, town, township, village, etc."),
(SPECIAL_DISTRICT, "an independent organization within a single state"),
]
organization_type = models.CharField(
max_length=255, choices=ORGANIZATION_CHOICES, help_text="Type of Organization"
)
EXECUTIVE = "Executive"
JUDICIAL = "Judicial"
LEGISLATIVE = "Legislative"
BRANCH_CHOICES = [(x, x) for x in (EXECUTIVE, JUDICIAL, LEGISLATIVE)]
federal_branch = models.CharField(
max_length=50,
choices=BRANCH_CHOICES,
null=True,
help_text="Branch of federal government",
)
is_election_office = models.BooleanField(
null=True, help_text="Is your ogranization an election office?"
)
organization_name = models.TextField(null=True, help_text="Organization name")
street_address = models.TextField(null=True, help_text="Street Address")
unit_type = models.CharField(max_length=15, null=True, help_text="Unit type")
unit_number = models.CharField(max_length=255, null=True, help_text="Unit number")
state_territory = models.CharField(
max_length=2, null=True, help_text="State/Territory"
)
zip_code = models.CharField(max_length=10, null=True, help_text="ZIP code")
authorizing_official = models.ForeignKey(
Contact,
null=True,
related_name="authorizing_official",
on_delete=models.PROTECT,
)
# "+" means no reverse relation to lookup applications from Website
current_websites = models.ManyToManyField(Website, related_name="current+")
requested_domain = models.ForeignKey(
Website,
null=True,
help_text="The requested domain",
related_name="requested+",
on_delete=models.PROTECT,
)
alternative_domains = models.ManyToManyField(Website, related_name="alternatives+")
submitter = models.ForeignKey(
Contact, null=True, related_name="submitted_applications", on_delete=models.PROTECT
)
purpose = models.TextField(null=True, help_text="Purpose of the domain")
other_contacts = models.ManyToManyField(
Contact, related_name="contact_applications"
)
security_email = models.CharField(
max_length=320, null=True, help_text="Security email for public use"
)
anything_else = models.TextField(
null=True, help_text="Anything else we should know?"
)
acknowledged_policy = models.BooleanField(
null=True,
help_text="Acknowledged .gov acceptable use policy"
)
def can_submit(self):
"""Return True if this instance can be marked as submitted."""
if not string_could_be_domain(requested_domain):
return False
return True
@transition(
field="status", source=STARTED, target=SUBMITTED, conditions=[can_submit]
)
def submit(self):
"""Submit an application that is started."""
# don't need to do anything inside this method although we could
pass

View file

@ -0,0 +1,48 @@
from django.test import TestCase
from django.db.utils import IntegrityError
from registrar.models import Contact, DomainApplication, User, Website
class TestDomainApplication(TestCase):
def test_empty_create_fails(self):
"""Can't create a completely empty domain application."""
with self.assertRaisesRegex(IntegrityError, "creator"):
DomainApplication.objects.create()
def test_minimal_create(self):
"""Can create with just a creator."""
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user)
self.assertEquals(application.status, DomainApplication.STARTED)
def test_full_create(self):
"""Can create with all fields."""
user, _ = User.objects.get_or_create()
contact = Contact.objects.create()
com_website, _ = Website.objects.get_or_create(website="igorville.com")
gov_website, _ = Website.objects.get_or_create(website="igorville.gov")
application = DomainApplication.objects.create(
creator=user,
investigator=user,
organization_type=DomainApplication.FEDERAL,
federal_branch=DomainApplication.EXECUTIVE,
is_election_office=False,
organization_name="Test",
street_address="100 Main St.",
unit_type="APT",
unit_number="1A",
state_territory="CA",
zip_code="12345-6789",
authorizing_official=contact,
requested_domain=gov_website,
submitter=contact,
purpose="Igorville rules!",
security_email="security@igorville.gov",
anything_else="All of Igorville loves the dotgov program.",
acknowledged_policy=True,
)
application.current_websites.add(com_website)
application.alternative_domains.add(gov_website)
application.other_contacts.add(contact)
application.save()