diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index af887a5f3b..0000000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index f729763635..83688be353 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,6 @@ tdrs-frontend/.env* #BACKEND -tdrs-backend/*/env_vars/*.env* -tdrs-backend/*/env_vars/*.env*.* tdrs-backend/vars-backend.yml tdrs-backend/coverage.xml tdrs-backend/htmlcov/* @@ -38,6 +36,7 @@ tmp *.txt compliance/opencontrols/ compliance/exports/ +tdrs-backend/tdpservice/static/* # don't ignore requirements.txt !requirements.txt @@ -87,3 +86,6 @@ htmlcov/* # Coverage tdrs-backend/htmlcov/* + +# VIM +*.swp diff --git a/docs/Technical-Documentation/user_role_management.md b/docs/Technical-Documentation/user_role_management.md new file mode 100644 index 0000000000..1dbcec44c6 --- /dev/null +++ b/docs/Technical-Documentation/user_role_management.md @@ -0,0 +1,169 @@ +# User Role Management + +## Updating User Roles through the Django Admin Interface + +**Description** + +For the OFA MVP we will be assigning and updating application user roles through the +Django Admin Interface. This can be accessed via the backend at: + +`/admin` + +The admin interface requires special privileges which can only be granted via the +Django CLI [Detailed Below](#cli) or a Django Data migration. + +Once a user has been granted privileges they can go to the admin page described above +and log in. The admin interface provides links to Users, STTs, Regions and Groups which +can can be each be modified through the interface. + +### Log in to Admin + +- Enter your username and password to gain access + +![](images/admin_login.png) + +### Admin Home + +- The admin home page gives you access to all of the objects you can manage + +![](images/admin_home.png) + +### Group List + +- Clicking on "Groups" from the Admin Home page gives you a list of all of the existing groups. +- Current groups listed are "OFA Admin" and "Data Prepper". + +![](images/group_list.png) + +### Group Permissions + +- When you click on a group in the Group List, you can add/update/remove permissions for each group. + +![](images/group_permissions.png) + +### Region List + +- When you click on "Regions" from the Admin Home page, you can view the regions currently in the system + +![](images/region_list.png) + +### STT List + +- When you click on "STTs" from the Admin Home page, you can view a list of the STTs currently in the system + +![](images/stt_list.png) + +### STT Edit + +- Clicking on an STT allows you to update it's data + +![](images/stt_edit.png) + +### User List + +- When you click on "Users" from the Admin Home page, you can see a list of users currently in the system. + +![](images/user_list.png) + +### User Edit + +- When you click on a user from the User List, you can edit that user's information, including +first name, last name and username, as well as the user's Active Status, assigned Groups, STT and Region. You can +also view the date and time the user joined and when they last logged in. + +![](images/user_edit.png) + + +## Updating User Roles through the CLI + +**Description** + +For the OFA MVP, we will need to assign the Django built-in roles of `superuser` and `staff` to the deployed applicataion. +This will be needed for users to have access to the Django Admin interface detailed above. + +This guide will provide instructions on how to define them in local and deployed environments. +Access to the CLI is strictly controlled by the Product Owner. + + +**Local Development** + +1.) After following the instructions in the README.md for the TDRS Frontend and +Backend services, you will now be able to login via Login.gov which will result in +your account being generated and stored in the local database. + +2.) With the backend instance running execute the following commands from the +`tdrs-backend` directory: + + + ```bash + docker-compose run web sh -c "python manage.py shell" + ``` + This will open up a shell prompt that will allow you to execute commands + directly to the TDRS Backend Django application. + + To update your user, edit the sample script below to reflect your Login.gov + email associated with your login process and press enter. After this you may + exit the shell and resume using the application with the `superuser` role. + + ``` +from django.contrib.auth import get_user_model +User = get_user_model() +user = User.objects.get(username="test@example.com") +user.is_superuser = True +user.save() +``` + +To assign the user to a custom role, like `Data Prepper` use the following + +``` +from django.contrib.auth.models import Group +from django.contrib.auth import get_user_model + +data_prepper = Group.objects.get(name="Data Prepper") +User = get_user_model() +user = User.objects.get(username="test@example.com") +data_prepper.user_set.add(user) + +``` + + + **Deployed Evironment** + +1.) Users targeted for Superuser creation will have to be manually elevated by system administrators with access to the intended Cloud.gov environment. + +2.) Admins with access will have to ssh into the environment via the following command + + ```bash + cf ssh tdp-backend +``` + +3.) After moving into the `tdpapp` directory, the admin will then have to set the alias for the python executable if it has not been set and execute the shell script to promote the existing user. + +Commands to move to the correct directory and make python available +```bash +cd ../tdpapp +alias pytemp='/usr/local/bin/python3.7' +pytemp manage.py shell +``` + +Python script to promote the targer user to `superuser`: + +``` +from django.contrib.auth import get_user_model +User = get_user_model() +user = User.objects.get(username="test@example.com") +user.is_superuser = True +user.save() +``` +See an example of this [here](../images/make_superuser_example.png) + +To assign the user to a custom role, like `Data Prepper` use the following + +``` +from django.contrib.auth.models import Group +from django.contrib.auth import get_user_model + +data_prepper = Group.objects.get(name="Data Prepper") +User = get_user_model() +user = User.objects.get(username="test@example.com") +data_prepper.user_set.add(user) diff --git a/tdrs-backend/Pipfile b/tdrs-backend/Pipfile index d2d3fe8856..5ce0c2e8f6 100644 --- a/tdrs-backend/Pipfile +++ b/tdrs-backend/Pipfile @@ -41,6 +41,7 @@ ptpython = "*" pyjwt = "*" python-dotenv = "*" requests = "*" +django-admin-logs = "*" [requires] python_version = "3.8" diff --git a/tdrs-backend/Pipfile.lock b/tdrs-backend/Pipfile.lock index a4bf212f8e..7144321aa1 100644 --- a/tdrs-backend/Pipfile.lock +++ b/tdrs-backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f37285af1a60f75ef46041162c3e5ca11db6ccdb4230e96a3019a32fa3c59357" + "sha256": "9e2f894bc2ad6f41df2c74147e6d424035a65d62bf6ba249ed3eb5991a1c86ad" }, "pipfile-spec": 6, "requires": { @@ -23,12 +23,21 @@ ], "version": "==1.4.4" }, + "appnope": { + "hashes": [ + "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442", + "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.1.2" + }, "asgiref": { "hashes": [ - "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", - "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" + "sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17", + "sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0" ], - "version": "==3.2.10" + "markers": "python_version >= '3.5'", + "version": "==3.3.1" }, "backcall": { "hashes": [ @@ -39,66 +48,66 @@ }, "boto3": { "hashes": [ - "sha256:28bf1bce2979d4d1674d63b1b4d6ac30b6844b5d3604e69d8847b18602588861", - "sha256:78f3ebcdff149d5327f27a5c461a9e394306b7db9a60e8bd65c9401cc41d99d3" + "sha256:2a6e92194bd6f2341908dc9b133af057ea1ff20b7d7e54674f48cdb531d93ca5", + "sha256:a35e0915547ea659ddd832c9aaf55038c56fa894c4cc2a2a46cd6c642494012a" ], "index": "pypi", - "version": "==1.15.0" + "version": "==1.16.35" }, "botocore": { "hashes": [ - "sha256:de5f9fc0c7e88ee7ba831fa27475be258ae09ece99143ed623d3618a3c84ee2c", - "sha256:e224754230e7e015836ba20037cac6321e8e2ce9b8627c14d579fcb37249decd" + "sha256:633aa910509b060717df4130f7e2841f1101c0c47fd5871f4903b4b1dbab7e23", + "sha256:d31dce56799edb5796085d5296931faae201e28e14e568d9db4dac237a135fe3" ], - "version": "==1.18.18" + "version": "==1.19.35" }, "certifi": { "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2020.6.20" + "version": "==2020.12.5" }, "cffi": { "hashes": [ - "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", - "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", - "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", - "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", - "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", - "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", - "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", - "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", - "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", - "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", - "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", - "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", - "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", - "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", - "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", - "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", - "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", - "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", - "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", - "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", - "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", - "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", - "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", - "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", - "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", - "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", - "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", - "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", - "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", - "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", - "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", - "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", - "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", - "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", - "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", - "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" - ], - "version": "==1.14.3" + "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e", + "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d", + "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a", + "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec", + "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362", + "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668", + "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c", + "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b", + "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06", + "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698", + "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2", + "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c", + "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7", + "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009", + "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03", + "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b", + "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909", + "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53", + "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35", + "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26", + "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b", + "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01", + "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb", + "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293", + "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd", + "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d", + "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3", + "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d", + "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e", + "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca", + "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d", + "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775", + "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375", + "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b", + "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b", + "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f" + ], + "version": "==1.14.4" }, "chardet": { "hashes": [ @@ -109,31 +118,23 @@ }, "cryptography": { "hashes": [ - "sha256:22f8251f68953553af4f9c11ec5f191198bc96cff9f0ac5dd5ff94daede0ee6d", - "sha256:284e275e3c099a80831f9898fb5c9559120d27675c3521278faba54e584a7832", - "sha256:3e17d02941c0f169c5b877597ca8be895fca0e5e3eb882526a74aa4804380a98", - "sha256:52a47e60953679eea0b4d490ca3c241fb1b166a7b161847ef4667dfd49e7699d", - "sha256:57b8c1ed13b8aa386cabbfde3be175d7b155682470b0e259fecfe53850967f8a", - "sha256:6a8f64ed096d13f92d1f601a92d9fd1f1025dc73a2ca1ced46dcf5e0d4930943", - "sha256:6e8a3c7c45101a7eeee93102500e1b08f2307c717ff553fcb3c1127efc9b6917", - "sha256:7ef41304bf978f33cfb6f43ca13bb0faac0c99cda33693aa20ad4f5e34e8cb8f", - "sha256:87c2fffd61e934bc0e2c927c3764c20b22d7f5f7f812ee1a477de4c89b044ca6", - "sha256:88069392cd9a1e68d2cfd5c3a2b0d72a44ef3b24b8977a4f7956e9e3c4c9477a", - "sha256:8a0866891326d3badb17c5fd3e02c926b635e8923fa271b4813cd4d972a57ff3", - "sha256:8f0fd8b0751d75c4483c534b209e39e918f0d14232c0d8a2a76e687f64ced831", - "sha256:9a07e6d255053674506091d63ab4270a119e9fc83462c7ab1dbcb495b76307af", - "sha256:9a8580c9afcdcddabbd064c0a74f337af74ff4529cdf3a12fa2e9782d677a2e5", - "sha256:bd80bc156d3729b38cb227a5a76532aef693b7ac9e395eea8063ee50ceed46a5", - "sha256:d1cbc3426e6150583b22b517ef3720036d7e3152d428c864ff0f3fcad2b97591", - "sha256:e15ac84dcdb89f92424cbaca4b0b34e211e7ce3ee7b0ec0e4f3c55cee65fae5a", - "sha256:e4789b84f8dedf190148441f7c5bfe7244782d9cbb194a36e17b91e7d3e1cca9", - "sha256:f01c9116bfb3ad2831e125a73dcd957d173d6ddca7701528eff1e7d97972872c", - "sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae", - "sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f", - "sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef" - ], - "index": "pypi", - "version": "==3.2" + "sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d", + "sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7", + "sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901", + "sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c", + "sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244", + "sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6", + "sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5", + "sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e", + "sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c", + "sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0", + "sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812", + "sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a", + "sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030", + "sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302" + ], + "index": "pypi", + "version": "==3.3.1" }, "decorator": { "hashes": [ @@ -152,11 +153,19 @@ }, "django": { "hashes": [ - "sha256:59c8125ca873ed3bdae9c12b146fbbd6ed8d0f743e4cf5f5817af50c51f1fc2f", - "sha256:b5fbb818e751f660fa2d576d9f40c34a4c615c8b48dd383f5216e609f383371f" + "sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2", + "sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03" ], "index": "pypi", - "version": "==3.1.1" + "version": "==3.1.4" + }, + "django-admin-logs": { + "hashes": [ + "sha256:565ea1e86c9b702c0274b648ed6c0be937b3086d65b664dd635ce00475d4ed39", + "sha256:a68d4887f895d42f7fbb55fdc5f8a85edb99f696b8d722370bfd7083aa95aeec" + ], + "index": "pypi", + "version": "==1.0.1" }, "django-configurations": { "hashes": [ @@ -168,35 +177,35 @@ }, "django-cors-headers": { "hashes": [ - "sha256:9322255c296d5f75089571f29e520c83ff9693df17aa3cf9f6a4bea7c6740169", - "sha256:db82b2840f667d47872ae3e4a4e0a0d72fbecb42779b8aa233fa8bb965f7836a" + "sha256:5665fc1b1aabf1b678885cf6f8f8bd7da36ef0a978375e767d491b48d3055d8f", + "sha256:ba898dd478cd4be3a38ebc3d8729fa4d044679f8c91b2684edee41129d7e968a" ], "index": "pypi", - "version": "==3.5.0" + "version": "==3.6.0" }, "django-extensions": { "hashes": [ - "sha256:6809c89ca952f0e08d4e0766bc0101dfaf508d7649aced1180c091d737046ea7", - "sha256:dc663652ac9460fd06580a973576820430c6d428720e874ae46b041fa63e0efa" + "sha256:7cd002495ff0a0e5eb6cdd6be759600905b4e4079232ea27618fc46bdd853651", + "sha256:c7f88625a53f631745d4f2bef9ec4dcb999ed59476393bdbbe99db8596778846" ], "index": "pypi", - "version": "==3.0.9" + "version": "==3.1.0" }, "django-filter": { "hashes": [ - "sha256:11e63dd759835d9ba7a763926ffb2662cf8a6dcb4c7971a95064de34dbc7e5af", - "sha256:616848eab6fc50193a1b3730140c49b60c57a3eda1f7fc57fa8505ac156c6c75" + "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06", + "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1" ], "index": "pypi", - "version": "==2.3.0" + "version": "==2.4.0" }, "django-model-utils": { "hashes": [ - "sha256:9cf882e5b604421b62dbe57ad2b18464dc9c8f963fc3f9831badccae66c1139c", - "sha256:adf09e5be15122a7f4e372cb5a6dd512bbf8d78a23a90770ad0983ee9d909061" + "sha256:eb5dd05ef7d7ce6bc79cae54ea7c4a221f6f81e2aad7722933aee66489e7264b", + "sha256:ef7c440024e797796a3811432abdd2be8b5225ae64ef346f8bfc6de7d8e5d73c" ], "index": "pypi", - "version": "==4.0.0" + "version": "==4.1.1" }, "django-storages": { "hashes": [ @@ -216,11 +225,10 @@ }, "djangorestframework": { "hashes": [ - "sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32", - "sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b" + "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7" ], "index": "pypi", - "version": "==3.11.1" + "version": "==3.12.2" }, "gunicorn": { "hashes": [ @@ -235,22 +243,23 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "ipdb": { "hashes": [ - "sha256:d6f46d261c45a65e65a2f7ec69288a1c511e16206edb2875e7ec6b2f66997e78" + "sha256:c85398b5fb82f82399fc38c44fe3532c0dde1754abee727d8f5cfcc74547b334" ], "index": "pypi", - "version": "==0.13.3" + "version": "==0.13.4" }, "ipython": { "hashes": [ - "sha256:2e22c1f74477b5106a6fb301c342ab8c64bb75d702e350f05a649e8cb40a0fb8", - "sha256:a331e78086001931de9424940699691ad49dfb457cea31f5471eae7b78222d5e" + "sha256:c987e8178ced651532b3b1ff9965925bfd445c279239697052561a9ab806d28f", + "sha256:cbb2ef3d5961d44e6a963b9817d4ea4e1fa2eb589c371a470fed14d8d40cbd6a" ], "index": "pypi", - "version": "==7.18.1" + "version": "==7.19.0" }, "ipython-genutils": { "hashes": [ @@ -264,6 +273,7 @@ "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20", "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.17.2" }, "jmespath": { @@ -271,6 +281,7 @@ "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.0" }, "jwcrypto": { @@ -283,17 +294,18 @@ }, "markdown": { "hashes": [ - "sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17", - "sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59" + "sha256:5d9f2b5ca24bc4c7a390d22323ca4bad200368612b5aaa7796babf971d2b2f18", + "sha256:c109c15b7dc20a9ac454c9e6025927d44460b85bd039da028d85e2b6d0bcc328" ], "index": "pypi", - "version": "==3.2.2" + "version": "==3.3.3" }, "parso": { "hashes": [ "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea", "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.7.1" }, "pexpect": { @@ -316,54 +328,57 @@ "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c", "sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63" ], + "markers": "python_full_version >= '3.6.1'", "version": "==3.0.8" }, "psycopg2-binary": { "hashes": [ - "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", - "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", - "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", + "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", + "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", + "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", + "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", + "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5", "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", + "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", + "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", + "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", + "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", - "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", - "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", - "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", - "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", - "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", + "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", + "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", + "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", + "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", + "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", - "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", - "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", - "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", - "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", - "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", - "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", - "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", - "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", - "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", - "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", + "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", + "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", + "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", - "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", + "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", + "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", + "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", - "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", - "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", - "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", - "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", + "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", + "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", + "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", + "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", - "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" + "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", + "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f" ], "index": "pypi", "version": "==2.8.6" }, "ptpython": { "hashes": [ - "sha256:382d080e4130b9be254776787206380359be4f73c25e6b4e8cc371905c4fa587", - "sha256:5094e7e4daa77453d3c33eb7b7ebbf1060be4446521865a94e698bc85ff15930" + "sha256:34814eb410f854c823be4c4a34124e1dc8ca696da1c1fa611f9da606c5a8a609", + "sha256:c40b096c80bd09ebc4012017444cf7281fe5308f6dd7ca6a45ef9250cb271428" ], "index": "pypi", - "version": "==3.0.5" + "version": "==3.0.7" }, "ptyprocess": { "hashes": [ @@ -377,14 +392,16 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pygments": { "hashes": [ - "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0", - "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773" + "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716", + "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08" ], - "version": "==2.7.2" + "markers": "python_version >= '3.5'", + "version": "==2.7.3" }, "pyjwt": { "hashes": [ @@ -399,30 +416,31 @@ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.1" }, "python-dotenv": { "hashes": [ - "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", - "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" + "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e", + "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0" ], "index": "pypi", - "version": "==0.14.0" + "version": "==0.15.0" }, "pytz": { "hashes": [ - "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", - "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268", + "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd" ], - "version": "==2020.1" + "version": "==2020.4" }, "requests": { "hashes": [ - "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", - "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", + "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" ], "index": "pypi", - "version": "==2.24.0" + "version": "==2.25.0" }, "s3transfer": { "hashes": [ @@ -436,6 +454,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "sqlparse": { @@ -443,6 +462,7 @@ "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" ], + "markers": "python_version >= '3.5'", "version": "==0.4.1" }, "traitlets": { @@ -450,15 +470,16 @@ "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396", "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426" ], + "markers": "python_version >= '3.7'", "version": "==5.0.5" }, "urllib3": { "hashes": [ - "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", - "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" + "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", + "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" ], "markers": "python_version != '3.4'", - "version": "==1.25.11" + "version": "==1.26.2" }, "wcwidth": { "hashes": [ @@ -478,10 +499,11 @@ }, "attrs": { "hashes": [ - "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", - "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "version": "==20.2.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" }, "black": { "hashes": [ @@ -497,6 +519,7 @@ "sha256:b1fdd7e7a675295630f9ae71527a8ebc10bfefa236b3d6aa4932ee4462c17ba3", "sha256:caad5211e7ba5afe04367cdd4cfc68fa886e2e08f6f35e76b7387d2109ccea6e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.7" }, "click": { @@ -504,6 +527,7 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "coverage": { @@ -543,29 +567,32 @@ "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==5.3" }, "factory-boy": { "hashes": [ - "sha256:2ce2f665045d9f15145a6310565fcb8255d52fc6fd867f3b783b3ac3de6cf10e" + "sha256:d8626622550c8ba31392f9e19fdbcef9f139cf1ad643c5923f20490a7b3e2e3d", + "sha256:ded73e49135c24bd4d3f45bf1eb168f8d290090f5cf4566b8df3698317dc9c08" ], "index": "pypi", - "version": "==3.0.1" + "version": "==3.1.0" }, "faker": { "hashes": [ - "sha256:30afa8f564350770373f299d2d267bff42aaba699a7ae0a3b6f378b2a8170569", - "sha256:a7a36c3c657f06bd1e3e3821b9480f2a92017d8a26e150e464ab6b97743cbc92" + "sha256:1fcb415562ee6e2395b041e85fa6901d4708d30b84d54015226fa754ed0822c3", + "sha256:e8beccb398ee9b8cc1a91d9295121d66512b6753b4846eb1e7370545d46b3311" ], - "version": "==4.14.0" + "markers": "python_version >= '3.6'", + "version": "==5.0.1" }, "flake8": { "hashes": [ - "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", - "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" + "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", + "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" ], "index": "pypi", - "version": "==3.8.3" + "version": "==3.8.4" }, "flake8-docstrings": { "hashes": [ @@ -579,6 +606,7 @@ "hashes": [ "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.18.2" }, "iniconfig": { @@ -590,25 +618,27 @@ }, "isort": { "hashes": [ - "sha256:171c5f365791073426b5ed3a156c2081a47f88c329161fd28228ff2da4c97ddb", - "sha256:ba91218eee31f1e300ecc079ef0c524cea3fc41bfbb979cbdf5fd3a889e3cfed" + "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7", + "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58" ], "index": "pypi", - "version": "==5.5.2" + "version": "==5.6.4" }, "jinja2": { "hashes": [ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.2" }, "joblib": { "hashes": [ - "sha256:698c311779f347cf6b7e6b8a39bb682277b8ee4aba8cf9507bc0cf4cd4737b72", - "sha256:9e284edd6be6b71883a63c9b7f124738a3c16195513ad940eae7e3438de885d5" + "sha256:75ead23f13484a2a414874779d69ade40d4fa1abe62b222a23cd50d4bc822f6f", + "sha256:7ad866067ac1fdec27d51c8678ea760601b70e32ff1881d4dc8e1171f2b64b24" ], - "version": "==0.17.0" + "markers": "python_version >= '3.6'", + "version": "==1.0.0" }, "livereload": { "hashes": [ @@ -628,11 +658,11 @@ }, "markdown": { "hashes": [ - "sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17", - "sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59" + "sha256:5d9f2b5ca24bc4c7a390d22323ca4bad200368612b5aaa7796babf971d2b2f18", + "sha256:c109c15b7dc20a9ac454c9e6025927d44460b85bd039da028d85e2b6d0bcc328" ], "index": "pypi", - "version": "==3.2.2" + "version": "==3.3.3" }, "markupsafe": { "hashes": [ @@ -670,6 +700,7 @@ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "mccabe": { @@ -687,13 +718,6 @@ "index": "pypi", "version": "==1.1.2" }, - "more-itertools": { - "hashes": [ - "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20", - "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c" - ], - "version": "==8.5.0" - }, "nltk": { "hashes": [ "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35" @@ -718,37 +742,41 @@ }, "packaging": { "hashes": [ - "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", - "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858", + "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093" ], - "version": "==20.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.8" }, "pathspec": { "hashes": [ - "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", - "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", + "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" ], - "version": "==0.8.0" + "version": "==0.8.1" }, "pluggy": { "hashes": [ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, "py": { "hashes": [ - "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", - "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" ], - "version": "==1.9.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" }, "pycodestyle": { "hashes": [ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pydocstyle": { @@ -756,6 +784,7 @@ "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" ], + "markers": "python_version >= '3.5'", "version": "==5.1.1" }, "pyflakes": { @@ -763,6 +792,7 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pygraphviz": { @@ -782,11 +812,11 @@ }, "pytest": { "hashes": [ - "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40", - "sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043" + "sha256:b12e09409c5bdedc28d308469e156127004a436b41e9b44f9bff6446cbab9152", + "sha256:d69e1a80b34fe4d596c9142f35d9e523d98a2838976f1a68419a8f051b24cec6" ], "index": "pypi", - "version": "==6.0.2" + "version": "==6.2.0" }, "pytest-cov": { "hashes": [ @@ -798,11 +828,11 @@ }, "pytest-django": { "hashes": [ - "sha256:4de6dbd077ed8606616958f77655fed0d5e3ee45159475671c7fa67596c6dba6", - "sha256:c33e3d3da14d8409b125d825d4e74da17bb252191bf6fc3da6856e27a8b73ea4" + "sha256:10e384e6b8912ded92db64c58be8139d9ae23fb8361e5fc139d8e4f8fc601bc2", + "sha256:26f02c16d36fd4c8672390deebe3413678d89f30720c16efb8b2a6bf63b9041f" ], "index": "pypi", - "version": "==3.10.0" + "version": "==4.1.0" }, "pytest-mock": { "hashes": [ @@ -817,6 +847,7 @@ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.1" }, "pyyaml": { @@ -824,11 +855,13 @@ "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" @@ -837,41 +870,56 @@ }, "regex": { "hashes": [ - "sha256:0cb23ed0e327c18fb7eac61ebbb3180ebafed5b9b86ca2e15438201e5903b5dd", - "sha256:1a065e7a6a1b4aa851a0efa1a2579eabc765246b8b3a5fd74000aaa3134b8b4e", - "sha256:1a511470db3aa97432ac8c1bf014fcc6c9fbfd0f4b1313024d342549cf86bcd6", - "sha256:1c447b0d108cddc69036b1b3910fac159f2b51fdeec7f13872e059b7bc932be1", - "sha256:2278453c6a76280b38855a263198961938108ea2333ee145c5168c36b8e2b376", - "sha256:240509721a663836b611fa13ca1843079fc52d0b91ef3f92d9bba8da12e768a0", - "sha256:4e21340c07090ddc8c16deebfd82eb9c9e1ec5e62f57bb86194a2595fd7b46e0", - "sha256:570e916a44a361d4e85f355aacd90e9113319c78ce3c2d098d2ddf9631b34505", - "sha256:59d5c6302d22c16d59611a9fd53556554010db1d47e9df5df37be05007bebe75", - "sha256:6a46eba253cedcbe8a6469f881f014f0a98819d99d341461630885139850e281", - "sha256:6f567df0601e9c7434958143aebea47a9c4b45434ea0ae0286a4ec19e9877169", - "sha256:781906e45ef1d10a0ed9ec8ab83a09b5e0d742de70e627b20d61ccb1b1d3964d", - "sha256:8469377a437dbc31e480993399fd1fd15fe26f382dc04c51c9cb73e42965cc06", - "sha256:8cd0d587aaac74194ad3e68029124c06245acaeddaae14cb45844e5c9bebeea4", - "sha256:97a023f97cddf00831ba04886d1596ef10f59b93df7f855856f037190936e868", - "sha256:a973d5a7a324e2a5230ad7c43f5e1383cac51ef4903bf274936a5634b724b531", - "sha256:af360e62a9790e0a96bc9ac845d87bfa0e4ee0ee68547ae8b5a9c1030517dbef", - "sha256:b706c70070eea03411b1761fff3a2675da28d042a1ab7d0863b3efe1faa125c9", - "sha256:bfd7a9fddd11d116a58b62ee6c502fd24cfe22a4792261f258f886aa41c2a899", - "sha256:c30d8766a055c22e39dd7e1a4f98f6266169f2de05db737efe509c2fb9c8a3c8", - "sha256:c53dc8ee3bb7b7e28ee9feb996a0c999137be6c1d3b02cb6b3c4cba4f9e5ed09", - "sha256:c95d514093b80e5309bdca5dd99e51bcf82c44043b57c34594d9d7556bd04d05", - "sha256:d43cf21df524283daa80ecad551c306b7f52881c8d0fe4e3e76a96b626b6d8d8", - "sha256:d62205f00f461fe8b24ade07499454a3b7adf3def1225e258b994e2215fd15c5", - "sha256:e289a857dca3b35d3615c3a6a438622e20d1bf0abcb82c57d866c8d0be3f44c4", - "sha256:e5f6aa56dda92472e9d6f7b1e6331f4e2d51a67caafff4d4c5121cadac03941e", - "sha256:f4b1c65ee86bfbf7d0c3dfd90592a9e3d6e9ecd36c367c884094c050d4c35d04" - ], - "version": "==2020.10.23" + "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538", + "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4", + "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc", + "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa", + "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444", + "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1", + "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af", + "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8", + "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9", + "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88", + "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba", + "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364", + "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e", + "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7", + "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0", + "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31", + "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683", + "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee", + "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b", + "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884", + "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c", + "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e", + "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562", + "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85", + "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c", + "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6", + "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d", + "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b", + "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70", + "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b", + "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b", + "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f", + "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0", + "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5", + "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5", + "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f", + "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e", + "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512", + "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d", + "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917", + "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f" + ], + "version": "==2020.11.13" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { @@ -890,31 +938,66 @@ }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "version": "==0.10.1" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" }, "tornado": { "hashes": [ - "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc", - "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52", - "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6", - "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d", - "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b", - "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673", - "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9", - "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a", - "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740" - ], - "version": "==6.0.4" + "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb", + "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c", + "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288", + "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95", + "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558", + "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe", + "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791", + "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d", + "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326", + "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b", + "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4", + "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c", + "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910", + "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5", + "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c", + "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0", + "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675", + "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd", + "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f", + "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c", + "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea", + "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6", + "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05", + "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd", + "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575", + "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a", + "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37", + "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795", + "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f", + "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32", + "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c", + "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01", + "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4", + "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2", + "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921", + "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085", + "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df", + "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102", + "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5", + "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68", + "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5" + ], + "markers": "python_version >= '3.5'", + "version": "==6.1" }, "tqdm": { "hashes": [ - "sha256:9ad44aaf0fc3697c06f6e05c7cf025dd66bc7bcb7613c66d85f4464c47ac8fad", - "sha256:ef54779f1c09f346b2b5a8e5c61f96fbcb639929e640e59f8cf810794f406432" + "sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5", + "sha256:d4f413aecb61c9779888c64ddf0c62910ad56dcbe857d8922bb505d4dbff0df1" ], - "version": "==4.51.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==4.54.1" }, "typed-ast": { "hashes": [ @@ -931,6 +1014,7 @@ "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", @@ -942,8 +1026,10 @@ "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", + "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], "version": "==1.4.1" diff --git a/tdrs-backend/README.md b/tdrs-backend/README.md index d2bab63c5d..5c281d86fc 100644 --- a/tdrs-backend/README.md +++ b/tdrs-backend/README.md @@ -26,13 +26,14 @@ This project uses a Pipfile for dependency management. However, due to the limit **Commands are to be executed from within the `tdrs-backend` directory** +1.) For configuration of the `JWT_KEY` and `JWT_CERT_TEST` environment variables for local development/testing documentation is forthcoming. For configuration of a superuser for admin tasks please refer to the [user_role_management.md](docs/user_role_management.md) guide. -1.) Configure your local environment variables via the `.env.local` file found in this path: +2.) Configure your local environment variables via the `.env.local` file found in this path: ```tdpservice/settings/env_vars/.env.local``` -2.)Build and start the backend via docker-compose: +3.)Build and start the backend via docker-compose: ```bash @@ -41,21 +42,21 @@ $ docker-compose up -d --build This command will start the following containers: `tdrs-backend_web_1` (webserver) on port `8080`, `tdrs-backend_postgres_1` (`postgresql` DB) on port `5432`, and `tdrs-backend_zaproxy_1` (OWASP ZAP). -3.) The backend service will now be available via the following URL: +4.) The backend service will now be available via the following URL: ``` http://localhost:8080 ``` -4.) To get an OpenAPI compliant schema of all the API endpoints, do a `GET` on `http://localhost:8080/api-scehma.json` or go to http://localhost:8080/apidocs/ in the browser to view all API endpoints. +5.) To get an OpenAPI compliant schema of all the API endpoints, do a `GET` on `http://localhost:8080/api-scehma.json` or go to http://localhost:8080/apidocs/ in the browser to view all API endpoints. -5.) To `exec` into the PostgreSQL database in the container. +6.) To `exec` into the PostgreSQL database in the container. ```bash $ docker exec -it tdrs-backend_postgres_1 psql -U tdpuser -d tdrs_test ``` -5.) Backend project tear down: +7.) Backend project tear down: ```bash $ docker-compose down --remove-orphans @@ -95,7 +96,6 @@ Although CircleCi is [set up to auto deploy](https://github.com/raft-tech/TANF-a 1.) Build and push a tagged docker image while on the the target Github branch: - ```bash $ docker build -t goraftdocker/tdp-backend:local . -f docker/Dockerfile.dev @@ -154,4 +154,4 @@ $ cf bind-service tdp-backend tdp-db 6.) To apply this newly bound service or apply any changes made to environment variables you will need to restage the application: ```bash $ cf restage tdp-backend -``` \ No newline at end of file +``` diff --git a/tdrs-backend/docs/api/roles.md b/tdrs-backend/docs/api/roles.md new file mode 100644 index 0000000000..d52d855656 --- /dev/null +++ b/tdrs-backend/docs/api/roles.md @@ -0,0 +1,83 @@ + +# Roles +Accepts GET requests from [authenticated](api/authentication.md) Admin users to get a list of roles in the system. + + +---- +**Request**: + +`GET` `v1/roles/` +`GET` `v1/roles/{id}` + +Parameters: + +- Valid httpOnly cookie in the request header to track the users session + +*Note:* + +- Authorization Protected and Admin Only + +**Response**: + +```json +Content-Type application/json +200 Ok + +[ + { + "id": 1, + "name": "OFA Admin", + "permissions": [ + { + "id": 1, + "codename": "add_logentry", + "name": "Can add log entry" + }, + { + "id": 2, + "codename": "change_logentry", + "name": "Can change log entry" + }, + ] + }, + { + "id": 2, + "name": "Data Prepper", + "permissions": [ + { + "id": 36, + "codename": "view_stt", + "name": "Can view stt" + }, + ] + } +] +``` + +This will return a JSON response with the currently defined list of roles and their associated permissions (referenced by permission ID).Or an individual role if the associated group ID is included in the request + +- **id**: Integer value noting the primary key of the role in relation to its row in the database. +- **name**: A user friendly description of the role. +- **permission**: A list of permissions by their associated unique database primary key(ID). + +---- +**Failure to Authenticate Response:** + +```json +Content-Type application/json +403 Forbidden + +{ + "detail": "Authentication credentials were not provided." +} +``` +---- +**Calls made by authorized users who are not Admins:** +```json +Content-Type application/json +500 Internal Server Error + +System Error Message: +Does Not Exist +Group matching query does not exist. +``` diff --git a/tdrs-backend/docs/images/admin_home.png b/tdrs-backend/docs/images/admin_home.png new file mode 100644 index 0000000000..42a28ccc78 Binary files /dev/null and b/tdrs-backend/docs/images/admin_home.png differ diff --git a/tdrs-backend/docs/images/admin_login.png b/tdrs-backend/docs/images/admin_login.png new file mode 100644 index 0000000000..1ee7e6e054 Binary files /dev/null and b/tdrs-backend/docs/images/admin_login.png differ diff --git a/tdrs-backend/docs/images/group_list.png b/tdrs-backend/docs/images/group_list.png new file mode 100644 index 0000000000..c30826eb58 Binary files /dev/null and b/tdrs-backend/docs/images/group_list.png differ diff --git a/tdrs-backend/docs/images/group_permissions.png b/tdrs-backend/docs/images/group_permissions.png new file mode 100644 index 0000000000..0671d0e69f Binary files /dev/null and b/tdrs-backend/docs/images/group_permissions.png differ diff --git a/tdrs-backend/docs/images/log_entries.png b/tdrs-backend/docs/images/log_entries.png new file mode 100644 index 0000000000..b805017153 Binary files /dev/null and b/tdrs-backend/docs/images/log_entries.png differ diff --git a/tdrs-backend/docs/images/make_superuser_example.png b/tdrs-backend/docs/images/make_superuser_example.png new file mode 100644 index 0000000000..306e422dd0 Binary files /dev/null and b/tdrs-backend/docs/images/make_superuser_example.png differ diff --git a/tdrs-backend/docs/images/region_list.png b/tdrs-backend/docs/images/region_list.png new file mode 100644 index 0000000000..11479612ed Binary files /dev/null and b/tdrs-backend/docs/images/region_list.png differ diff --git a/tdrs-backend/docs/images/stt_edit.png b/tdrs-backend/docs/images/stt_edit.png new file mode 100644 index 0000000000..094010477b Binary files /dev/null and b/tdrs-backend/docs/images/stt_edit.png differ diff --git a/tdrs-backend/docs/images/stt_list.png b/tdrs-backend/docs/images/stt_list.png new file mode 100644 index 0000000000..9c09640f3f Binary files /dev/null and b/tdrs-backend/docs/images/stt_list.png differ diff --git a/tdrs-backend/docs/images/user_edit.png b/tdrs-backend/docs/images/user_edit.png new file mode 100644 index 0000000000..39edb711d9 Binary files /dev/null and b/tdrs-backend/docs/images/user_edit.png differ diff --git a/tdrs-backend/docs/images/user_list.png b/tdrs-backend/docs/images/user_list.png new file mode 100644 index 0000000000..feb7d616bf Binary files /dev/null and b/tdrs-backend/docs/images/user_list.png differ diff --git a/tdrs-backend/gunicorn_start.sh b/tdrs-backend/gunicorn_start.sh index d305a9319e..26b192d7ab 100755 --- a/tdrs-backend/gunicorn_start.sh +++ b/tdrs-backend/gunicorn_start.sh @@ -4,5 +4,6 @@ echo "Applying database migrations" python manage.py makemigrations python manage.py migrate python manage.py populate_stts +python manage.py collectstatic --noinput echo "Starting Gunicorn" -exec gunicorn tdpservice.wsgi:application --bind 0.0.0.0:8080 --timeout 10 --workers 3 --log-file=- --log-level debug \ No newline at end of file +exec gunicorn tdpservice.wsgi:application --bind 0.0.0.0:8080 --timeout 10 --workers 3 --log-file=- --log-level debug diff --git a/tdrs-backend/mkdocs.yml b/tdrs-backend/mkdocs.yml index ba9cab557d..14693f6dd3 100644 --- a/tdrs-backend/mkdocs.yml +++ b/tdrs-backend/mkdocs.yml @@ -7,5 +7,5 @@ dev_addr: 0.0.0.0:8001 nav: - API: - - Authentication: 'api/authentication.md' - - Users: 'api/login.md' + - Authentication: 'test_api/authentication.md' + - Users: 'test_api/login.md' diff --git a/tdrs-backend/requirements.txt b/tdrs-backend/requirements.txt index fbbc7df1dd..23e358bb5e 100644 --- a/tdrs-backend/requirements.txt +++ b/tdrs-backend/requirements.txt @@ -1,50 +1,52 @@ --i https://pypi.org/simple/ +-i https://pypi.org/simple appdirs==1.4.4 -asgiref==3.2.10 +appnope==0.1.2; sys_platform == 'darwin' +asgiref==3.3.1; python_version >= '3.5' backcall==0.2.0 -boto3==1.15.0 -botocore==1.18.18 -certifi==2020.6.20 -cffi==1.14.3 +boto3==1.16.35 +botocore==1.19.35 +certifi==2020.12.5 +cffi==1.14.4 chardet==3.0.4 -cryptography==3.2 +cryptography==3.3.1 decorator==4.4.2 dj-database-url==0.5.0 +django-admin-logs==1.0.1 django-configurations==2.2 -django-cors-headers==3.5.0 -django-extensions==3.0.9 -django-filter==2.3.0 -django-model-utils==4.0.0 +django-cors-headers==3.6.0 +django-extensions==3.1.0 +django-filter==2.4.0 +django-model-utils==4.1.1 django-storages==1.10.1 django-unique-upload==0.2.1 -django==3.1.1 -djangorestframework==3.11.1 +django==3.1.4 +djangorestframework==3.12.2 gunicorn==20.0.4 -idna==2.10 -ipdb==0.13.3 +idna==2.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +ipdb==0.13.4 ipython-genutils==0.2.0 -ipython==7.18.1 -jedi==0.17.2 -jmespath==0.10.0 +ipython==7.19.0 +jedi==0.17.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +jmespath==0.10.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' jwcrypto==0.8 -markdown==3.2.2 -parso==0.7.1 -pexpect==4.8.0 ; sys_platform != 'win32' +markdown==3.3.3 +parso==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +pexpect==4.8.0; sys_platform != 'win32' pickleshare==0.7.5 -prompt-toolkit==3.0.8 +prompt-toolkit==3.0.8; python_full_version >= '3.6.1' psycopg2-binary==2.8.6 -ptpython==3.0.5 +ptpython==3.0.7 ptyprocess==0.6.0 -pycparser==2.20 -pygments==2.7.2 +pycparser==2.20; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +pygments==2.7.3; python_version >= '3.5' pyjwt==1.7.1 -python-dateutil==2.8.1 -python-dotenv==0.14.0 -pytz==2020.1 -requests==2.24.0 +python-dateutil==2.8.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +python-dotenv==0.15.0 +pytz==2020.4 +requests==2.25.0 s3transfer==0.3.3 -six==1.15.0 -sqlparse==0.4.1 -traitlets==5.0.5 -urllib3==1.25.11 ; python_version != '3.4' +six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +sqlparse==0.4.1; python_version >= '3.5' +traitlets==5.0.5; python_version >= '3.7' +urllib3==1.26.2; python_version != '3.4' wcwidth==0.2.5 diff --git a/tdrs-backend/setup.cfg b/tdrs-backend/setup.cfg index f163e2449a..71a818a8c4 100644 --- a/tdrs-backend/setup.cfg +++ b/tdrs-backend/setup.cfg @@ -7,6 +7,7 @@ omit = tdpservice/settings/staging.py tdpservice/wsgi.py *test* + *migrations* [coverage:report] ignore_errors = True @@ -18,6 +19,7 @@ omit = tdpservice/settings/staging.py tdpservice/wsgi.py *test* + *migrations* [flake8] docstring-convention=numpy diff --git a/tdrs-backend/tdpservice/core/__init__.py b/tdrs-backend/tdpservice/core/__init__.py new file mode 100644 index 0000000000..2e329f07c6 --- /dev/null +++ b/tdrs-backend/tdpservice/core/__init__.py @@ -0,0 +1,3 @@ +"""Core app. +Used for globally useful code. +""" diff --git a/tdrs-backend/tdpservice/core/apps.py b/tdrs-backend/tdpservice/core/apps.py new file mode 100644 index 0000000000..9ae46399ec --- /dev/null +++ b/tdrs-backend/tdpservice/core/apps.py @@ -0,0 +1,9 @@ +"""Core AppConfig.""" + +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + """Core AppConfig.""" + + name = "tdpservice.core" diff --git a/tdrs-backend/tdpservice/core/migrations/0001_initial.py b/tdrs-backend/tdpservice/core/migrations/0001_initial.py new file mode 100644 index 0000000000..f70d69ae7d --- /dev/null +++ b/tdrs-backend/tdpservice/core/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1.1 on 2020-10-09 12:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="GlobalPermission", + fields=[], + options={ + "verbose_name": "global_permission", + "proxy": True, + "indexes": [], + "constraints": [], + }, + bases=("auth.permission",), + ), + ] diff --git a/tdrs-backend/tdpservice/core/migrations/0002_auto_20201013_0630.py b/tdrs-backend/tdpservice/core/migrations/0002_auto_20201013_0630.py new file mode 100644 index 0000000000..408f8a83e3 --- /dev/null +++ b/tdrs-backend/tdpservice/core/migrations/0002_auto_20201013_0630.py @@ -0,0 +1,12 @@ +# Generated by Django 3.1.2 on 2020-10-13 06:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0001_initial"), + ] + + operations = [] diff --git a/tdrs-backend/tdpservice/core/migrations/__init__.py b/tdrs-backend/tdpservice/core/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tdrs-backend/tdpservice/core/models.py b/tdrs-backend/tdpservice/core/models.py new file mode 100644 index 0000000000..6e9c99e387 --- /dev/null +++ b/tdrs-backend/tdpservice/core/models.py @@ -0,0 +1,45 @@ +"""Core models.""" + +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.db import models + +"""Global permissions + +Allows for the creation of permissions that are +not related to a specific model. This allows us +broader flexibility is assigning permissions +where needed. + +NOTE: At this moment, the GlobalPermission and GlobalPermissionManager classes +are not directly in use, but are included as they make up part of the core +permission architecture addressed in this PR. +""" + + +class GlobalPermissionManager(models.Manager): + """Manager for global permissions.""" + + def get_queryset(self): + """Return global permissions.""" + return super().get_queryset().filter(content_type__model="global_permission") + + +class GlobalPermission(Permission): + """A global permission, not attached to a model.""" + + objects = GlobalPermissionManager() + + class Meta: + """Metadata.""" + + proxy = True + verbose_name = "global_permission" + + def save(self, *args, **kwargs): + """Save the permission using the global permission content type.""" + content_type, _ = ContentType.objects.get_or_create( + model=self._meta.verbose_name, app_label=self._meta.app_label, + ) + self.content_type = content_type + super().save(*args, **kwargs) diff --git a/tdrs-backend/tdpservice/core/test/test_models.py b/tdrs-backend/tdpservice/core/test/test_models.py new file mode 100644 index 0000000000..1c3d799816 --- /dev/null +++ b/tdrs-backend/tdpservice/core/test/test_models.py @@ -0,0 +1,14 @@ +"""Module for testing the core model.""" +import pytest + +from tdpservice.core.models import GlobalPermission + + +@pytest.mark.django_db +def test_manager_get_queryset(): + """Test the get queryset method returns a query.""" + GlobalPermission.objects.create( + name="Can View User Data", codename="view_user_data" + ) + global_permissions = GlobalPermission.objects.first() + assert global_permissions.name == "Can View User Data" diff --git a/tdrs-backend/tdpservice/settings/common.py b/tdrs-backend/tdpservice/settings/common.py index 0affac7243..70ed2f10bf 100755 --- a/tdrs-backend/tdpservice/settings/common.py +++ b/tdrs-backend/tdpservice/settings/common.py @@ -25,9 +25,11 @@ class Common(Configuration): "rest_framework", # Utilities for rest apis "rest_framework.authtoken", # Token authentication "django_filters", + 'django_admin_logs', # logs for admin site "corsheaders", "django_extensions", # Local apps + "tdpservice.core.apps.CoreConfig", "tdpservice.users", "tdpservice.stts", ) @@ -98,7 +100,7 @@ class Common(Configuration): # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.0/howto/static-files/ - STATIC_ROOT = os.path.normpath(join(os.path.dirname(BASE_DIR), "static")) + STATIC_ROOT = os.path.join(BASE_DIR, "static") STATICFILES_DIRS = [] STATIC_URL = "/static/" STATICFILES_FINDERS = ( @@ -225,6 +227,8 @@ class Common(Configuration): "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.TokenAuthentication", ), + "TEST_REQUEST_DEFAULT_FORMAT": "json", + "TEST_REQUEST_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"], } AUTHENTICATION_BACKENDS = ( diff --git a/tdrs-backend/tdpservice/stts/admin.py b/tdrs-backend/tdpservice/stts/admin.py new file mode 100644 index 0000000000..f1fb21bada --- /dev/null +++ b/tdrs-backend/tdpservice/stts/admin.py @@ -0,0 +1,8 @@ +"""Add STTs and Regions to Django Admin.""" + +from django.contrib import admin +from .models import STT, Region + + +admin.site.register(STT) +admin.site.register(Region) diff --git a/tdrs-backend/tdpservice/stts/test/test_models.py b/tdrs-backend/tdpservice/stts/test/test_models.py index c32233acab..dcb856a07e 100644 --- a/tdrs-backend/tdpservice/stts/test/test_models.py +++ b/tdrs-backend/tdpservice/stts/test/test_models.py @@ -1,4 +1,4 @@ -"""Module for testing the user model.""" +"""Module for testing the stt and region models.""" import pytest from ..models import Region, STT diff --git a/tdrs-backend/tdpservice/urls.py b/tdrs-backend/tdpservice/urls.py index 2629207d96..dcdb47dde7 100755 --- a/tdrs-backend/tdpservice/urls.py +++ b/tdrs-backend/tdpservice/urls.py @@ -12,18 +12,22 @@ from .users.api.logout_redirect_oidc import LogoutRedirectOIDC urlpatterns = [ - path("admin/", admin.site.urls), path("login", TokenAuthorizationOIDC.as_view(), name="login"), path("login/oidc", LoginRedirectOIDC.as_view(), name="oidc-auth"), path("logout", LogoutUser.as_view(), name="logout"), path("logout/oidc", LogoutRedirectOIDC.as_view(), name="oidc-logout"), path("auth_check", AuthorizationCheck.as_view(), name="authorization-check"), - path("users/", include("tdpservice.users.urls")), + path("", include("tdpservice.users.urls")), path("stts/", include("tdpservice.stts.urls")), - # the 'api-root' from django rest-frameworks default router + # the 'test_api-root' from django rest-frameworks default router # http://www.django-rest-framework.org/api-guide/routers/#defaultrouter - re_path(r"^$", RedirectView.as_view(url=reverse_lazy("api-root"), permanent=False)), -] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + re_path( + r"^$", RedirectView.as_view(url=reverse_lazy("test_api-root"), permanent=False) + ), +] # Add 'prefix' to all urlpatterns to make it easier to version/group endpoints -urlpatterns = [path("v1/", include(urlpatterns))] +urlpatterns = [ + path("v1/", include(urlpatterns)), + path("admin/", admin.site.urls), +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/tdrs-backend/tdpservice/users/admin.py b/tdrs-backend/tdpservice/users/admin.py new file mode 100644 index 0000000000..3a2fce82a5 --- /dev/null +++ b/tdrs-backend/tdpservice/users/admin.py @@ -0,0 +1,29 @@ +"""Add users to Django Admin.""" + +from django import forms +from django.contrib import admin +from .models import User +from rest_framework.authtoken.models import TokenProxy + + +class UserForm(forms.ModelForm): + """Customize the user admin form.""" + + class Meta: + """Define customizations.""" + + model = User + exclude = ['password', 'is_staff', 'is_superuser', 'user_permissions'] + readonly_fields = ['last_login', 'date_joined'] + + +class UserAdmin(admin.ModelAdmin): + """Customize the user admin functions.""" + + exclude = ['password', 'is_staff', 'is_superuser', 'user_permissions'] + readonly_fields = ['last_login', 'date_joined'] + form = UserForm + + +admin.site.register(User, UserAdmin) +admin.site.unregister(TokenProxy) diff --git a/tdrs-backend/tdpservice/users/api/authorization_check.py b/tdrs-backend/tdpservice/users/api/authorization_check.py index 116af51042..b061a40a57 100644 --- a/tdrs-backend/tdpservice/users/api/authorization_check.py +++ b/tdrs-backend/tdpservice/users/api/authorization_check.py @@ -33,7 +33,7 @@ def get(self, request, *args, **kwargs): "Auth check PASS for user: %s on %s", user.username, timezone.now() ) res = Response(auth_params) - res['Access-Control-Allow-Headers'] = "X-CSRFToken" + res["Access-Control-Allow-Headers"] = "X-CSRFToken" return res else: logger.info("Auth check FAIL for user on %s", timezone.now()) diff --git a/tdrs-backend/tdpservice/users/api/utils.py b/tdrs-backend/tdpservice/users/api/utils.py index 5d197f2694..d79aa2bdf9 100644 --- a/tdrs-backend/tdpservice/users/api/utils.py +++ b/tdrs-backend/tdpservice/users/api/utils.py @@ -1,4 +1,4 @@ -"""Define utility methods for users api.""" +"""Define utility methods for users test_api.""" import logging import os diff --git a/tdrs-backend/tdpservice/users/management/commands/__init__.py b/tdrs-backend/tdpservice/users/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tdrs-backend/tdpservice/users/management/commands/generate_test_users.py b/tdrs-backend/tdpservice/users/management/commands/generate_test_users.py new file mode 100644 index 0000000000..6d642940f2 --- /dev/null +++ b/tdrs-backend/tdpservice/users/management/commands/generate_test_users.py @@ -0,0 +1,72 @@ +"""generate_test_users command.""" + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from django.core.management import BaseCommand +from django.db import IntegrityError, transaction + +import factory + +User = get_user_model() + + +class Command(BaseCommand): + """Command class.""" + + help = "Generate a test user for each role." + + def handle(self, *args, **options): + """Generate a test user for each role.""" + first_name = factory.Faker("first_name") + last_name = factory.Faker("last_name") + password = "test_password" # Static password so we can login. + user_count = 0 + try: + user = User.objects.create_user( + username="test__unassigned", + email="test+unassigned@example.com", + password=password, + first_name=first_name, + last_name=last_name, + ) + superuser = User.objects.create_superuser( + username="test__superuser", + email="test+superuser@example.com", + password=password, + first_name=first_name, + last_name=last_name, + ) + except IntegrityError: # pragma: nocover + # User already exists. + pass + else: + user_count += 2 + self.stdout.write(f"Username: {user.username}") + self.stdout.write(f"Password: {password}") + self.stdout.write() + self.stdout.write(f"Username: {superuser.username}") + self.stdout.write(f"Password: {password}") + for group in Group.objects.all(): + username = f"test__{group.name.replace(' ', '_').lower()}" + email = f"test_{group.name.replace(' ', '_').lower()}" + "@example.com" + + try: + with transaction.atomic(): + user = User.objects.create_user( + username=username, + email=email, + password=password, + first_name=first_name, + last_name=last_name, + ) + user.groups.add(group) + except IntegrityError: # pragma: nocover + # User already exists. + pass + else: + user_count += 1 + self.stdout.write(f"Username: {user.username}") + self.stdout.write(f"Password: {password}") + self.stdout.write() + + self.stdout.write(f"Created {user_count} users.") diff --git a/tdrs-backend/tdpservice/users/migrations/0001_initial.py b/tdrs-backend/tdpservice/users/migrations/0001_initial.py index b5585df955..a8b99afac4 100644 --- a/tdrs-backend/tdpservice/users/migrations/0001_initial.py +++ b/tdrs-backend/tdpservice/users/migrations/0001_initial.py @@ -127,8 +127,6 @@ class Migration(migrations.Migration): "verbose_name_plural": "users", "abstract": False, }, - managers=[ - ("objects", django.contrib.auth.models.UserManager()), - ], + managers=[("objects", django.contrib.auth.models.UserManager()),], ), ] diff --git a/tdrs-backend/tdpservice/users/migrations/0003_tdrsadmin_tdrsedit_tdrsread.py b/tdrs-backend/tdpservice/users/migrations/0003_tdrsadmin_tdrsedit_tdrsread.py new file mode 100644 index 0000000000..c6800ab423 --- /dev/null +++ b/tdrs-backend/tdpservice/users/migrations/0003_tdrsadmin_tdrsedit_tdrsread.py @@ -0,0 +1,58 @@ +# Generated by Django 3.1.1 on 2020-10-09 12:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0002_auto_20200822_0939"), + ] + + operations = [ + migrations.CreateModel( + name="TDRSAdmin", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + options={"permissions": [("tdrs_can_admin", "Can Admin Applicable STT")],}, + ), + migrations.CreateModel( + name="TDRSEdit", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + options={"permissions": [("tdrs_can_edit_data", "Can Prepare STT Data")],}, + ), + migrations.CreateModel( + name="TDRSRead", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + options={"permissions": [("tdrs_can_read_data", "Can Read STT Data")],}, + ), + ] diff --git a/tdrs-backend/tdpservice/users/migrations/0004_merge_20201022_1417.py b/tdrs-backend/tdpservice/users/migrations/0004_merge_20201022_1417.py new file mode 100644 index 0000000000..737e09968f --- /dev/null +++ b/tdrs-backend/tdpservice/users/migrations/0004_merge_20201022_1417.py @@ -0,0 +1,13 @@ +# Generated by Django 3.1.2 on 2020-10-22 14:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0003_tdrsadmin_tdrsedit_tdrsread"), + ("users", "0003_auto_20200921_1753"), + ] + + operations = [] diff --git a/tdrs-backend/tdpservice/users/migrations/0005_auto_20201022_1423.py b/tdrs-backend/tdpservice/users/migrations/0005_auto_20201022_1423.py new file mode 100644 index 0000000000..3d3a9c903f --- /dev/null +++ b/tdrs-backend/tdpservice/users/migrations/0005_auto_20201022_1423.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.2 on 2020-10-22 14:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ("users", "0004_merge_20201022_1417"), + ] + + operations = [ + migrations.DeleteModel(name="TDRSAdmin",), + migrations.DeleteModel(name="TDRSEdit",), + migrations.DeleteModel(name="TDRSRead",), + migrations.AddField( + model_name="user", + name="requested_roles", + field=models.ManyToManyField( + related_name="users_requested", to="auth.Group" + ), + ), + ] diff --git a/tdrs-backend/tdpservice/users/migrations/0006_auto_20201117_1717.py b/tdrs-backend/tdpservice/users/migrations/0006_auto_20201117_1717.py new file mode 100644 index 0000000000..7fea1aa03c --- /dev/null +++ b/tdrs-backend/tdpservice/users/migrations/0006_auto_20201117_1717.py @@ -0,0 +1,26 @@ +from django.contrib.auth.models import Group +from django.db import migrations + + +def add_groups(apps, schema_editor): + groups = [ + Group(name="OFA Admin"), + Group(name="Data Prepper"), + Group(name="OFA Analyst"), + ] + Group.objects.bulk_create(groups) + + +def remove_groups(apps, schema_editor): + Group.objects.filter(name__in={"OFA Admin", "Data Prepper", "OFA Analyst"}).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0005_auto_20201022_1423"), + ] + + operations = [ + migrations.RunPython(add_groups, remove_groups), + ] diff --git a/tdrs-backend/tdpservice/users/migrations/0007_auto_20201223_1404.py b/tdrs-backend/tdpservice/users/migrations/0007_auto_20201223_1404.py new file mode 100644 index 0000000000..4609e842e6 --- /dev/null +++ b/tdrs-backend/tdpservice/users/migrations/0007_auto_20201223_1404.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.4 on 2020-12-23 14:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0006_auto_20201117_1717'), + ] + + operations = [ + migrations.RenameField('User', 'requested_roles', 'roles') + ] diff --git a/tdrs-backend/tdpservice/users/migrations/0008_remove_user_roles.py b/tdrs-backend/tdpservice/users/migrations/0008_remove_user_roles.py new file mode 100644 index 0000000000..22f52b2d7f --- /dev/null +++ b/tdrs-backend/tdpservice/users/migrations/0008_remove_user_roles.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.4 on 2020-12-31 17:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0007_auto_20201223_1404'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + ] diff --git a/tdrs-backend/tdpservice/users/migrations/0009_user_roles.py b/tdrs-backend/tdpservice/users/migrations/0009_user_roles.py new file mode 100644 index 0000000000..f27a99baa6 --- /dev/null +++ b/tdrs-backend/tdpservice/users/migrations/0009_user_roles.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.4 on 2020-12-31 17:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('users', '0008_remove_user_roles'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='roles', + field=models.ManyToManyField(related_name='users_requested', to='auth.Group'), + ), + ] diff --git a/tdrs-backend/tdpservice/users/migrations/0010_remove_user_roles.py b/tdrs-backend/tdpservice/users/migrations/0010_remove_user_roles.py new file mode 100644 index 0000000000..bf05f5079a --- /dev/null +++ b/tdrs-backend/tdpservice/users/migrations/0010_remove_user_roles.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.4 on 2020-12-31 17:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0009_user_roles'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + ] diff --git a/tdrs-backend/tdpservice/users/migrations/0011_auto_20210108_1741.py b/tdrs-backend/tdpservice/users/migrations/0011_auto_20210108_1741.py new file mode 100644 index 0000000000..e43b2414ce --- /dev/null +++ b/tdrs-backend/tdpservice/users/migrations/0011_auto_20210108_1741.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.4 on 2021-01-08 17:41 + +from django.contrib.auth.models import Group +from django.db import migrations + + +def remove_groups(apps, schema_editor): + Group.objects.filter(name__in={"OFA Analyst"}).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0010_remove_user_roles'), + ] + + operations = [ + migrations.RunPython(remove_groups), + ] diff --git a/tdrs-backend/tdpservice/users/models.py b/tdrs-backend/tdpservice/users/models.py index b5328b0644..f4e73d7588 100644 --- a/tdrs-backend/tdpservice/users/models.py +++ b/tdrs-backend/tdpservice/users/models.py @@ -2,7 +2,7 @@ import uuid -from django.contrib.auth.models import AbstractUser +from django.contrib.auth.models import AbstractUser, Group from django.db import models from tdpservice.stts.models import STT, Region @@ -17,3 +17,11 @@ class User(AbstractUser): def __str__(self): """Return the username as the string representation of the object.""" return self.username + + @property + def is_admin(self): + """Check if the user is an admin.""" + return ( + self.is_superuser + or Group.objects.get(name="OFA Admin") in self.groups.all() + ) diff --git a/tdrs-backend/tdpservice/users/permissions.py b/tdrs-backend/tdpservice/users/permissions.py index 0587ec2f0a..fc1b5c8d9b 100644 --- a/tdrs-backend/tdpservice/users/permissions.py +++ b/tdrs-backend/tdpservice/users/permissions.py @@ -3,18 +3,21 @@ from rest_framework import permissions -class IsUserOrReadOnly(permissions.BasePermission): +class IsUser(permissions.BasePermission): """Object-level permission to only allow owners of an object to edit it.""" - def has_permission(self, request, view): + def has_object_permission(self, request, view, obj): """Check if user has required permissions.""" - if request.method in permissions.SAFE_METHODS: - return True - return request.user.is_authenticated + return obj == request.user + + +class IsAdmin(permissions.BasePermission): + """Permission for admin-only views.""" def has_object_permission(self, request, view, obj): - """Check if user has required permissions.""" - if request.method in permissions.SAFE_METHODS: - return True + """Check if a user is admin or superuser.""" + return request.user.is_authenticated and request.user.is_admin - return obj == request.user + def has_permission(self, request, view): + """Check if a user is admin or superuser.""" + return request.user.is_authenticated and request.user.is_admin diff --git a/tdrs-backend/tdpservice/users/serializers.py b/tdrs-backend/tdpservice/users/serializers.py index 3168e1a02d..c100a99ed6 100644 --- a/tdrs-backend/tdpservice/users/serializers.py +++ b/tdrs-backend/tdpservice/users/serializers.py @@ -1,8 +1,10 @@ """Serialize user data.""" import logging +from django.contrib.auth.models import Group, Permission from rest_framework import serializers + from .models import User from tdpservice.stts.serializers import STTUpdateSerializer @@ -11,6 +13,35 @@ logger.addHandler(logging.StreamHandler()) +class PermissionSerializer(serializers.ModelSerializer): + """Permission serializer.""" + + content_type = serializers.CharField( + required=False, allow_null=True, write_only=True + ) + + class Meta: + """Metadata.""" + + model = Permission + fields = ["id", "codename", "name", "content_type"] + extra_kwargs = { + "content_type": {"allow_null": True}, + } + + +class GroupSerializer(serializers.ModelSerializer): + """Group (role) serializer.""" + + permissions = PermissionSerializer(many=True) + + class Meta: + """Metadata.""" + + model = Group + fields = ["id", "name", "permissions"] + + class UserSerializer(serializers.ModelSerializer): """Define meta user serializer class.""" @@ -59,12 +90,15 @@ class UserProfileSerializer(serializers.ModelSerializer): stt = STTUpdateSerializer(required=True) email = serializers.SerializerMethodField("get_email") + roles = GroupSerializer( + many=True, required=False, allow_empty=True, source="groups" + ) class Meta: """Metadata.""" model = User - fields = ["first_name", "last_name", "stt", "email"] + fields = ["first_name", "last_name", "email", "stt", "roles"] """Enforce first and last name to be in API call and not empty""" extra_kwargs = { diff --git a/tdrs-backend/tdpservice/users/test/test_api/__init__.py b/tdrs-backend/tdpservice/users/test/test_api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tdrs-backend/tdpservice/users/test/test_api/test_basic.py b/tdrs-backend/tdpservice/users/test/test_api/test_basic.py new file mode 100644 index 0000000000..cebe60ebf2 --- /dev/null +++ b/tdrs-backend/tdpservice/users/test/test_api/test_basic.py @@ -0,0 +1,47 @@ +"""Basic API User Tests.""" +from django.contrib.auth import get_user_model +from django.core.management import call_command +import pytest +from rest_framework import status + +User = get_user_model() + +@pytest.mark.django_db +@pytest.fixture(scope="function") +def create_test_users(): + """Create users for each group.""" + call_command("generate_test_users") + + +@pytest.mark.django_db +def test_retrieve_user(api_client, user): + """Test user retrieval.""" + api_client.login(username=user.username, password="test_password") + response = api_client.get(f"/v1/users/{user.pk}/") + assert response.status_code == status.HTTP_200_OK + assert response.data["username"] == user.username + + +@pytest.mark.django_db +def test_can_update_own_user(api_client, user): + """Test a user can update their own user.""" + api_client.login(username=user.username, password="test_password") + response = api_client.patch(f"/v1/users/{user.pk}/", {"first_name": "Jane"}) + assert response.status_code == status.HTTP_200_OK + assert response.data["first_name"] == "Jane" + assert User.objects.filter(first_name="Jane").exists() + + +@pytest.mark.django_db +def test_cannot_update_user_anonymously(api_client, user): + """Test an unauthenticated user cannot update a user.""" + response = api_client.patch(f"/v1/users/{user.pk}/", {"first_name": "Jane"}) + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_create_user(api_client, user_data): + """Test user creation.""" + response = api_client.post("/v1/users/", user_data) + assert response.status_code == status.HTTP_201_CREATED + assert User.objects.filter(username=user_data["username"]).exists() diff --git a/tdrs-backend/tdpservice/users/test/test_api/test_permissions.py b/tdrs-backend/tdpservice/users/test/test_api/test_permissions.py new file mode 100644 index 0000000000..000c2e2fbb --- /dev/null +++ b/tdrs-backend/tdpservice/users/test/test_api/test_permissions.py @@ -0,0 +1,112 @@ +"""API User Permission Tests.""" +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission +from django.core.management import call_command +import pytest +from rest_framework import status + +User = get_user_model() + + +@pytest.mark.django_db +@pytest.fixture(scope="function") +def create_test_users(): + """Create users for each group.""" + call_command("generate_test_users") + + +@pytest.mark.django_db +def test_permission_list_not_found(api_client, create_test_users): + """Test permission list no longer exists.""" + # Django comes with some permissions, so we'll just test those. + api_client.login(username="test__ofa_admin", password="test_password") + response = api_client.get("/v1/permissions/") + assert response.status_code == status.HTTP_404_NOT_FOUND + + +# @pytest.mark.django_db +# def test_permission_list_unauthorized(api_client, create_test_users): +# """Test data prepper does not have access.""" +# # Django comes with some permissions, so we'll just test those. +# api_client.login(username="test__data_prepper", password="test_password") +# response = api_client.get("/v1/permissions/") +# assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_permission_create_not_found(api_client, create_test_users): + """Test permission creation no longer exists.""" + api_client.login(username="test__ofa_admin", password="test_password") + response = api_client.post( + "/v1/permissions/", {"codename": "foo", "name": "Foo", "content_type": None} + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + +# @pytest.mark.django_db +# def test_permission_create_unauthorized(api_client, create_test_users): +# """Test data prepper does not have access.""" +# api_client.login(username="test__data_prepper", password="test_password") +# response = api_client.post( +# "/v1/permissions/", {"codename": "foo", "name": "Foo", "content_type": None} +# ) +# assert response.status_code == status.HTTP_403_FORBIDDEN + + +# @pytest.mark.django_db +# def test_permission_create_with_content_type(api_client, create_test_users): +# """Test can create a permission with a content type.""" +# content_type = ContentType.objects.get_for_model(User) +# api_client.login(username="test__ofa_admin", password="test_password") +# response = api_client.post( +# "/v1/permissions/", +# { +# "codename": "foo", +# "name": "Foo", +# "content_type": f"{content_type.app_label}.{content_type.model}", +# }, +# ) +# assert response.status_code == status.HTTP_201_CREATED +# assert response.data["codename"] == "foo" +# assert response.data["name"] == "Foo" +# assert Permission.objects.filter(codename="foo").exists() + + +@pytest.mark.django_db +def test_permission_update_not_found(api_client, create_test_users): + """Test permission update.""" + permission = Permission.objects.first() + api_client.login(username="test__ofa_admin", password="test_password") + response = api_client.patch( + f"/v1/permissions/{permission.id}/", {"codename": "foo"} + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + +# @pytest.mark.django_db +# def test_permission_update_unauthorized(api_client, create_test_users): +# """Test data prepper does not have access.""" +# permission = Permission.objects.first() +# api_client.login(username="test__data_prepper", password="test_password") +# response = api_client.patch( +# f"/v1/permissions/{permission.id}/", {"codename": "foo"} +# ) +# assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_permission_delete_not_found(api_client, create_test_users): + """Test permission deletion no longer exists.""" + permission = Permission.objects.first() + api_client.login(username="test__ofa_admin", password="test_password") + response = api_client.delete(f"/v1/permissions/{permission.id}/") + assert response.status_code == status.HTTP_404_NOT_FOUND + + +# @pytest.mark.django_db +# def test_permission_delete_unauthorized(api_client, create_test_users): +# """Test data prepper does not have access.""" +# permission = Permission.objects.first() +# api_client.login(username="test__data_prepper", password="test_password") +# response = api_client.delete(f"/v1/permissions/{permission.id}/") +# assert response.status_code == status.HTTP_403_FORBIDDEN diff --git a/tdrs-backend/tdpservice/users/test/test_api/test_roles.py b/tdrs-backend/tdpservice/users/test/test_api/test_roles.py new file mode 100644 index 0000000000..fbd6e3632d --- /dev/null +++ b/tdrs-backend/tdpservice/users/test/test_api/test_roles.py @@ -0,0 +1,98 @@ +"""API User Role Tests.""" +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group, Permission +from django.core.management import call_command +import pytest +from rest_framework import status + +User = get_user_model() + + +@pytest.mark.django_db +@pytest.fixture(scope="function") +def create_test_users(): + """Create users for each group.""" + call_command("generate_test_users") + + +@pytest.mark.django_db +def test_role_list(api_client, create_test_users): + """Test role list.""" + # Groups are populated in a data migrations, so are already available. + api_client.login(username="test__ofa_admin", password="test_password") + response = api_client.get("/v1/roles/") + assert response.status_code == status.HTTP_200_OK + role_names = {group["name"] for group in response.data} + assert role_names == {"OFA Admin", "Data Prepper"} + + +@pytest.mark.django_db +def test_role_list_unauthorized(api_client, create_test_users): + """Data prepper does not have access.""" + # Groups are populated in a data migrations, so are already available. + api_client.login(username="test__data_prepper", password="test_password") + response = api_client.get("/v1/roles/") + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_role_create_forbidden(api_client, create_test_users): + """Test creating a role is no longer allowed.""" + api_client.login(username="test__ofa_admin", password="test_password") + response = api_client.post("/v1/roles/", {"name": "Test Role"}) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + + +@pytest.mark.django_db +def test_role_create_unauthorized(api_client, create_test_users): + """Test data prepper does not have access.""" + api_client.login(username="test__data_prepper", password="test_password") + response = api_client.post("/v1/roles/", {"name": "Test Role"}) + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_role_create_with_permission_forbidden(api_client, create_test_users): + """Test creating a role with a permission is no longer allowed.""" + permission = Permission.objects.first() + api_client.login(username="test__ofa_admin", password="test_password") + response = api_client.post( + "/v1/roles/", {"name": "Test Role", "permissions": [permission.id]} + ) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + + +@pytest.mark.django_db +def test_role_update_not_found(api_client, create_test_users): + """Test role update no longer exists.""" + group = Group.objects.get(name="OFA Admin") + api_client.login(username="test__ofa_admin", password="test_password") + response = api_client.patch(f"/v1/roles/{group.id}/", {"name": "staff"}) + assert response.status_code == status.HTTP_404_NOT_FOUND + + +# @pytest.mark.django_db +# def test_role_update_unauthorized(api_client, create_test_users): +# """Test data prepper does not have access.""" +# group = Group.objects.get(name="Data Prepper") +# api_client.login(username="test__data_prepper", password="test_password") +# response = api_client.patch(f"/v1/roles/{group.id}/", {"name": "staff"}) +# assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_role_delete_not_found(api_client, create_test_users): + """Test role deletion no longer exists.""" + group = Group.objects.get(name="OFA Admin") + api_client.login(username="test__ofa_admin", password="test_password") + response = api_client.delete(f"/v1/roles/{group.id}/") + assert response.status_code == status.HTTP_404_NOT_FOUND + + +# @pytest.mark.django_db +# def test_role_delete_unauthorized(api_client, create_test_users): +# """Test data prepper does not have access.""" +# group = Group.objects.get(name="Data Prepper") +# api_client.login(username="test__data_prepper", password="test_password") +# response = api_client.delete(f"/v1/roles/{group.id}/") +# assert response.status_code == status.HTTP_403_FORBIDDEN diff --git a/tdrs-backend/tdpservice/users/test/test_api.py b/tdrs-backend/tdpservice/users/test/test_api/test_set_profile.py similarity index 70% rename from tdrs-backend/tdpservice/users/test/test_api.py rename to tdrs-backend/tdpservice/users/test/test_api/test_set_profile.py index e0fbb1657b..be096b5f44 100644 --- a/tdrs-backend/tdpservice/users/test/test_api.py +++ b/tdrs-backend/tdpservice/users/test/test_api/test_set_profile.py @@ -1,43 +1,18 @@ -"""API User Tests.""" +"""API User Set Profile Tests.""" from django.contrib.auth import get_user_model +from django.core.management import call_command import pytest from rest_framework import status -from ...stts.models import STT +from tdpservice.stts.models import STT User = get_user_model() @pytest.mark.django_db -def test_retrieve_user(api_client, user): - """Test user retrieval.""" - response = api_client.get(f"/v1/users/{user.pk}/") - assert response.status_code == status.HTTP_200_OK - assert response.data["username"] == user.username - - -@pytest.mark.django_db -def test_can_update_own_user(api_client, user): - """Test a user can update their own user.""" - api_client.login(username=user.username, password="test_password") - response = api_client.patch(f"/v1/users/{user.pk}/", {"first_name": "Jane"}) - assert response.status_code == status.HTTP_200_OK - assert response.data["first_name"] == "Jane" - assert User.objects.filter(first_name="Jane").exists() - - -@pytest.mark.django_db -def test_cannot_update_user_anonymously(api_client, user): - """Test an unauthenticated user cannot update a user.""" - response = api_client.patch(f"/v1/users/{user.pk}/", {"first_name": "Jane"}) - assert response.status_code == status.HTTP_403_FORBIDDEN - - -@pytest.mark.django_db -def test_create_user(api_client, user_data): - """Test user creation.""" - response = api_client.post("/v1/users/", user_data) - assert response.status_code == status.HTTP_201_CREATED - assert User.objects.filter(username=user_data["username"]).exists() +@pytest.fixture(scope="function") +def create_test_users(): + """Create users for each group.""" + call_command("generate_test_users") @pytest.mark.django_db @@ -47,7 +22,7 @@ def test_set_profile_data(api_client, user): stt = STT.objects.first() response = api_client.patch( "/v1/users/set_profile/", - {"first_name": "Joe", "last_name": "Bloggs", "stt": {"id": stt.id}}, + {"first_name": "Joe", "last_name": "Bloggs", "stt": {"id": stt.id}, }, format="json", ) assert response.status_code == status.HTTP_200_OK @@ -55,12 +30,8 @@ def test_set_profile_data(api_client, user): "email": user.username, "first_name": "Joe", "last_name": "Bloggs", - "stt": { - "id": stt.id, - "type": stt.type, - "code": stt.code, - "name": stt.name, - }, + "stt": {"id": stt.id, "type": stt.type, "code": stt.code, "name": stt.name, }, + "roles": [], } user.refresh_from_db() assert user.first_name == "Joe" @@ -75,7 +46,7 @@ def test_set_profile_data_last_name_apostrophe(api_client, user): stt = STT.objects.first() response = api_client.patch( "/v1/users/set_profile/", - {"first_name": "Mike", "last_name": "O'Hare", "stt": {"id": stt.id}}, + {"first_name": "Mike", "last_name": "O'Hare", "stt": {"id": stt.id}, }, format="json", ) assert response.status_code == status.HTTP_200_OK @@ -83,12 +54,8 @@ def test_set_profile_data_last_name_apostrophe(api_client, user): "email": user.username, "first_name": "Mike", "last_name": "O'Hare", - "stt": { - "id": stt.id, - "type": stt.type, - "code": stt.code, - "name": stt.name, - }, + "stt": {"id": stt.id, "type": stt.type, "code": stt.code, "name": stt.name, }, + "roles": [], } user.refresh_from_db() assert user.first_name == "Mike" @@ -103,11 +70,7 @@ def test_set_profile_data_first_name_apostrophe(api_client, user): stt = STT.objects.first() response = api_client.patch( "/v1/users/set_profile/", - { - "first_name": "Pat'Jack", - "last_name": "Smith", - "stt": {"id": stt.id}, - }, + {"first_name": "Pat'Jack", "last_name": "Smith", "stt": {"id": stt.id}, }, format="json", ) assert response.status_code == status.HTTP_200_OK @@ -115,12 +78,8 @@ def test_set_profile_data_first_name_apostrophe(api_client, user): "email": user.username, "first_name": "Pat'Jack", "last_name": "Smith", - "stt": { - "id": stt.id, - "type": stt.type, - "code": stt.code, - "name": stt.name, - }, + "stt": {"id": stt.id, "type": stt.type, "code": stt.code, "name": stt.name, }, + "roles": [], } user.refresh_from_db() assert user.first_name == "Pat'Jack" @@ -133,8 +92,7 @@ def test_set_profile_data_empty_first_name(api_client, user): """Test profile data cannot be be set if first name is blank.""" api_client.login(username=user.username, password="test_password") response = api_client.patch( - "/v1/users/set_profile/", - {"first_name": "", "last_name": "Jones"}, + "/v1/users/set_profile/", {"first_name": "", "last_name": "Jones"}, ) assert response.status_code == status.HTTP_400_BAD_REQUEST @@ -144,8 +102,7 @@ def test_set_profile_data_empty_last_name(api_client, user): """Test profile data cannot be set last name is blank.""" api_client.login(username=user.username, password="test_password") response = api_client.patch( - "/v1/users/set_profile/", - {"first_name": "John", "last_name": ""}, + "/v1/users/set_profile/", {"first_name": "John", "last_name": ""}, ) assert response.status_code == status.HTTP_400_BAD_REQUEST @@ -155,8 +112,7 @@ def test_set_profile_data_empty_first_name_and_last_name(api_client, user): """Test profile data cannot be set if first and last name are blank.""" api_client.login(username=user.username, password="test_password") response = api_client.patch( - "/v1/users/set_profile/", - {"first_name": "", "last_name": ""}, + "/v1/users/set_profile/", {"first_name": "", "last_name": ""}, ) assert response.status_code == status.HTTP_400_BAD_REQUEST @@ -168,11 +124,7 @@ def test_set_profile_data_special_last_name(api_client, user): stt = STT.objects.first() response = api_client.patch( "/v1/users/set_profile/", - { - "first_name": "John", - "last_name": "Smith-O'Hare", - "stt": {"id": stt.id}, - }, + {"first_name": "John", "last_name": "Smith-O'Hare", "stt": {"id": stt.id}, }, format="json", ) assert response.status_code == status.HTTP_200_OK @@ -180,12 +132,8 @@ def test_set_profile_data_special_last_name(api_client, user): "email": user.username, "first_name": "John", "last_name": "Smith-O'Hare", - "stt": { - "id": stt.id, - "type": stt.type, - "code": stt.code, - "name": stt.name, - }, + "stt": {"id": stt.id, "type": stt.type, "code": stt.code, "name": stt.name, }, + "roles": [] } user.refresh_from_db() assert user.first_name == "John" @@ -200,11 +148,7 @@ def test_set_profile_data_special_first_name(api_client, user): stt = STT.objects.first() response = api_client.patch( "/v1/users/set_profile/", - { - "first_name": "John-Tom'", - "last_name": "Jacobs", - "stt": {"id": stt.id}, - }, + {"first_name": "John-Tom'", "last_name": "Jacobs", "stt": {"id": stt.id}, }, format="json", ) assert response.status_code == status.HTTP_200_OK @@ -212,12 +156,8 @@ def test_set_profile_data_special_first_name(api_client, user): "email": user.username, "first_name": "John-Tom'", "last_name": "Jacobs", - "stt": { - "id": stt.id, - "type": stt.type, - "code": stt.code, - "name": stt.name, - }, + "stt": {"id": stt.id, "type": stt.type, "code": stt.code, "name": stt.name, }, + "roles": [] } user.refresh_from_db() assert user.first_name == "John-Tom'" @@ -232,11 +172,7 @@ def test_set_profile_data_spaced_last_name(api_client, user): api_client.login(username=user.username, password="test_password") response = api_client.patch( "/v1/users/set_profile/", - { - "first_name": "Joan", - "last_name": "Mary Ann", - "stt": {"id": stt.id}, - }, + {"first_name": "Joan", "last_name": "Mary Ann", "stt": {"id": stt.id}, }, format="json", ) assert response.status_code == status.HTTP_200_OK @@ -244,12 +180,8 @@ def test_set_profile_data_spaced_last_name(api_client, user): "email": user.username, "first_name": "Joan", "last_name": "Mary Ann", - "stt": { - "id": stt.id, - "type": stt.type, - "code": stt.code, - "name": stt.name, - }, + "stt": {"id": stt.id, "type": stt.type, "code": stt.code, "name": stt.name, }, + "roles": [] } user.refresh_from_db() assert user.first_name == "Joan" @@ -264,11 +196,7 @@ def test_set_profile_data_spaced_first_name(api_client, user): stt = STT.objects.first() response = api_client.patch( "/v1/users/set_profile/", - { - "first_name": "John Jim", - "last_name": "Smith", - "stt": {"id": stt.id}, - }, + {"first_name": "John Jim", "last_name": "Smith", "stt": {"id": stt.id}}, format="json", ) assert response.status_code == status.HTTP_200_OK @@ -276,12 +204,8 @@ def test_set_profile_data_spaced_first_name(api_client, user): "email": user.username, "first_name": "John Jim", "last_name": "Smith", - "stt": { - "id": stt.id, - "type": stt.type, - "code": stt.code, - "name": stt.name, - }, + "stt": {"id": stt.id, "type": stt.type, "code": stt.code, "name": stt.name}, + "roles": [] } user.refresh_from_db() assert user.first_name == "John Jim" @@ -296,11 +220,7 @@ def test_set_profile_data_last_name_with_tilde_over_char(api_client, user): stt = STT.objects.first() response = api_client.patch( "/v1/users/set_profile/", - { - "first_name": "Max", - "last_name": "Grecheñ", - "stt": {"id": stt.id}, - }, + {"first_name": "Max", "last_name": "Grecheñ", "stt": {"id": stt.id}, }, format="json", ) assert response.status_code == status.HTTP_200_OK @@ -308,12 +228,8 @@ def test_set_profile_data_last_name_with_tilde_over_char(api_client, user): "email": user.username, "first_name": "Max", "last_name": "Grecheñ", - "stt": { - "id": stt.id, - "type": stt.type, - "code": stt.code, - "name": stt.name, - }, + "stt": {"id": stt.id, "type": stt.type, "code": stt.code, "name": stt.name, }, + "roles": [] } user.refresh_from_db() assert user.first_name == "Max" @@ -328,11 +244,7 @@ def test_set_profile_data_last_name_with_tilde(api_client, user): stt = STT.objects.first() response = api_client.patch( "/v1/users/set_profile/", - { - "first_name": "Max", - "last_name": "Glen~", - "stt": {"id": stt.id}, - }, + {"first_name": "Max", "last_name": "Glen~", "stt": {"id": stt.id}, }, format="json", ) assert response.status_code == status.HTTP_200_OK @@ -340,12 +252,8 @@ def test_set_profile_data_last_name_with_tilde(api_client, user): "email": user.username, "first_name": "Max", "last_name": "Glen~", - "stt": { - "id": stt.id, - "type": stt.type, - "code": stt.code, - "name": stt.name, - }, + "stt": {"id": stt.id, "type": stt.type, "code": stt.code, "name": stt.name, }, + "roles": [] } user.refresh_from_db() assert user.first_name == "Max" @@ -382,6 +290,7 @@ def test_set_profile_data_extra_field_include_required(api_client, user): "code": stt.code, "name": stt.name, }, + "roles": [] } user.refresh_from_db() assert user.first_name == "Heather" @@ -395,12 +304,7 @@ def test_set_profile_data_extra_field_include_required(api_client, user): def test_set_profile_data_missing_last_name_field(api_client, user): """Test profile data cannot be set if last name field is missing.""" api_client.login(username=user.username, password="test_password") - response = api_client.patch( - "/v1/users/set_profile/", - { - "first_name": "Heather", - }, - ) + response = api_client.patch("/v1/users/set_profile/", {"first_name": "Heather", },) assert response.status_code == status.HTTP_400_BAD_REQUEST @@ -408,10 +312,5 @@ def test_set_profile_data_missing_last_name_field(api_client, user): def test_set_profile_data_missing_first_name_field(api_client, user): """Test profile data cannot be set if first name field is missing.""" api_client.login(username=user.username, password="test_password") - response = api_client.patch( - "/v1/users/set_profile/", - { - "last_name": "Heather", - }, - ) + response = api_client.patch("/v1/users/set_profile/", {"last_name": "Heather", },) assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/tdrs-backend/tdpservice/users/test/test_auth_check.py b/tdrs-backend/tdpservice/users/test/test_auth_check.py index cdc4fef182..45e71c2aae 100644 --- a/tdrs-backend/tdpservice/users/test/test_auth_check.py +++ b/tdrs-backend/tdpservice/users/test/test_auth_check.py @@ -22,6 +22,11 @@ def test_auth_check_endpoint_with_authenticated_user(api_client, user): assert response.status_code == status.HTTP_200_OK assert user.is_authenticated is True assert response.data["authenticated"] is True + assert response.data["user"]["first_name"] == user.first_name + assert response.data["user"]["last_name"] == user.last_name + assert response.data["user"]["email"] == user.username + assert response.data["user"]["stt"]["id"] == user.stt.id + assert response.data["user"]["roles"] == [] @pytest.mark.django_db diff --git a/tdrs-backend/tdpservice/users/urls.py b/tdrs-backend/tdpservice/users/urls.py index 61273758ea..9e7061cc76 100644 --- a/tdrs-backend/tdpservice/users/urls.py +++ b/tdrs-backend/tdpservice/users/urls.py @@ -1,11 +1,13 @@ """Routing for Users.""" from rest_framework.routers import DefaultRouter + from . import views router = DefaultRouter() -router.register("", views.UserViewSet) +router.register("users", views.UserViewSet) +router.register("roles", views.GroupViewSet) urlpatterns = [] diff --git a/tdrs-backend/tdpservice/users/views.py b/tdrs-backend/tdpservice/users/views.py index faaf523182..be17eebc57 100644 --- a/tdrs-backend/tdpservice/users/views.py +++ b/tdrs-backend/tdpservice/users/views.py @@ -1,18 +1,19 @@ """Define API views for user class.""" import logging - +from django.contrib.auth.models import Group from rest_framework import mixins, viewsets from rest_framework.decorators import action from rest_framework.permissions import AllowAny from rest_framework.response import Response from .models import User -from .permissions import IsUserOrReadOnly +from .permissions import IsAdmin, IsUser from django.utils import timezone from .serializers import ( CreateUserSerializer, UserProfileSerializer, UserSerializer, + GroupSerializer ) logger = logging.getLogger(__name__) @@ -26,14 +27,20 @@ class UserViewSet( ): """User accounts viewset.""" - queryset = User.objects.select_related("stt") + queryset = User.objects\ + .select_related("stt")\ + .prefetch_related("groups__permissions") def get_permissions(self): """Get permissions for the viewset.""" - if self.action == "create": - permission_classes = [AllowAny] - else: - permission_classes = [IsUserOrReadOnly] + permission_classes = { + "create": [AllowAny], + "retrieve": [IsUser | IsAdmin], + "set_profile": [IsUser | IsAdmin], + "partial_update": [IsUser | IsAdmin], + "update": [IsUser | IsAdmin], + "list": [IsAdmin] + }.get(self.action, [IsAdmin]) return [permission() for permission in permission_classes] def get_serializer_class(self): @@ -53,3 +60,12 @@ def set_profile(self, request, pk=None): "Profile update for user: %s on %s", self.request.user, timezone.now() ) return Response(serializer.data) + + +class GroupViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): + """GET for groups (roles).""" + + pagination_class = None + queryset = Group.objects.all() + permission_classes = [IsAdmin] + serializer_class = GroupSerializer