Compare commits

..

9 Commits

Author SHA1 Message Date
ImgBotApp
d5dc879733 [ImgBot] Optimize images
*Total -- 8,936.02kb -> 7,990.11kb (10.59%)

/assets/static/imgs/square_logo.png -- 23.90kb -> 17.64kb (26.18%)
/RIGS/static/imgs/tappytaptap.gif -- 6,433.15kb -> 5,493.51kb (14.61%)
/RIGS/static/imgs/rigs.jpg -- 277.61kb -> 277.60kb (0%)
/RIGS/static/imgs/training.jpg -- 852.42kb -> 852.42kb (0%)
/RIGS/static/imgs/assets.jpg -- 1,348.94kb -> 1,348.94kb (0%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2022-01-18 19:32:05 +00:00
c537118037 Fix typo in training level list 2022-01-18 17:43:52 +00:00
466a9a9693 Delete broken migration
Manual SQL time whee
2022-01-18 16:20:18 +00:00
d25381b2de Create the training database (#463)
Co-authored-by: josephjboyden <josephjboyden@gmail.com>
2022-01-18 15:47:53 +00:00
dependabot[bot]
eaf891daf7 Build(deps): Bump copy-props from 2.0.4 to 2.0.5 (#468)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-15 12:13:40 +00:00
dependabot[bot]
801d2e8a7d Build(deps): Bump marked from 4.0.8 to 4.0.10 (#466)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-15 11:51:58 +00:00
dependabot[bot]
3d329219b8 Build(deps): Bump follow-redirects from 1.14.6 to 1.14.7 (#467)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-15 11:50:56 +00:00
2ddc8923ba CHORE: Fix pep8 2022-01-14 18:01:59 +00:00
276a86c5be FEAT(Asset): Add filter by date acquired
Date created isn't a DB field, so isn't efficient to filter by...
2022-01-14 17:54:20 +00:00
52 changed files with 660 additions and 378 deletions

View File

@@ -33,11 +33,11 @@ envparse = "~=0.2.0"
gunicorn = "~=20.0.4" gunicorn = "~=20.0.4"
icalendar = "~=4.0.7" icalendar = "~=4.0.7"
idna = "~=2.10" idna = "~=2.10"
lxml = "~=4.6.3" lxml = "~=4.7.1"
Markdown = "~=3.3.3" Markdown = "~=3.3.3"
msgpack = "~=1.0.2" msgpack = "~=1.0.2"
pep517 = "~=0.9.1" pep517 = "~=0.9.1"
Pillow = "~=8.3.2" Pillow = "~=9.0.0"
premailer = "~=3.7.0" premailer = "~=3.7.0"
progress = "~=1.5" progress = "~=1.5"
psutil = "~=5.8.0" psutil = "~=5.8.0"
@@ -78,6 +78,7 @@ diff-match-patch = "*"
python-barcode = "*" python-barcode = "*"
django-hCaptcha = "*" django-hCaptcha = "*"
importlib-metadata = "*" importlib-metadata = "*"
django-hcaptcha = "*"
[dev-packages] [dev-packages]
selenium = "~=3.141.0" selenium = "~=3.141.0"

254
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "db33559aff5586d7f78c1aa6a10e2f0bb05167c598ad995ec549f65f4710ae0a" "sha256": "7db5b3a9029be79c79efff791a42803a4765fe52c4f264f8a7be48ac4b1bda7a"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -309,10 +309,11 @@
}, },
"django-hcaptcha": { "django-hcaptcha": {
"hashes": [ "hashes": [
"sha256:2b80197c07bb8444249bcce3758b0472d369cca309fb02d7abcd0a856431b76b" "sha256:18804fb38a01827b6c65d111bac31265c1b96fcf52d7a54c3e2d2cb1c62ddcde",
"sha256:b2519eaf0cc97865ac72f825301122c5cf61e1e4852d6895994160222acb6c1a"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.1.0" "version": "==0.2.0"
}, },
"django-htmlmin": { "django-htmlmin": {
"hashes": [ "hashes": [
@@ -362,11 +363,11 @@
}, },
"django-widget-tweaks": { "django-widget-tweaks": {
"hashes": [ "hashes": [
"sha256:19bcb66a4a9e68493ced04e7124882d753c5be517ed001556f9e35a40147f545", "sha256:9bfc5c705684754a83cc81da328b39ad1b80f32bd0f4340e2a810cbab4b0c00e",
"sha256:d6c64fbf92cd2df9031f597c1374982233c05a1190d295c39d1c57ce007569c7" "sha256:fe6b17d5d595c63331f300917980db2afcf71f240ab9341b954aea8f45d25b9a"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.4.9" "version": "==1.4.12"
}, },
"envparse": { "envparse": {
"hashes": [ "hashes": [
@@ -424,69 +425,69 @@
}, },
"lxml": { "lxml": {
"hashes": [ "hashes": [
"sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59", "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4",
"sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3", "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f",
"sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9", "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a",
"sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f", "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944",
"sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b", "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1",
"sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04", "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d",
"sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0", "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d",
"sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120", "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e",
"sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570", "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d",
"sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b", "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a",
"sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c", "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675",
"sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2", "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3",
"sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9", "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55",
"sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c", "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60",
"sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242", "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d",
"sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41", "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6",
"sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89", "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e",
"sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33", "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5",
"sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d", "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5",
"sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808", "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42",
"sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e", "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0",
"sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c", "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d",
"sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a", "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489",
"sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44", "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440",
"sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5", "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e",
"sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210", "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6",
"sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca", "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e",
"sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8", "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f",
"sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887", "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d",
"sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5", "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03",
"sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71", "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9",
"sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b", "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9",
"sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b", "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd",
"sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996", "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6",
"sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542", "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4",
"sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0", "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868",
"sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace", "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267",
"sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd", "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2",
"sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b", "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4",
"sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c", "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24",
"sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93", "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2",
"sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1", "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db",
"sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2", "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a",
"sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808", "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8",
"sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9", "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175",
"sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090", "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851",
"sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9", "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b",
"sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb", "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e",
"sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144", "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986",
"sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d", "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f",
"sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203", "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419",
"sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a", "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7",
"sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408", "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7",
"sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71", "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36",
"sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952", "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc",
"sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4", "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b",
"sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352", "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e",
"sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8", "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17",
"sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944", "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3",
"sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1" "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.6.5" "version": "==4.7.1"
}, },
"markdown": { "markdown": {
"hashes": [ "hashes": [
@@ -563,6 +564,7 @@
"sha256:11a9c17f6262113d37454638d61c6102eff298309ebfcf4b6c96a3fe3dd57785", "sha256:11a9c17f6262113d37454638d61c6102eff298309ebfcf4b6c96a3fe3dd57785",
"sha256:15cf594b41ba10415181c22cd9e1aab288929bd1b382a534a05f82293b0eac3a", "sha256:15cf594b41ba10415181c22cd9e1aab288929bd1b382a534a05f82293b0eac3a",
"sha256:19fbfce2b7b7cefd6227f4cd067611d0026dd5e8ef4c42b7f49e4e0016b1cf1a", "sha256:19fbfce2b7b7cefd6227f4cd067611d0026dd5e8ef4c42b7f49e4e0016b1cf1a",
"sha256:2250f45865a177688e7a225f76db4fad7fb9af46e43fad77081ca41c74307874",
"sha256:27a034849fa052e97b262be97efed65f8b1bf681214a754846faeccacd51a61d", "sha256:27a034849fa052e97b262be97efed65f8b1bf681214a754846faeccacd51a61d",
"sha256:394d93eafa7688efb4f1c6365ec540fa8768888c041396354209386f72849eb2", "sha256:394d93eafa7688efb4f1c6365ec540fa8768888c041396354209386f72849eb2",
"sha256:3dae8f11be19f55d3cf4b3eaf2b257aaf39f8f8bfd7eaab134c60c0f3438ec5c", "sha256:3dae8f11be19f55d3cf4b3eaf2b257aaf39f8f8bfd7eaab134c60c0f3438ec5c",
@@ -584,68 +586,48 @@
"sha256:bcd68a15d06987b519148a09ff1e6840ee71249130bde59ffdf374825dd5826d", "sha256:bcd68a15d06987b519148a09ff1e6840ee71249130bde59ffdf374825dd5826d",
"sha256:beef92deb39a04c08a7401eebbe99dbec44b136e0a4f31fe3670159755feea38", "sha256:beef92deb39a04c08a7401eebbe99dbec44b136e0a4f31fe3670159755feea38",
"sha256:c714685a0868f277fdf36afeb84a2aa696dab0182eaef4bb91cf3e6b776ba468", "sha256:c714685a0868f277fdf36afeb84a2aa696dab0182eaef4bb91cf3e6b776ba468",
"sha256:cd575cf0131683a7b661357bfd777b27c3c6c0d0fb7ef27e627f521122f75536",
"sha256:fb3d7fb390192cfb1e287503dbc03229c1c77fe9820cf084546bb63fa997fd87" "sha256:fb3d7fb390192cfb1e287503dbc03229c1c77fe9820cf084546bb63fa997fd87"
], ],
"version": "==4.3.1" "version": "==4.3.1"
}, },
"pillow": { "pillow": {
"hashes": [ "hashes": [
"sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30", "sha256:03b27b197deb4ee400ed57d8d4e572d2d8d80f825b6634daf6e2c18c3c6ccfa6",
"sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9", "sha256:0b281fcadbb688607ea6ece7649c5d59d4bbd574e90db6cd030e9e85bde9fecc",
"sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71", "sha256:0ebd8b9137630a7bbbff8c4b31e774ff05bbb90f7911d93ea2c9371e41039b52",
"sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9", "sha256:113723312215b25c22df1fdf0e2da7a3b9c357a7d24a93ebbe80bfda4f37a8d4",
"sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b", "sha256:2d16b6196fb7a54aff6b5e3ecd00f7c0bab1b56eee39214b2b223a9d938c50af",
"sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630", "sha256:2fd8053e1f8ff1844419842fd474fc359676b2e2a2b66b11cc59f4fa0a301315",
"sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875", "sha256:31b265496e603985fad54d52d11970383e317d11e18e856971bdbb86af7242a4",
"sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2", "sha256:3586e12d874ce2f1bc875a3ffba98732ebb12e18fb6d97be482bd62b56803281",
"sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1", "sha256:47f5cf60bcb9fbc46011f75c9b45a8b5ad077ca352a78185bd3e7f1d294b98bb",
"sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7", "sha256:490e52e99224858f154975db61c060686df8a6b3f0212a678e5d2e2ce24675c9",
"sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3", "sha256:500d397ddf4bbf2ca42e198399ac13e7841956c72645513e8ddf243b31ad2128",
"sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b", "sha256:52abae4c96b5da630a8b4247de5428f593465291e5b239f3f843a911a3cf0105",
"sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6", "sha256:6579f9ba84a3d4f1807c4aab4be06f373017fc65fff43498885ac50a9b47a553",
"sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba", "sha256:68e06f8b2248f6dc8b899c3e7ecf02c9f413aab622f4d6190df53a78b93d97a5",
"sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4", "sha256:6c5439bfb35a89cac50e81c751317faea647b9a3ec11c039900cd6915831064d",
"sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864", "sha256:72c3110228944019e5f27232296c5923398496b28be42535e3b2dc7297b6e8b6",
"sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056", "sha256:72f649d93d4cc4d8cf79c91ebc25137c358718ad75f99e99e043325ea7d56100",
"sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228", "sha256:7aaf07085c756f6cb1c692ee0d5a86c531703b6e8c9cae581b31b562c16b98ce",
"sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8", "sha256:80fe92813d208ce8aa7d76da878bdc84b90809f79ccbad2a288e9bcbeac1d9bd",
"sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb", "sha256:95545137fc56ce8c10de646074d242001a112a92de169986abd8c88c27566a05",
"sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d", "sha256:97b6d21771da41497b81652d44191489296555b761684f82b7b544c49989110f",
"sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da", "sha256:98cb63ca63cb61f594511c06218ab4394bf80388b3d66cd61d0b1f63ee0ea69f",
"sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073", "sha256:9f3b4522148586d35e78313db4db0df4b759ddd7649ef70002b6c3767d0fdeb7",
"sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3", "sha256:a09a9d4ec2b7887f7a088bbaacfd5c07160e746e3d47ec5e8050ae3b2a229e9f",
"sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616", "sha256:b5050d681bcf5c9f2570b93bee5d3ec8ae4cf23158812f91ed57f7126df91762",
"sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa", "sha256:bb47a548cea95b86494a26c89d153fd31122ed65255db5dcbc421a2d28eb3379",
"sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979", "sha256:bc462d24500ba707e9cbdef436c16e5c8cbf29908278af053008d9f689f56dee",
"sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a", "sha256:c2067b3bb0781f14059b112c9da5a91c80a600a97915b4f48b37f197895dd925",
"sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b", "sha256:d154ed971a4cc04b93a6d5b47f37948d1f621f25de3e8fa0c26b2d44f24e3e8f",
"sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6", "sha256:d5dcea1387331c905405b09cdbfb34611050cc52c865d71f2362f354faee1e9f",
"sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441", "sha256:ee6e2963e92762923956fe5d3479b1fdc3b76c83f290aad131a2f98c3df0593e",
"sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624", "sha256:fd0e5062f11cb3e730450a7d9f323f4051b532781026395c4323b8ad055523c4"
"sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd",
"sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550",
"sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09",
"sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196",
"sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b",
"sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1",
"sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6",
"sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83",
"sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f",
"sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4",
"sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19",
"sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341",
"sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96",
"sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355",
"sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c",
"sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c",
"sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629",
"sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2",
"sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87",
"sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5",
"sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"
], ],
"index": "pypi", "index": "pypi",
"version": "==8.3.2" "version": "==9.0.0"
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
@@ -859,11 +841,11 @@
}, },
"sentry-sdk": { "sentry-sdk": {
"hashes": [ "hashes": [
"sha256:2a1757d6611e4bec7d672c2b7ef45afef79fed201d064f53994753303944f5a8", "sha256:2cec50166bcb67e1965f8073541b2321e3864cd6fd42a526bcde9f0c4e4cc3f8",
"sha256:e4cb107e305b2c1b919414775fa73a9997f996447417d22b98e7610ded1e9eb5" "sha256:7bbaa32bba806ec629962f207b597e86831c7ee2c1f287c21ba7de7fea9a9c46"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.5.1" "version": "==1.5.2"
}, },
"simplejson": { "simplejson": {
"hashes": [ "hashes": [
@@ -1063,11 +1045,11 @@
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed",
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.26.7" "version": "==1.26.8"
}, },
"webencodings": { "webencodings": {
"hashes": [ "hashes": [
@@ -1322,11 +1304,11 @@
}, },
"charset-normalizer": { "charset-normalizer": {
"hashes": [ "hashes": [
"sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd",
"sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"
], ],
"markers": "python_version >= '3'", "markers": "python_version >= '3'",
"version": "==2.0.9" "version": "==2.0.10"
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
@@ -1536,11 +1518,11 @@
}, },
"pytest-reverse": { "pytest-reverse": {
"hashes": [ "hashes": [
"sha256:9f2a3b163378922dd332ed056a58af4cfd1ccc8ad4a76606f43ed43cfff2140b", "sha256:1695b7c9e51b28db5af13d579b33b54a80958d86b886dfabd2a246bcad3e082e",
"sha256:d878e28c785fb20291580aa816d566a21beac508e06a2c9eb4f934d49c31ce5c" "sha256:6acfb50acd11caf3d222366f5e1458dea2351d47b6ca5b08ab408158636250ba"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.3.0" "version": "==1.4.0"
}, },
"pytest-splinter": { "pytest-splinter": {
"hashes": [ "hashes": [
@@ -1602,11 +1584,11 @@
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed",
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.26.7" "version": "==1.26.8"
}, },
"zope.component": { "zope.component": {
"hashes": [ "hashes": [

View File

@@ -84,7 +84,7 @@ class BootstrapSelectElement(Region):
return [self.BootstrapSelectOption(self, i) for i in options] return [self.BootstrapSelectOption(self, i) for i in options]
def set_option(self, name, selected): def set_option(self, name, selected):
options = list((x for x in self.options if x.name == name)) options = [x for x in self.options if x.name == name]
assert len(options) == 1 assert len(options) == 1
options[0].set_selected(selected) options[0].set_selected(selected)

View File

@@ -44,7 +44,7 @@ def get_request_url(url):
@pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData', @pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData',
'deleteSampleData']) 'deleteSampleData', 'generateSampleTrainingData', 'generate_sample_training_users'])
def test_production_exception(command): def test_production_exception(command):
from django.core.management.base import CommandError from django.core.management.base import CommandError
with pytest.raises(CommandError, match=".*production"): with pytest.raises(CommandError, match=".*production"):

View File

@@ -101,11 +101,13 @@ class SecureAPIRequest(generic.View):
for field in fields: for field in fields:
q = Q(**{field + "__icontains": part}) q = Q(**{field + "__icontains": part})
qs.append(q) qs.append(q)
for filter in filters:
q = Q(**{field: True})
qs.append(q)
queries.append(reduce(operator.or_, qs)) queries.append(reduce(operator.or_, qs))
for f in filters:
q = Q(**{f: True})
queries.append(q)
# Build the data response list # Build the data response list
results = [] results = []
query = reduce(operator.and_, queries) query = reduce(operator.and_, queries)

View File

@@ -1 +0,0 @@
default_app_config = 'RIGS.apps.RIGSAppConfig'

View File

@@ -24,7 +24,7 @@ class InvoiceIndex(generic.ListView):
template_name = 'invoice_list.html' template_name = 'invoice_list.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(InvoiceIndex, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
total = 0 total = 0
for i in context['object_list']: for i in context['object_list']:
total += i.balance total += i.balance
@@ -41,8 +41,9 @@ class InvoiceDetail(generic.DetailView):
template_name = 'invoice_detail.html' template_name = 'invoice_detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(InvoiceDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "Invoice {} ({}) ".format(self.object.display_id, self.object.invoice_date.strftime("%d/%m/%Y")) invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date}) "
if self.object.void: if self.object.void:
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>" context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
elif self.object.is_closed: elif self.object.is_closed:
@@ -117,7 +118,7 @@ class InvoiceArchive(generic.ListView):
paginate_by = 25 paginate_by = 25
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(InvoiceArchive, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "Invoice Archive" context['page_title'] = "Invoice Archive"
context['description'] = "This page displays all invoices: outstanding, paid, and void" context['description'] = "This page displays all invoices: outstanding, paid, and void"
return context return context
@@ -196,7 +197,7 @@ class PaymentCreate(generic.CreateView):
template_name = 'payment_form.html' template_name = 'payment_form.html'
def get_initial(self): def get_initial(self):
initial = super(generic.CreateView, self).get_initial() initial = super().get_initial()
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None)) invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
if invoicepk is None: if invoicepk is None:
raise Http404() raise Http404()

View File

@@ -8,6 +8,7 @@ from django.utils import timezone
from reversion import revisions as reversion from reversion import revisions as reversion
from RIGS import models from RIGS import models
from training.models import TrainingLevel
# Override the django form defaults to use the HTML date/time/datetime UI elements # Override the django form defaults to use the HTML date/time/datetime UI elements
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'}) forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
@@ -96,10 +97,10 @@ class EventForm(forms.ModelForm):
raise forms.ValidationError( raise forms.ValidationError(
'You haven\'t provided any client contact details. Please add a person or organisation.', 'You haven\'t provided any client contact details. Please add a person or organisation.',
code='contact') code='contact')
return super(EventForm, self).clean() return super().clean()
def save(self, commit=True): def save(self, commit=True):
m = super(EventForm, self).save(commit=False) m = super().save(commit=False)
if (commit): if (commit):
m.save() m.save()
@@ -138,7 +139,7 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm): class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(InternalClientEventAuthorisationForm, self).__init__(**kwargs) super().__init__(**kwargs)
self.fields['uni_id'].required = True self.fields['uni_id'].required = True
self.fields['account_code'].required = True self.fields['account_code'].required = True
@@ -153,7 +154,7 @@ class EventAuthorisationRequestForm(forms.Form):
class EventRiskAssessmentForm(forms.ModelForm): class EventRiskAssessmentForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EventRiskAssessmentForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for name, field in self.fields.items(): for name, field in self.fields.items():
if str(name) == 'supervisor_consulted': if str(name) == 'supervisor_consulted':
field.widget = forms.CheckboxInput() field.widget = forms.CheckboxInput()
@@ -164,6 +165,9 @@ class EventRiskAssessmentForm(forms.ModelForm):
], attrs={'class': 'custom-control-input', 'required': 'true'}) ], attrs={'class': 'custom-control-input', 'required': 'true'})
def clean(self): def clean(self):
if self.cleaned_data.get('big_power'):
if not self.cleaned_data.get('power_mic').level_qualifications.filter(level__department=TrainingLevel.POWER).exists():
self.add_error('power_mic', forms.ValidationError("Your Power MIC must be a Power Technician.", code="power_tech_required"))
# Check expected values # Check expected values
unexpected_values = [] unexpected_values = []
for field, value in models.RiskAssessment.expected_values.items(): for field, value in models.RiskAssessment.expected_values.items():
@@ -181,7 +185,7 @@ class EventRiskAssessmentForm(forms.ModelForm):
class EventChecklistForm(forms.ModelForm): class EventChecklistForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EventChecklistForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['date'].widget.format = '%Y-%m-%d' self.fields['date'].widget.format = '%Y-%m-%d'
for name, field in self.fields.items(): for name, field in self.fields.items():
if field.__class__ == forms.NullBooleanField: if field.__class__ == forms.NullBooleanField:

View File

@@ -3,6 +3,7 @@
from django.db import models, migrations from django.db import models, migrations
import RIGS.models import RIGS.models
import versioning
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -25,6 +26,6 @@ class Migration(migrations.Migration):
], ],
options={ options={
}, },
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
] ]

View File

@@ -3,6 +3,7 @@
from django.db import models, migrations from django.db import models, migrations
import RIGS.models import RIGS.models
import versioning
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -21,6 +22,6 @@ class Migration(migrations.Migration):
], ],
options={ options={
}, },
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
] ]

View File

@@ -4,6 +4,7 @@
from django.db import models, migrations from django.db import models, migrations
from django.conf import settings from django.conf import settings
import RIGS.models import RIGS.models
import versioning
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -41,7 +42,7 @@ class Migration(migrations.Migration):
], ],
options={ options={
}, },
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
migrations.CreateModel( migrations.CreateModel(
name='EventItem', name='EventItem',
@@ -70,7 +71,7 @@ class Migration(migrations.Migration):
], ],
options={ options={
}, },
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
migrations.AddField( migrations.AddField(
model_name='event', model_name='event',

View File

@@ -4,6 +4,7 @@ import RIGS.models
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import versioning
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -58,7 +59,7 @@ class Migration(migrations.Migration):
'ordering': ['event'], 'ordering': ['event'],
'permissions': [('review_eventchecklist', 'Can review Event Checklists')], 'permissions': [('review_eventchecklist', 'Can review Event Checklists')],
}, },
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
migrations.CreateModel( migrations.CreateModel(
name='EventChecklistCrew', name='EventChecklistCrew',
@@ -69,7 +70,7 @@ class Migration(migrations.Migration):
('end', models.DateTimeField()), ('end', models.DateTimeField()),
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.eventchecklist')), ('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.eventchecklist')),
], ],
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
migrations.CreateModel( migrations.CreateModel(
name='EventChecklistVehicle', name='EventChecklistVehicle',
@@ -78,7 +79,7 @@ class Migration(migrations.Migration):
('vehicle', models.CharField(max_length=255)), ('vehicle', models.CharField(max_length=255)),
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to='RIGS.eventchecklist')), ('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to='RIGS.eventchecklist')),
], ],
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
migrations.CreateModel( migrations.CreateModel(
name='RiskAssessment', name='RiskAssessment',
@@ -117,7 +118,7 @@ class Migration(migrations.Migration):
'ordering': ['event'], 'ordering': ['event'],
'permissions': [('review_riskassessment', 'Can review Risk Assessments')], 'permissions': [('review_riskassessment', 'Can review Risk Assessments')],
}, },
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='eventcrew', model_name='eventcrew',

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.11 on 2022-01-09 14:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0043_auto_20211027_1519'),
]
operations = [
migrations.AddField(
model_name='profile',
name='is_supervisor',
field=models.BooleanField(default=False),
),
]

View File

@@ -27,6 +27,7 @@ class Profile(AbstractUser):
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that... # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
last_emailed = models.DateTimeField(blank=True, null=True) last_emailed = models.DateTimeField(blank=True, null=True)
dark_theme = models.BooleanField(default=False) dark_theme = models.BooleanField(default=False)
is_supervisor = models.BooleanField(default=False)
reversion_hide = True reversion_hide = True
@@ -56,11 +57,6 @@ class Profile(AbstractUser):
def latest_events(self): def latest_events(self):
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists') return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
@cached_property
def as_trainee(self):
from training.models import Trainee
return Trainee.objects.get(pk=self.pk)
@classmethod @classmethod
def admins(cls): def admins(cls):
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x]) return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
@@ -73,7 +69,7 @@ class Profile(AbstractUser):
return self.name return self.name
class RevisionMixin(object): class RevisionMixin:
@property @property
def is_first_version(self): def is_first_version(self):
versions = Version.objects.get_for_object(self) versions = Version.objects.get_for_object(self)
@@ -103,7 +99,7 @@ class RevisionMixin(object):
version = self.current_version version = self.current_version
if version is None: if version is None:
return None return None
return "V{0} | R{1}".format(version.pk, version.revision.pk) return f"V{version.pk} | R{version.revision.pk}"
class Person(models.Model, RevisionMixin): class Person(models.Model, RevisionMixin):
@@ -211,7 +207,7 @@ class VatRate(models.Model, RevisionMixin):
get_latest_by = 'start_at' get_latest_by = 'start_at'
def __str__(self): def __str__(self):
return self.comment + " " + str(self.start_at) + " @ " + str(self.as_percent) + "%" return f"{self.comment} {self.start_at} @ {self.as_percent}%"
class Venue(models.Model, RevisionMixin): class Venue(models.Model, RevisionMixin):
@@ -351,10 +347,10 @@ class Event(models.Model, RevisionMixin):
if self.pk: if self.pk:
if self.is_rig: if self.is_rig:
return str("N%05d" % self.pk) return str("N%05d" % self.pk)
else:
return self.pk return self.pk
else:
return "????" return "????"
# Calculated values # Calculated values
""" """
@@ -479,7 +475,7 @@ class Event(models.Model, RevisionMixin):
return reverse('event_detail', kwargs={'pk': self.pk}) return reverse('event_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return "{}: {}".format(self.display_id, self.name) return f"{self.display_id}: {self.name}"
def clean(self): def clean(self):
errdict = {} errdict = {}
@@ -525,11 +521,11 @@ class EventItem(models.Model, RevisionMixin):
ordering = ['order'] ordering = ['order']
def __str__(self): def __str__(self):
return "{}.{}: {} | {}".format(self.event_id, self.order, self.event.name, self.name) return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return str("item {}".format(self.name)) return f"item {self.name}"
@reversion.register @reversion.register
@@ -547,7 +543,7 @@ class EventAuthorisation(models.Model, RevisionMixin):
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return "{} (requested by {})".format(self.event.display_id, self.sent_by.initials) return f"{self.event.display_id} (requested by {self.sent_by.initials})"
class InvoiceManager(models.Manager): class InvoiceManager(models.Manager):
@@ -675,7 +671,6 @@ class RiskAssessment(models.Model, RevisionMixin):
# Power # Power
big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?") big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
# If yes to the above two, you must answer...
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True, power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)") verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)")
outside = models.BooleanField(help_text="Is the event outdoors?") outside = models.BooleanField(help_text="Is the event outdoors?")

View File

@@ -38,7 +38,7 @@ class RigboardIndex(generic.TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# get super context # get super context
context = super(RigboardIndex, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# call out method to get current events # call out method to get current events
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists') context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
@@ -50,7 +50,7 @@ class WebCalendar(generic.TemplateView):
template_name = 'calendar.html' template_name = 'calendar.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(WebCalendar, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['view'] = kwargs.get('view', '') context['view'] = kwargs.get('view', '')
context['date'] = kwargs.get('date', '') context['date'] = kwargs.get('date', '')
return context return context
@@ -61,8 +61,8 @@ class EventDetail(generic.DetailView):
model = models.Event model = models.Event
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
title = "{} | {}".format(self.object.display_id, self.object.name) title = f"{self.object.display_id} | {self.object.name}"
if self.object.dry_hire: if self.object.dry_hire:
title += " <span class='badge badge-secondary'>Dry Hire</span>" title += " <span class='badge badge-secondary'>Dry Hire</span>"
context['page_title'] = title context['page_title'] = title
@@ -84,7 +84,7 @@ class EventCreate(generic.CreateView):
template_name = 'event_form.html' template_name = 'event_form.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventCreate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "New Event" context['page_title'] = "New Event"
context['edit'] = True context['edit'] = True
context['currentVAT'] = models.VatRate.objects.current_rate() context['currentVAT'] = models.VatRate.objects.current_rate()
@@ -110,8 +110,8 @@ class EventUpdate(generic.UpdateView):
template_name = 'event_form.html' template_name = 'event_form.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventUpdate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "Event {}".format(self.object.display_id) context['page_title'] = f"Event {self.object.display_id}"
context['edit'] = True context['edit'] = True
form = context['form'] form = context['form']
@@ -134,7 +134,7 @@ class EventUpdate(generic.UpdateView):
if hasattr(self.object, 'authorised'): if hasattr(self.object, 'authorised'):
messages.warning(self.request, messages.warning(self.request,
'This event has already been authorised by the client, any changes to the price will require reauthorisation.') 'This event has already been authorised by the client, any changes to the price will require reauthorisation.')
return super(EventUpdate, self).render_to_response(context, **response_kwargs) return super().render_to_response(context, **response_kwargs)
def get_success_url(self): def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk}) return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
@@ -142,7 +142,7 @@ class EventUpdate(generic.UpdateView):
class EventDuplicate(EventUpdate): class EventDuplicate(EventUpdate):
def get_object(self, queryset=None): def get_object(self, queryset=None):
old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating) old = super().get_object(queryset) # Get the object (the event you're duplicating)
new = copy.copy(old) # Make a copy of the object in memory new = copy.copy(old) # Make a copy of the object in memory
new.based_on = old # Make the new event based on the old event new.based_on = old # Make the new event based on the old event
new.purchase_order = None # Remove old PO new.purchase_order = None # Remove old PO
@@ -167,8 +167,8 @@ class EventDuplicate(EventUpdate):
return new return new
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventDuplicate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "Duplicate of Event {}".format(self.object.display_id) context['page_title'] = f"Duplicate of Event {self.object.display_id}"
context["duplicate"] = True context["duplicate"] = True
return context return context
@@ -210,8 +210,7 @@ class EventArchive(generic.ListView):
paginate_by = 25 paginate_by = 25
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# get super context context = super().get_context_data(**kwargs)
context = super(EventArchive, self).get_context_data(**kwargs)
context['start'] = self.request.GET.get('start', None) context['start'] = self.request.GET.get('start', None)
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d')) context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
@@ -266,7 +265,7 @@ class EventArchive(generic.ListView):
# Preselect related for efficiency # Preselect related for efficiency
qs.select_related('person', 'organisation', 'venue', 'mic') qs.select_related('person', 'organisation', 'venue', 'mic')
if len(qs) == 0: if not qs.exists():
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.") messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
return qs return qs
@@ -283,7 +282,7 @@ class EventAuthorise(generic.UpdateView):
self.template_name = self.success_template self.template_name = self.success_template
messages.add_message(self.request, messages.SUCCESS, messages.add_message(self.request, messages.SUCCESS,
'Success! Your event has been authorised. ' + 'Success! Your event has been authorised. ' +
'You will also receive email confirmation to %s.' % self.object.email) f'You will also receive email confirmation to {self.object.email}.')
return self.render_to_response(self.get_context_data()) return self.render_to_response(self.get_context_data())
@property @property
@@ -297,10 +296,10 @@ class EventAuthorise(generic.UpdateView):
return forms.InternalClientEventAuthorisationForm return forms.InternalClientEventAuthorisationForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventAuthorise, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['event'] = self.event context['event'] = self.event
context['tos_url'] = settings.TERMS_OF_HIRE_URL context['tos_url'] = settings.TERMS_OF_HIRE_URL
context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name) context['page_title'] = f"{self.event.display_id}: {self.event.name}"
if self.event.dry_hire: if self.event.dry_hire:
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>' context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
context['preview'] = self.preview context['preview'] = self.preview
@@ -319,7 +318,7 @@ class EventAuthorise(generic.UpdateView):
return super(EventAuthorise, self).get(request, *args, **kwargs) return super(EventAuthorise, self).get(request, *args, **kwargs)
def get_form(self, **kwargs): def get_form(self, **kwargs):
form = super(EventAuthorise, self).get_form(**kwargs) form = super().get_form(**kwargs)
form.instance.event = self.event form.instance.event = self.event
form.instance.email = self.request.email form.instance.email = self.request.email
form.instance.sent_by = self.request.sent_by form.instance.sent_by = self.request.sent_by
@@ -335,7 +334,7 @@ class EventAuthorise(generic.UpdateView):
except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist): except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist):
raise SuspiciousOperation( raise SuspiciousOperation(
"This URL is invalid. Please ask your TEC contact for a new URL") "This URL is invalid. Please ask your TEC contact for a new URL")
return super(EventAuthorise, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin): class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
@@ -345,7 +344,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
@method_decorator(decorators.nottinghamtec_address_required) @method_decorator(decorators.nottinghamtec_address_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(EventAuthorisationRequest, self).dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
@property @property
def object(self): def object(self):
@@ -406,13 +405,13 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
css = finders.find('css/email.css') css = finders.find('css/email.css')
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs) response = super().render_to_response(context, **response_kwargs)
assert isinstance(response, HttpResponse) assert isinstance(response, HttpResponse)
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform() response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
return response return response
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventAuthoriseRequestEmailPreview, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['hmac'] = signing.dumps({ context['hmac'] = signing.dumps({
'pk': self.object.pk, 'pk': self.object.pk,
'email': self.request.GET.get('email', 'hello@world.test'), 'email': self.request.GET.get('email', 'hello@world.test'),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 MiB

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

After

Width:  |  Height:  |  Size: 852 KiB

View File

@@ -6,7 +6,7 @@ class PersonList(GenericListView):
model = models.Person model = models.Person
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(PersonList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "People" context['page_title'] = "People"
context['create'] = 'person_create' context['create'] = 'person_create'
context['edit'] = 'person_update' context['edit'] = 'person_update'
@@ -19,7 +19,7 @@ class PersonDetail(GenericDetailView):
model = models.Person model = models.Person
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(PersonDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['history_link'] = 'person_history' context['history_link'] = 'person_history'
context['detail_link'] = 'person_detail' context['detail_link'] = 'person_detail'
context['update_link'] = 'person_update' context['update_link'] = 'person_update'
@@ -49,7 +49,7 @@ class OrganisationList(GenericListView):
model = models.Organisation model = models.Organisation
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(OrganisationList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['create'] = 'organisation_create' context['create'] = 'organisation_create'
context['edit'] = 'organisation_update' context['edit'] = 'organisation_update'
context['can_edit'] = self.request.user.has_perm('RIGS.change_organisation') context['can_edit'] = self.request.user.has_perm('RIGS.change_organisation')
@@ -62,7 +62,7 @@ class OrganisationDetail(GenericDetailView):
model = models.Organisation model = models.Organisation
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(OrganisationDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['history_link'] = 'organisation_history' context['history_link'] = 'organisation_history'
context['detail_link'] = 'organisation_detail' context['detail_link'] = 'organisation_detail'
context['update_link'] = 'organisation_update' context['update_link'] = 'organisation_update'
@@ -92,7 +92,7 @@ class VenueList(GenericListView):
model = models.Venue model = models.Venue
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(VenueList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['create'] = 'venue_create' context['create'] = 'venue_create'
context['edit'] = 'venue_update' context['edit'] = 'venue_update'
context['can_edit'] = self.request.user.has_perm('RIGS.change_venue') context['can_edit'] = self.request.user.has_perm('RIGS.change_venue')
@@ -104,7 +104,7 @@ class VenueDetail(GenericDetailView):
model = models.Venue model = models.Venue
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(VenueDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['history_link'] = 'venue_history' context['history_link'] = 'venue_history'
context['detail_link'] = 'venue_detail' context['detail_link'] = 'venue_detail'
context['update_link'] = 'venue_update' context['update_link'] = 'venue_update'

View File

@@ -1 +0,0 @@
default_app_config = 'assets.apps.AssetsAppConfig'

View File

@@ -33,6 +33,7 @@ class AssetSearchForm(forms.Form):
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False) category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False) status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
is_cable = forms.BooleanField(required=False) is_cable = forms.BooleanField(required=False)
date_acquired = forms.DateField(required=False)
class SupplierForm(forms.ModelForm): class SupplierForm(forms.ModelForm):
@@ -45,11 +46,3 @@ class CableTypeForm(forms.ModelForm):
class Meta: class Meta:
model = models.CableType model = models.CableType
fields = '__all__' fields = '__all__'
def clean(self): # TODO Does unique_together work better than this?
form_data = self.cleaned_data
queryset = models.CableType.objects.filter(Q(plug=form_data['plug']) & Q(socket=form_data['socket']) & Q(circuits=form_data['circuits']) & Q(cores=form_data['cores']))
# Being identical to itself shouldn't count...
if queryset.exists() and self.instance.pk != queryset[0].pk:
raise forms.ValidationError("A cable type that exactly matches this one already exists, please use that instead.", code="notunique")
return form_data

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.11 on 2022-01-12 19:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0021_auto_20210302_1204'),
]
operations = [
migrations.AlterUniqueTogether(
name='cabletype',
unique_together={('plug', 'socket', 'circuits', 'cores')},
),
]

View File

@@ -6,7 +6,8 @@ from django.urls import reverse
from reversion import revisions as reversion from reversion import revisions as reversion
from reversion.models import Version from reversion.models import Version
from RIGS.models import RevisionMixin, Profile from RIGS.models import Profile
from versioning.versioning import RevisionMixin
class AssetCategory(models.Model): class AssetCategory(models.Model):
@@ -75,10 +76,11 @@ class CableType(models.Model):
class Meta: class Meta:
ordering = ['plug', 'socket', '-circuits'] ordering = ['plug', 'socket', '-circuits']
unique_together = ['plug', 'socket', 'circuits', 'cores']
def __str__(self): def __str__(self):
if self.plug and self.socket: if self.plug and self.socket:
return "%s%s" % (self.plug.description, self.socket.description) return f"{self.plug.description}{self.socket.description}"
else: else:
return "Unknown" return "Unknown"
@@ -147,7 +149,7 @@ class Asset(models.Model, RevisionMixin):
] ]
def __str__(self): def __str__(self):
return "{} | {}".format(self.asset_id, self.description) return f"{self.asset_id} | {self.description}"
def get_absolute_url(self): def get_absolute_url(self):
return reverse('asset_detail', kwargs={'pk': self.asset_id}) return reverse('asset_detail', kwargs={'pk': self.asset_id})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -61,25 +61,45 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col px-0"> <div class="col px-0">
<form id="asset-search-form" method="GET" class="form-inline justify-content-end"> <form id="asset-search-form" method="GET">
<div class="input-group px-1 mb-2 mb-sm-0 flex-nowrap"> <div class="form-row">
{% render_field form.q|add_class:'form-control' placeholder='Enter Asset ID/Desc/Serial' %} <div class="col">
<label for="q" class="sr-only">Asset ID/Description/Serial Number:</label> <div class="input-group px-1 mb-2 mb-sm-0 flex-nowrap">
<span class="input-group-append">{% button 'search' id="id_search" %}</span> {% render_field form.q|add_class:'form-control' placeholder='Enter Asset ID/Desc/Serial' %}
</div> <label for="q" class="sr-only">Asset ID/Description/Serial Number:</label>
<div id="category-group" class="form-group px-1" style="margin-bottom: 0;"> <span class="input-group-append">{% button 'search' id="id_search" %}</span>
<label for="category" class="sr-only">Category</label> </div>
{% render_field form.category|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %} </div>
</div> </div>
<div id="status-group" class="form-group px-1" style="margin-bottom: 0;"> <div class="form-row mt-2">
<label for="status" class="sr-only">Status</label> <div class="col">
{% render_field form.status|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %} <div id="category-group" class="form-group px-1" style="margin-bottom: 0;">
</div> <label for="category" class="sr-only">Category</label>
<div class="form-check form-check-inline"> {% render_field form.category|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
{% render_field form.is_cable|add_class:'form-check-input' %} </div>
<label class="form-check-label" for="is_cable">Only Cables?</label> </div>
</div> <div class="col">
<button id="filter-submit" type="submit" class="btn btn-secondary" style="width: 6em">Filter</button> <div id="status-group" class="form-group px-1" style="margin-bottom: 0;">
<label for="status" class="sr-only">Status</label>
{% render_field form.status|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
</div>
</div>
<div class="col mt-2">
<div class="form-check form-check-inline">
{% render_field form.is_cable|add_class:'form-check-input' %}
<label class="form-check-label" for="is_cable">Only Cables?</label>
</div>
</div>
<div class="col-auto">
<div class="form-group d-flex flex-nowrap">
<label for="date_acquired" class="text-nowrap mt-auto">Date Acquired</label>
{% render_field form.date_acquired|add_class:'form-control mx-2' %}
</div>
</div>
<div class="col-auto mr-auto">
<button id="filter-submit" type="submit" class="btn btn-secondary" style="width: 6em">Filter</button>
</div>
</div>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -180,7 +180,7 @@ class TestAssetForm(AutoLoginTest):
def test_asset_edit(self): def test_asset_edit(self):
self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open() self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
self.assertTrue(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly') is not None) self.assertIsNotNone(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly'))
new_description = "Big Shelf" new_description = "Big Shelf"
self.page.description = new_description self.page.description = new_description
@@ -335,7 +335,7 @@ class TestAssetAudit(AutoLoginTest):
self.assertNotIn(self.asset.asset_id, self.page.assets) self.assertNotIn(self.asset.asset_id, self.page.assets)
def test_audit_list(self): def test_audit_list(self):
self.assertEqual(len(models.Asset.objects.filter(last_audited_at=None)), len(self.page.assets)) self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets))
asset_row = self.page.assets[0] asset_row = self.page.assets[0]
self.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click() self.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click()
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal'))) self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))

View File

@@ -105,7 +105,6 @@ def test_asset_edit(admin_client, test_asset):
def test_cable_edit(admin_client, test_cable): def test_cable_edit(admin_client, test_cable):
url = reverse('asset_update', kwargs={'pk': test_cable.asset_id}) url = reverse('asset_update', kwargs={'pk': test_cable.asset_id})
# TODO Why do I have to send is_cable=True here?
response = admin_client.post(url, {'is_cable': True, 'length': -3, 'csa': -3}) response = admin_client.post(url, {'is_cable': True, 'length': -3, 'csa': -3})
# TODO Can't figure out how to select the 'none' option... # TODO Can't figure out how to select the 'none' option...

View File

@@ -61,6 +61,9 @@ class AssetList(LoginRequiredMixin, generic.ListView):
if form.cleaned_data['is_cable']: if form.cleaned_data['is_cable']:
queryset = queryset.filter(is_cable=True) queryset = queryset.filter(is_cable=True)
if form.cleaned_data['date_acquired']:
queryset = queryset.filter(date_acquired=form.cleaned_data['date_acquired'])
if form.cleaned_data['category']: if form.cleaned_data['category']:
queryset = queryset.filter(category__in=form.cleaned_data['category']) queryset = queryset.filter(category__in=form.cleaned_data['category'])
@@ -73,7 +76,7 @@ class AssetList(LoginRequiredMixin, generic.ListView):
return queryset.select_related('category', 'status') return queryset.select_related('category', 'status')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AssetList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["form"] = self.form context["form"] = self.form
if hasattr(self.form, 'cleaned_data'): if hasattr(self.form, 'cleaned_data'):
context["category_filters"] = self.form.cleaned_data.get('category') context["category_filters"] = self.form.cleaned_data.get('category')
@@ -114,7 +117,7 @@ class AssetDetail(LoginRequiredMixin, AssetIDUrlMixin, generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["page_title"] = "Asset {}".format(self.object.display_id) context["page_title"] = f"Asset {self.object.display_id}"
return context return context
@@ -127,7 +130,7 @@ class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["edit"] = True context["edit"] = True
context["connectors"] = models.Connector.objects.all() context["connectors"] = models.Connector.objects.all()
context["page_title"] = "Edit Asset: {}".format(self.object.display_id) context["page_title"] = f"Edit Asset: {self.object.display_id}"
return context return context
def get_success_url(self): def get_success_url(self):
@@ -147,7 +150,7 @@ class AssetCreate(LoginRequiredMixin, generic.CreateView):
form_class = forms.AssetForm form_class = forms.AssetForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AssetCreate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["create"] = True context["create"] = True
context["connectors"] = models.Connector.objects.all() context["connectors"] = models.Connector.objects.all()
context["page_title"] = "Create Asset" context["page_title"] = "Create Asset"
@@ -174,8 +177,9 @@ class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["create"] = None context["create"] = None
context["duplicate"] = True context["duplicate"] = True
context['previous_asset_id'] = self.get_object().asset_id old_id = self.get_object().asset_id
context["page_title"] = "Duplication of Asset: {}".format(context['previous_asset_id']) context['previous_asset_id'] = old_id
context["page_title"] = f"Duplication of Asset: {old_id}"
return context return context
@@ -198,7 +202,7 @@ class AssetAuditList(AssetList):
return self.model.objects.filter(Q(last_audited_at__isnull=True)).select_related('category', 'status') return self.model.objects.filter(Q(last_audited_at__isnull=True)).select_related('category', 'status')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AssetAuditList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "Asset Audit List" context['page_title'] = "Asset Audit List"
return context return context
@@ -209,7 +213,7 @@ class AssetAudit(AssetEdit):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["page_title"] = "Audit Asset: {}".format(self.object.display_id) context["page_title"] = f"Audit Asset: {self.object.display_id}"
return context return context
def get_success_url(self): def get_success_url(self):
@@ -226,7 +230,7 @@ class SupplierList(GenericListView):
ordering = ['name'] ordering = ['name']
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SupplierList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['create'] = 'supplier_create' context['create'] = 'supplier_create'
context['edit'] = 'supplier_update' context['edit'] = 'supplier_update'
context['can_edit'] = self.request.user.has_perm('assets.change_supplier') context['can_edit'] = self.request.user.has_perm('assets.change_supplier')
@@ -253,7 +257,7 @@ class SupplierDetail(GenericDetailView):
model = models.Supplier model = models.Supplier
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SupplierDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['history_link'] = 'supplier_history' context['history_link'] = 'supplier_history'
context['update_link'] = 'supplier_update' context['update_link'] = 'supplier_update'
context['detail_link'] = 'supplier_detail' context['detail_link'] = 'supplier_detail'
@@ -272,7 +276,7 @@ class SupplierCreate(GenericCreateView, ModalURLMixin):
form_class = forms.SupplierForm form_class = forms.SupplierForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SupplierCreate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if is_ajax(self.request): if is_ajax(self.request):
context['override'] = "base_ajax.html" context['override'] = "base_ajax.html"
else: else:
@@ -318,8 +322,8 @@ class CableTypeDetail(generic.DetailView):
template_name = 'cable_type_detail.html' template_name = 'cable_type_detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CableTypeDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["page_title"] = "Cable Type {}".format(str(self.object)) context["page_title"] = f"Cable Type {self.object}"
return context return context
@@ -329,7 +333,7 @@ class CableTypeCreate(generic.CreateView):
form_class = forms.CableTypeForm form_class = forms.CableTypeForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CableTypeCreate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["create"] = True context["create"] = True
context["page_title"] = "Create Cable Type" context["page_title"] = "Create Cable Type"
@@ -345,9 +349,9 @@ class CableTypeUpdate(generic.UpdateView):
form_class = forms.CableTypeForm form_class = forms.CableTypeForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CableTypeUpdate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["edit"] = True context["edit"] = True
context["page_title"] = "Edit Cable Type" context["page_title"] = f"Edit Cable Type {self.object}"
return context return context
@@ -362,10 +366,10 @@ def generate_label(pk):
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20) font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20)
obj = get_object_or_404(models.Asset, asset_id=pk) obj = get_object_or_404(models.Asset, asset_id=pk)
asset_id = "Asset: {}".format(obj.asset_id) asset_id = f"Asset: {obj.asset_id}"
if obj.is_cable: if obj.is_cable:
length = "Length: {}m".format(obj.length) length = f"Length: {obj.length}m"
csa = "CSA: {}mm²".format(obj.csa) csa = f"CSA: {obj.csa}mm²"
image = Image.new("RGB", size, white) image = Image.new("RGB", size, white)
logo = Image.open("static/imgs/square_logo.png") logo = Image.open("static/imgs/square_logo.png")

View File

@@ -2,9 +2,7 @@ from django.conf import settings
import django import django
import pytest import pytest
from django.core.management import call_command from django.core.management import call_command
from RIGS.models import VatRate, Profile from RIGS.models import VatRate
import random
from django.db import connection
from PyRIGS.tests import pages from PyRIGS.tests import pages
import os import os
from selenium import webdriver from selenium import webdriver

29
package-lock.json generated
View File

@@ -10173,12 +10173,19 @@
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
}, },
"copy-props": { "copy-props": {
"version": "2.0.4", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz", "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz",
"integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==", "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==",
"requires": { "requires": {
"each-props": "^1.3.0", "each-props": "^1.3.2",
"is-plain-object": "^2.0.1" "is-plain-object": "^5.0.0"
},
"dependencies": {
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
}
} }
}, },
"core-util-is": { "core-util-is": {
@@ -11171,9 +11178,9 @@
} }
}, },
"follow-redirects": { "follow-redirects": {
"version": "1.14.6", "version": "1.14.7",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
"integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==", "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==",
"dev": true "dev": true
}, },
"for-in": { "for-in": {
@@ -12892,9 +12899,9 @@
} }
}, },
"marked": { "marked": {
"version": "4.0.8", "version": "4.0.10",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.8.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz",
"integrity": "sha512-dkpJMIlJpc833hbjjg8jraw1t51e/eKDoG8TFOgc5O0Z77zaYKigYekTDop5AplRoKFGIaoazhYEhGkMtU3IeA==" "integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw=="
}, },
"matchdep": { "matchdep": {
"version": "2.0.0", "version": "2.0.0",

View File

@@ -6,3 +6,6 @@ from reversion.admin import VersionAdmin
admin.site.register(models.TrainingCategory, VersionAdmin) admin.site.register(models.TrainingCategory, VersionAdmin)
admin.site.register(models.TrainingItem, VersionAdmin) admin.site.register(models.TrainingItem, VersionAdmin)
admin.site.register(models.TrainingLevel, VersionAdmin) admin.site.register(models.TrainingLevel, VersionAdmin)
admin.site.register(models.TrainingItemQualification, VersionAdmin)
admin.site.register(models.TrainingLevelQualification, VersionAdmin)
admin.site.register(models.TrainingLevelRequirement, VersionAdmin)

View File

@@ -2,4 +2,4 @@ from PyRIGS.decorators import user_passes_test_with_403
def has_perm_or_supervisor(perm, login_url=None, oembed_view=None): def has_perm_or_supervisor(perm, login_url=None, oembed_view=None):
return user_passes_test_with_403(lambda u: (hasattr(u, 'as_trainee') and u.as_trainee.is_supervisor) or u.has_perm(perm), login_url=login_url, oembed_view=oembed_view) return user_passes_test_with_403(lambda u: (hasattr(u, 'is_supervisor') and u.is_supervisor) or u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)

View File

@@ -1,15 +1,9 @@
from django import forms from django import forms
from datetime import date
from training import models from training import models
from RIGS.models import Profile from RIGS.models import Profile
class SessionLogForm(forms.Form):
pass
class QualificationForm(forms.ModelForm): class QualificationForm(forms.ModelForm):
class Meta: class Meta:
model = models.TrainingItemQualification model = models.TrainingItemQualification
@@ -17,7 +11,7 @@ class QualificationForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
pk = kwargs.pop('pk', None) pk = kwargs.pop('pk', None)
super(QualificationForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['trainee'].initial = Profile.objects.get(pk=pk) self.fields['trainee'].initial = Profile.objects.get(pk=pk)
self.fields['date'].widget.format = '%Y-%m-%d' self.fields['date'].widget.format = '%Y-%m-%d'
@@ -45,5 +39,5 @@ class RequirementForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
pk = kwargs.pop('pk', None) pk = kwargs.pop('pk', None)
super(RequirementForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['level'].initial = models.TrainingLevel.objects.get(pk=pk) self.fields['level'].initial = models.TrainingLevel.objects.get(pk=pk)

View File

@@ -48,6 +48,7 @@ class Command(BaseCommand):
if profile: if profile:
self.id_map[child.find('ID').text] = profile.pk self.id_map[child.find('ID').text] = profile.pk
print(f"Found existing user {profile}, matching data")
tally[0] += 1 tally[0] += 1
else: else:
# PYTHONIC, BABY # PYTHONIC, BABY
@@ -59,6 +60,7 @@ class Command(BaseCommand):
initials=initials) initials=initials)
self.id_map[child.find('ID').text] = new_profile.pk self.id_map[child.find('ID').text] = new_profile.pk
tally[1] += 1 tally[1] += 1
print(f"No match found, creating new user {new_profile}")
except AttributeError: # W.T.F except AttributeError: # W.T.F
print("Trainee #{} is FUBAR".format(child.find('ID').text)) print("Trainee #{} is FUBAR".format(child.find('ID').text))
@@ -225,17 +227,16 @@ class Command(BaseCommand):
for child in root: for child in root:
try: try:
if child.find('Training_x0020_Level_x0020_ID') is None: trainee = Profile.objects.get(pk=self.id_map[child.find('Member_x0020_ID').text]) if child.find('Member_x0020_ID') is not None else False
print('Training Level Qualification #{} does not qualify in any level. How?'.format(child.find('ID').text)) level = models.TrainingLevel.objects.get(pk=int(child.find('Training_x0020_Level_x0020_ID').text)) if child.find('Training_x0020_Level_x0020_ID') is not None else False
if trainee and level:
obj, created = models.TrainingLevelQualification.objects.update_or_create(pk=int(child.find('ID').text),
trainee=trainee,
level=level)
else:
print('Training Level Qualification #{} failed to import. Trainee: {} and Level: {}'.format(child.find('ID').text, trainee, level))
continue continue
if child.find('Member_x0020_ID') is None:
print('Training Level Qualification #{} does not qualify anyone. How?!'.format(child.find('ID').text))
continue
obj, created = models.TrainingLevelQualification.objects.update_or_create(
pk=int(child.find('ID').text),
trainee=Profile.objects.get(pk=self.id_map[child.find('Member_x0020_ID').text]),
level=models.TrainingLevel.objects.get(pk=int(child.find('Training_x0020_Level_x0020_ID').text))
)
if child.find('Date_x0020_Level_x0020_Awarded') is not None: if child.find('Date_x0020_Level_x0020_Awarded') is not None:
obj.confirmed_on = make_aware(datetime.datetime.strptime(child.find('Date_x0020_Level_x0020_Awarded').text.split('T')[0], "%Y-%m-%d")) obj.confirmed_on = make_aware(datetime.datetime.strptime(child.find('Date_x0020_Level_x0020_Awarded').text.split('T')[0], "%Y-%m-%d"))

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.11 on 2022-01-05 12:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('training', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='traininglevel',
options={'ordering': ['department', 'level']},
),
]

View File

@@ -1,13 +1,11 @@
from django.db import models
from RIGS.models import RevisionMixin, Profile from RIGS.models import RevisionMixin, Profile
from reversion import revisions as reversion from reversion import revisions as reversion
from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.safestring import SafeData, mark_safe
@reversion.register(for_concrete_model=False) @reversion.register(for_concrete_model=False, fields=[], follow=["qualifications_obtained", "level_qualifications"])
class Trainee(Profile, RevisionMixin): class Trainee(Profile, RevisionMixin):
class Meta: class Meta:
proxy = True proxy = True
@@ -23,13 +21,6 @@ class Trainee(Profile, RevisionMixin):
.exclude(level__department=TrainingLevel.HAULAGE) \ .exclude(level__department=TrainingLevel.HAULAGE) \
.exclude(level__department__isnull=True).exists() .exclude(level__department__isnull=True).exists()
@property
def is_supervisor(self):
return self.level_qualifications.exclude(confirmed_on=None).select_related('level') \
.filter(level__level__gte=TrainingLevel.SUPERVISOR) \
.exclude(level__department=TrainingLevel.HAULAGE) \
.exclude(level__department__isnull=True).exists()
@property @property
def is_driver(self): def is_driver(self):
return self.level_qualifications.all().exclude(confirmed_on=None).select_related('level').filter(level__department=TrainingLevel.HAULAGE).exists() return self.level_qualifications.all().exclude(confirmed_on=None).select_related('level').filter(level__department=TrainingLevel.HAULAGE).exists()
@@ -43,18 +34,23 @@ class Trainee(Profile, RevisionMixin):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('trainee_detail', kwargs={'pk': self.pk}) return reverse('trainee_detail', kwargs={'pk': self.pk})
@property
def display_id(self):
return str(self)
class TrainingCategory(models.Model): class TrainingCategory(models.Model):
reference_number = models.IntegerField(unique=True) reference_number = models.IntegerField(unique=True)
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
def __str__(self): def __str__(self):
return "{}. {}".format(self.reference_number, self.name) return f"{self.reference_number}. {self.name}"
class Meta: class Meta:
verbose_name_plural = 'Training Categories' verbose_name_plural = 'Training Categories'
@reversion.register
class TrainingItem(models.Model): class TrainingItem(models.Model):
reference_number = models.IntegerField() reference_number = models.IntegerField()
category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.CASCADE) category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.CASCADE)
@@ -63,10 +59,10 @@ class TrainingItem(models.Model):
@property @property
def display_id(self): def display_id(self):
return "{}.{}".format(self.category.reference_number, self.reference_number) return f"{self.category.reference_number}.{self.reference_number}"
def __str__(self): def __str__(self):
name = "{} {}".format(self.display_id, self.name) name = f"{self.display_id} {self.name}"
if not self.active: if not self.active:
name += " (inactive)" name += " (inactive)"
return name return name
@@ -80,8 +76,8 @@ class TrainingItem(models.Model):
ordering = ['category__reference_number', 'reference_number'] ordering = ['category__reference_number', 'reference_number']
@reversion.register(follow=['trainee']) @reversion.register
class TrainingItemQualification(models.Model): class TrainingItemQualification(models.Model, RevisionMixin):
STARTED = 0 STARTED = 0
COMPLETE = 1 COMPLETE = 1
PASSED_OUT = 2 PASSED_OUT = 2
@@ -107,13 +103,13 @@ class TrainingItemQualification(models.Model):
return str("{} in {}".format(self.get_depth_display(), self.item)) return str("{} in {}".format(self.get_depth_display(), self.item))
@classmethod @classmethod
def get_colour_from_depth(obj, depth): def get_colour_from_depth(cls, obj, depth):
if depth == 0: if depth == 0:
return "warning" return "warning"
elif depth == 1: if depth == 1:
return "success" return "success"
else:
return "info" return "info"
def get_absolute_url(self): def get_absolute_url(self):
return reverse('trainee_item_detail', kwargs={'pk': self.trainee.pk}) return reverse('trainee_item_detail', kwargs={'pk': self.trainee.pk})
@@ -152,20 +148,23 @@ class TrainingLevel(models.Model, RevisionMixin):
prerequisite_levels = models.ManyToManyField('self', related_name='prerequisites', symmetrical=False, blank=True) prerequisite_levels = models.ManyToManyField('self', related_name='prerequisites', symmetrical=False, blank=True)
icon = models.CharField(null=True, blank=True, max_length=20) icon = models.CharField(null=True, blank=True, max_length=20)
class Meta:
ordering = ["department", "level"]
@property @property
def department_colour(self): def department_colour(self):
if self.department == self.SOUND: if self.department == self.SOUND:
return "info" return "info"
elif self.department == self.LIGHTING: if self.department == self.LIGHTING:
return "dark" return "dark"
elif self.department == self.POWER: if self.department == self.POWER:
return "danger" return "danger"
elif self.department == self.RIGGING: if self.department == self.RIGGING:
return "warning" return "warning"
elif self.department == self.HAULAGE: if self.department == self.HAULAGE:
return "light" return "light"
else:
return "primary" return "primary"
def get_requirements_of_depth(self, depth): def get_requirements_of_depth(self, depth):
return self.requirements.filter(depth=depth) return self.requirements.filter(depth=depth)
@@ -196,8 +195,8 @@ class TrainingLevel(models.Model, RevisionMixin):
if len(needed_qualifications) > 0: if len(needed_qualifications) > 0:
return int(relavant_qualifications / float(len(needed_qualifications)) * 100) return int(relavant_qualifications / float(len(needed_qualifications)) * 100)
else:
return 0 return 0
def user_has_requirements(self, user): def user_has_requirements(self, user):
has_required_items = all(TrainingItem.user_has_qualification(req.item, user, req.depth) for req in self.requirements.all()) has_required_items = all(TrainingItem.user_has_qualification(req.item, user, req.depth) for req in self.requirements.all())
@@ -224,7 +223,7 @@ class TrainingLevel(models.Model, RevisionMixin):
@property @property
def get_icon(self): def get_icon(self):
if self.icon is not None: if self.icon is not None:
icon = "<span class='fas fa-{}'></span>".format(self.icon) icon = f"<span class='fas fa-{self.icon}'></span>"
else: else:
icon = "".join([w[0] for w in str(self).split()]) icon = "".join([w[0] for w in str(self).split()])
return mark_safe("<span class='badge badge-{} badge-pill' data-toggle='tooltip' title='{}'>{}</span>".format(self.department_colour, str(self), icon)) return mark_safe("<span class='badge badge-{} badge-pill' data-toggle='tooltip' title='{}'>{}</span>".format(self.department_colour, str(self), icon))
@@ -245,7 +244,7 @@ class TrainingLevelRequirement(models.Model, RevisionMixin):
unique_together = ["level", "item"] unique_together = ["level", "item"]
@reversion.register(follow=['trainee']) @reversion.register
class TrainingLevelQualification(models.Model, RevisionMixin): class TrainingLevelQualification(models.Model, RevisionMixin):
trainee = models.ForeignKey('Trainee', related_name='level_qualifications', on_delete=models.CASCADE) trainee = models.ForeignKey('Trainee', related_name='level_qualifications', on_delete=models.CASCADE)
level = models.ForeignKey('TrainingLevel', on_delete=models.CASCADE) level = models.ForeignKey('TrainingLevel', on_delete=models.CASCADE)
@@ -258,10 +257,15 @@ class TrainingLevelQualification(models.Model, RevisionMixin):
def get_icon(self): def get_icon(self):
return self.level.get_icon return self.level.get_icon
def clean(self):
if self.level.level >= TrainingLevel.SUPERVISOR and self.level.department != TrainingLevel.HAULAGE:
self.trainee.is_supervisor = True
self.trainee.save()
def __str__(self): def __str__(self):
if self.level.is_common_competencies: if self.level.is_common_competencies:
return "{} is qualified in the {}".format(self.trainee, self.level) return f"{self.trainee} is qualified in the {self.level}"
return "{} is qualified as a {}".format(self.trainee, self.level) return f"{self.trainee} is qualified as a {self.level}"
class Meta: class Meta:
unique_together = ["trainee", "level"] unique_together = ["trainee", "level"]

View File

@@ -22,6 +22,13 @@
{% block content %} {% block content %}
{% if form.errors %} {% if form.errors %}
{% include 'form_errors.html' %} {% include 'form_errors.html' %}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script>
//Has to be done here or the pickers disappear on modal error
$('document').ready(function(){
$(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))});
});
</script>
{% endif %} {% endif %}
<form id="requirement-form" action="{{ form.action|default:request.path }}" method="post">{% csrf_token %} <form id="requirement-form" action="{{ form.action|default:request.path }}" method="post">{% csrf_token %}
{% render_field form.level|attr:'hidden' value=form.level.initial %} {% render_field form.level|attr:'hidden' value=form.level.initial %}

View File

@@ -42,7 +42,7 @@
</div> </div>
<div class="form-group form-row"> <div class="form-group form-row">
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label> <label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
<select name="supervisor" id="supervisor_id" class="form-control selectpicker custom-select col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required> <select name="supervisor" id="supervisor_id" class="form-control selectpicker custom-select col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials&filters=is_supervisor" required>
{% if object.supervisor %} {% if object.supervisor %}
<option value="{{object.supervisor.pk}}" selected>{{object.supervisor}}</option> <option value="{{object.supervisor.pk}}" selected>{{object.supervisor}}</option>
{% endif %} {% endif %}

View File

@@ -44,7 +44,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if request.user.as_trainee.is_supervisor or perms.training.add_traininglevelrequirement %} {% if request.user.is_supervisor or perms.training.add_traininglevelrequirement %}
<div class="col-sm-12 text-right pr-0"> <div class="col-sm-12 text-right pr-0">
<a type="button" class="btn btn-success mb-3" href="{% url 'add_requirement' pk=object.pk %}" id="requirement_button"> <a type="button" class="btn btn-success mb-3" href="{% url 'add_requirement' pk=object.pk %}" id="requirement_button">
<span class="fas fa-plus"></span> Add New Requirement <span class="fas fa-plus"></span> Add New Requirement
@@ -79,9 +79,9 @@
{% endfor %} {% endfor %}
<tr><th colspan="3" class="text-center">{{object}}</th></tr> <tr><th colspan="3" class="text-center">{{object}}</th></tr>
<tr> <tr>
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.as_trainee.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td> <td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.as_trainee.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td> <td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.as_trainee.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td> <td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -1,9 +1,9 @@
{% extends 'base_training.html' %} {% extends 'base_training.html' %}
{% load markdown_tags %} {% load markdown_tags %}
{% load get_supervisor from tags %}
{% block content %} {% block content %}
{% if request.user.is_staff %}
<div class="alert alert-info" role="alert"> <div class="alert alert-info" role="alert">
<p>Please Note:</p> <p>Please Note:</p>
<ul> <ul>
@@ -13,10 +13,17 @@
</ul> </ul>
<sup>Correct as of 3rd September 2021, check the Training Policy.</sup> <sup>Correct as of 3rd September 2021, check the Training Policy.</sup>
</div> </div>
{% endif %}
{% for level in object_list %} {% for level in object_list %}
<div class="card mb-2"> {% ifchanged level.department %}
<div class="card-header"><a href="{{level.get_absolute_url}}">{{level}}</a></div> {% if not forloop.first %}</div>{% endif %}
<div class="card-body">{{level.description|markdown}}</div> <div class="card-group">
{% endifchanged %}
<div class="card mb-2 border-{{level.department_colour}}">
<div class="card-body">
<h3 class="card-title"><a href="{{level.get_absolute_url}}">{{level}}</a></h2>
{{level.description|markdown}}
</div>
</div> </div>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@@ -1,4 +1,4 @@
{% if request.user.as_trainee.is_supervisor or perms.training.add_trainingitemqualification %} {% if request.user.is_supervisor or perms.training.add_trainingitemqualification %}
<a type="button" class="btn btn-success" href="{% url 'add_qualification' object.pk %}" id="add_record"> <a type="button" class="btn btn-success" href="{% url 'add_qualification' object.pk %}" id="add_record">
<span class="fas fa-plus"></span> Add New Training Record <span class="fas fa-plus"></span> Add New Training Record
</a> </a>

View File

@@ -19,7 +19,7 @@
<th>Date</th> <th>Date</th>
<th>Supervisor</th> <th>Supervisor</th>
<th>Notes</th> <th>Notes</th>
{% if request.user.as_trainee.is_supervisor or perms.training.change_trainingitemqualification %} {% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
<th></th> <th></th>
{% endif %} {% endif %}
</tr> </tr>
@@ -32,7 +32,7 @@
<td>{{ object.date }}</td> <td>{{ object.date }}</td>
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td> <td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
<td>{{ object.notes }}</td> <td>{{ object.notes }}</td>
{% if request.user.as_trainee.is_supervisor or perms.training.change_trainingitemqualification %} {% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
<td>{% button 'edit' 'edit_qualification' trainee.pk %}</td> <td>{% button 'edit' 'edit_qualification' trainee.pk %}</td>
{% endif %} {% endif %}
</tr> </tr>
@@ -46,4 +46,9 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row">
<div class="col text-right">
{% include 'partials/last_edited.html' with target="trainee_history" object=trainee %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -33,11 +33,6 @@ def colour_from_depth(depth):
return models.TrainingItemQualification.get_colour_from_depth(depth) return models.TrainingItemQualification.get_colour_from_depth(depth)
@register.filter
def get_supervisor(tech):
return models.TrainingLevel.objects.get(department=tech.department, level=models.TrainingLevel.SUPERVISOR)
@register.filter @register.filter
def get_levels_of_depth(trainee, level): def get_levels_of_depth(trainee, level):
return trainee.level_qualifications.all().exclude(confirmed_on=None).exclude(level__department=models.TrainingLevel.HAULAGE).select_related('level').filter(level__level=level) return trainee.level_qualifications.all().exclude(confirmed_on=None).exclude(level__department=models.TrainingLevel.HAULAGE).select_related('level').filter(level__level=level)

View File

@@ -0,0 +1,37 @@
import pytest
from training import models
from RIGS.models import Profile
@pytest.fixture
def trainee(db):
trainee = Profile.objects.create(username="trainee", first_name="Train", last_name="EE",
initials="TRN",
email="trainee@example.com", is_active=True, is_approved=True)
yield trainee
trainee.delete()
@pytest.fixture
def supervisor(db):
supervisor = Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor",
initials="SV",
email="supervisor@example.com", is_supervisor=True, is_active=True, is_approved=True)
yield supervisor
supervisor.delete()
@pytest.fixture
def training_item(db):
training_category = models.TrainingCategory.objects.create(reference_number=1, name="The Basics")
training_item = models.TrainingItem.objects.create(category=training_category, reference_number=1, name="How Not To Die")
yield training_item
training_category.delete()
training_item.delete()
@pytest.fixture
def level(db):
level = models.TrainingLevel.objects.create(description="There is no description.", level=models.TrainingLevel.TECHNICIAN)
yield level
level.delete()

View File

@@ -0,0 +1,42 @@
from django.urls import reverse
from pypom import Region
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from PyRIGS.tests import regions
from PyRIGS.tests.pages import BasePage, FormPage
class TraineeDetail(BasePage):
URL_TEMPLATE = 'training/trainee/{pk}'
_name_selector = (By.XPATH, '//h2')
@property
def page_name(self):
return self.find_element(*self._name_selector).text
class AddQualification(FormPage):
URL_TEMPLATE = 'training/trainee/{pk}/add_qualification/'
_item_selector = (By.XPATH, '//div[1]/form/div[1]/div')
_supervisor_selector = (By.XPATH, '//div[1]/form/div[3]/div')
form_items = {
'depth': (regions.SingleSelectPicker, (By.ID, 'id_depth')),
'date': (regions.DatePicker, (By.ID, 'id_date')),
'notes': (regions.TextBox, (By.ID, 'id_notes')),
}
@property
def item_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._item_selector))
@property
def supervisor_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._supervisor_selector))
@property
def success(self):
return 'add' not in self.driver.current_url

View File

@@ -0,0 +1,46 @@
import datetime
import time
from django.utils import timezone
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait
from PyRIGS.tests.base import AutoLoginTest, screenshot_failure_cls, assert_times_almost_equal
from PyRIGS.tests.pages import animation_is_finished
from training import models
from training.tests import pages
def test_add_qualification(logged_in_browser, live_server, trainee, supervisor, training_item):
page = pages.AddQualification(logged_in_browser.driver, live_server.url, pk=trainee.pk).open()
# assert page.name in str(trainee)
page.depth = "Training Started"
page.date = date = datetime.date(1984, 1, 1)
page.notes = "A note"
time.sleep(2) # Slow down for javascript
page.item_selector.toggle()
assert page.item_selector.is_open
page.item_selector.search(training_item.name)
time.sleep(2) # Slow down for javascript
page.item_selector.set_option(training_item.name, True)
assert page.item_selector.options[0].selected
page.item_selector.toggle()
page.supervisor_selector.toggle()
assert page.supervisor_selector.is_open
page.supervisor_selector.search(supervisor.name[:-6])
time.sleep(2) # Slow down for javascript
assert page.supervisor_selector.options[0].selected
page.supervisor_selector.toggle()
page.submit()
assert page.success
qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item)
assert qualification.supervisor.pk == supervisor.pk
assert qualification.date == date
assert qualification.notes == "A note"
assert qualification.depth == models.TrainingItemQualification.STARTED

View File

@@ -1,5 +1,38 @@
import datetime
import pytest import pytest
from django.utils import timezone
from django.urls import reverse
from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains
pytestmark = pytest.mark.django_db from training import models
def test_add_qualification(admin_client, trainee, admin_user):
url = reverse('add_qualification', kwargs={'pk': trainee.pk})
date = (timezone.now() + datetime.timedelta(days=3)).strftime("%Y-%m-%d")
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': trainee.pk})
assertFormError(response, 'form', 'date', 'Qualification date may not be in the future')
assertFormError(response, 'form', 'supervisor', 'One may not supervise oneself...')
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': admin_user.pk})
assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...')
def test_add_requirement(admin_client, level):
url = reverse('add_requirement', kwargs={'pk': level.pk})
response = admin_client.post(url)
assertContains(response, level.pk)
def test_trainee_detail(admin_client, trainee, admin_user):
url = reverse('trainee_detail', kwargs={'pk': admin_user.pk})
response = admin_client.get(url)
assertContains(response, "Your Training Record")
assertContains(response, "No qualifications in any levels")
url = reverse('trainee_detail', kwargs={'pk': trainee.pk})
response = admin_client.get(url)
assertNotContains(response, "Your")
name = trainee.first_name + " " + trainee.last_name
assertContains(response, f"{name}'s Training Record")

View File

@@ -13,7 +13,7 @@ urlpatterns = [
has_perm_or_supervisor('RIGS.view_profile')(views.TraineeDetail.as_view()), has_perm_or_supervisor('RIGS.view_profile')(views.TraineeDetail.as_view()),
name='trainee_detail'), name='trainee_detail'),
path('trainee/<int:pk>/history', has_perm_or_supervisor('RIGS.view_profile')(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think) path('trainee/<int:pk>/history', has_perm_or_supervisor('RIGS.view_profile')(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
path('trainee/<int:pk>/add_qualification/', has_perm_or_supervisor('training.add_trainingitemqualificaiton')(views.AddQualification.as_view()), path('trainee/<int:pk>/add_qualification/', has_perm_or_supervisor('training.add_trainingitemqualification')(views.AddQualification.as_view()),
name='add_qualification'), name='add_qualification'),
path('trainee/<int:pk>/edit_qualification/', has_perm_or_supervisor('training.change_trainingitemqualification')(views.EditQualification.as_view()), path('trainee/<int:pk>/edit_qualification/', has_perm_or_supervisor('training.change_trainingitemqualification')(views.EditQualification.as_view()),
name='edit_qualification'), name='edit_qualification'),

View File

@@ -1,14 +1,13 @@
import reversion import reversion
from django.shortcuts import render
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.views import generic from django.views import generic
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin
from training import models, forms
from django.utils import timezone from django.utils import timezone
from django.db import transaction from django.db import transaction
from django.db.models import Q, Count, OuterRef, F, Subquery, Window from django.db.models import Q, Count
from PyRIGS.views import is_ajax, ModalURLMixin
from training import models, forms
from users import views from users import views
@@ -17,7 +16,7 @@ class ItemList(generic.ListView):
model = models.TrainingItem model = models.TrainingItem
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ItemList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["page_title"] = "Training Items" context["page_title"] = "Training Items"
context["categories"] = models.TrainingCategory.objects.all() context["categories"] = models.TrainingCategory.objects.all()
return context return context
@@ -31,7 +30,7 @@ class TraineeDetail(views.ProfileDetail):
return self.model.objects.prefetch_related('qualifications_obtained') return self.model.objects.prefetch_related('qualifications_obtained')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(TraineeDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if self.request.user.pk == self.object.pk: if self.request.user.pk == self.object.pk:
context["page_title"] = "Your Training Record" context["page_title"] = "Your Training Record"
else: else:
@@ -98,17 +97,17 @@ class TraineeList(generic.ListView):
def get_queryset(self): def get_queryset(self):
q = self.request.GET.get('q', "") q = self.request.GET.get('q', "")
filter = Q(first_name__icontains=q) | Q(last_name__icontains=q) | Q(initials__icontains=q) filt = Q(first_name__icontains=q) | Q(last_name__icontains=q) | Q(initials__icontains=q)
# try and parse an int # try and parse an int
try: try:
val = int(q) val = int(q)
filter = filter | Q(pk=val) filt = filt | Q(pk=val)
except: # noqa except: # noqa
# not an integer # not an integer
pass pass
return self.model.objects.filter(filter).annotate(num_qualifications=Count('qualifications_obtained')).order_by('-num_qualifications').prefetch_related('level_qualifications', 'qualifications_obtained', 'qualifications_obtained__item') return self.model.objects.filter(filt).annotate(num_qualifications=Count('qualifications_obtained')).order_by('-num_qualifications').prefetch_related('level_qualifications', 'qualifications_obtained', 'qualifications_obtained__item')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@@ -121,8 +120,14 @@ class AddQualification(generic.CreateView, ModalURLMixin):
model = models.TrainingItemQualification model = models.TrainingItemQualification
form_class = forms.QualificationForm form_class = forms.QualificationForm
@transaction.atomic()
@reversion.create_revision()
def form_valid(self, form, *args, **kwargs):
reversion.add_to_revision(form.cleaned_data['trainee'])
return super().form_valid(form, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AddQualification, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["depths"] = models.TrainingItemQualification.CHOICES context["depths"] = models.TrainingItemQualification.CHOICES
if is_ajax(self.request): if is_ajax(self.request):
context['override'] = "base_ajax.html" context['override'] = "base_ajax.html"
@@ -146,16 +151,22 @@ class EditQualification(generic.UpdateView):
form_class = forms.QualificationForm form_class = forms.QualificationForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EditQualification, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["depths"] = models.TrainingItemQualification.CHOICES context["depths"] = models.TrainingItemQualification.CHOICES
context['page_title'] = "Edit Qualification {} for {}".format(self.object, models.Trainee.objects.get(pk=self.kwargs['pk'])) context['page_title'] = "Edit Qualification {} for {}".format(self.object, models.Trainee.objects.get(pk=self.kwargs['pk']))
return context return context
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super(EditQualification, self).get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs['pk'] = self.kwargs['pk'] kwargs['pk'] = self.kwargs['pk']
return kwargs return kwargs
@transaction.atomic()
@reversion.create_revision()
def form_valid(self, form, *args, **kwargs):
reversion.add_to_revision(form.cleaned_data['trainee'])
return super().form_valid(form, *args, **kwargs)
class AddLevelRequirement(generic.CreateView, ModalURLMixin): class AddLevelRequirement(generic.CreateView, ModalURLMixin):
template_name = "add_level_requirement.html" template_name = "add_level_requirement.html"
@@ -163,12 +174,12 @@ class AddLevelRequirement(generic.CreateView, ModalURLMixin):
form_class = forms.RequirementForm form_class = forms.RequirementForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AddLevelRequirement, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["page_title"] = "Add Requirements to Training Level {}".format(models.TrainingLevel.objects.get(pk=self.kwargs['pk'])) context["page_title"] = "Add Requirements to Training Level {}".format(models.TrainingLevel.objects.get(pk=self.kwargs['pk']))
return context return context
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super(AddLevelRequirement, self).get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs['pk'] = self.kwargs['pk'] kwargs['pk'] = self.kwargs['pk']
return kwargs return kwargs
@@ -179,7 +190,6 @@ class AddLevelRequirement(generic.CreateView, ModalURLMixin):
@reversion.create_revision() @reversion.create_revision()
def form_valid(self, form, *args, **kwargs): def form_valid(self, form, *args, **kwargs):
reversion.add_to_revision(form.cleaned_data['level']) reversion.add_to_revision(form.cleaned_data['level'])
reversion.set_comment("Level requirement added")
return super().form_valid(form, *args, **kwargs) return super().form_valid(form, *args, **kwargs)
@@ -189,7 +199,7 @@ class RemoveRequirement(generic.DeleteView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["page_title"] = "Delete Requirement '{}' from Training Level {}?".format(self.object, self.object.level) context["page_title"] = f"Delete Requirement '{self.object}' from Training Level {self.object.level}?"
return context return context
def get_success_url(self): def get_success_url(self):
@@ -206,7 +216,13 @@ class ConfirmLevel(generic.RedirectView):
@transaction.atomic() @transaction.atomic()
@reversion.create_revision() @reversion.create_revision()
def get_redirect_url(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
level_qualification = models.TrainingLevelQualification.objects.create(trainee=models.Trainee.objects.get(pk=kwargs['pk']), level=models.TrainingLevel.objects.get(pk=kwargs['level_pk']), confirmed_by=self.request.user, confirmed_on=timezone.now()) trainee = models.Trainee.objects.get(pk=kwargs['pk'])
reversion.add_to_revision(level_qualification.trainee) level_qualification, created = models.TrainingLevelQualification.objects.get_or_create(trainee=trainee, level=models.TrainingLevel.objects.get(pk=kwargs['level_pk']))
reversion.set_user(self.request.user)
if created:
level_qualification.confirmed_by = self.request.user
level_qualification.confirmed_on = timezone.now()
level_qualification.save()
reversion.add_to_revision(trainee)
return reverse_lazy('trainee_detail', kwargs={'pk': kwargs['pk']}) return reverse_lazy('trainee_detail', kwargs={'pk': kwargs['pk']})

View File

@@ -146,7 +146,7 @@ class RIGSVersionTestCase(TestCase):
self.assertFalse(current_version.changes.fields_changed) self.assertFalse(current_version.changes.fields_changed)
self.assertTrue(current_version.changes.anything_changed) self.assertTrue(current_version.changes.anything_changed)
self.assertTrue(diffs[0].old is None) self.assertIsNone(diffs[0].old)
self.assertEqual(diffs[0].new.name, "TI I1") self.assertEqual(diffs[0].new.name, "TI I1")
# Edit the item # Edit the item
@@ -188,4 +188,4 @@ class RIGSVersionTestCase(TestCase):
self.assertTrue(current_version.changes.anything_changed) self.assertTrue(current_version.changes.anything_changed)
self.assertEqual(diffs[0].old.name, "New Name") self.assertEqual(diffs[0].old.name, "New Name")
self.assertTrue(diffs[0].new is None) self.assertIsNone(diffs[0].new)

View File

@@ -1,5 +1,3 @@
import logging
from diff_match_patch import diff_match_patch from diff_match_patch import diff_match_patch
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@@ -7,20 +5,52 @@ from django.db.models import EmailField, IntegerField, TextField, CharField, Boo
from django.utils.functional import cached_property from django.utils.functional import cached_property
from reversion.models import Version, VersionQuerySet from reversion.models import Version, VersionQuerySet
from RIGS import models
from training.models import Trainee
logger = logging.getLogger('tec.pyrigs') class RevisionMixin:
@property
def is_first_version(self):
versions = Version.objects.get_for_object(self)
return len(versions) == 1
@property
def current_version(self):
version = Version.objects.get_for_object(self).select_related('revision').first()
return version
@property
def last_edited_at(self):
version = self.current_version
if version is None:
return None
return version.revision.date_created
@property
def last_edited_by(self):
version = self.current_version
if version is None:
return None
return version.revision.user
@property
def current_version_id(self):
version = self.current_version
if version is None:
return None
return "V{0} | R{1}".format(version.pk, version.revision.pk)
@property
def date_created(self):
return self.current_version.revision.date_created
class FieldComparison(object): class FieldComparison:
def __init__(self, field=None, old=None, new=None): def __init__(self, field=None, old=None, new=None):
self.field = field self.field = field
self._old = old self._old = old
self._new = new self._new = new
def display_value(self, value): def display_value(self, value):
if (isinstance(self.field, IntegerField) or isinstance(self.field, CharField)) and self.field.choices is not None and len(self.field.choices) > 0: if isinstance(self.field, (IntegerField, CharField)) and self.field.choices is not None and len(self.field.choices) > 0:
choice = [x[1] for x in self.field.choices if x[0] == value] choice = [x[1] for x in self.field.choices if x[0] == value]
if len(choice) > 0: if len(choice) > 0:
return choice[0] return choice[0]
@@ -71,8 +101,8 @@ class FieldComparison(object):
return outputDiffs return outputDiffs
class ModelComparison(object): class ModelComparison:
def __init__(self, old=None, new=None, version=None, follow=False, excluded_keys=[]): def __init__(self, old=None, new=None, version=None, follow=False, excluded_keys=['date_joined']):
# recieves two objects of the same model, and compares them. Returns an array of FieldCompare objects # recieves two objects of the same model, and compares them. Returns an array of FieldCompare objects
try: try:
self.fields = old._meta.get_fields() self.fields = old._meta.get_fields()
@@ -117,12 +147,13 @@ class ModelComparison(object):
@cached_property @cached_property
def item_changes(self): def item_changes(self):
from RIGS.models import EventAuthorisation
if self.follow and self.version.object is not None: if self.follow and self.version.object is not None:
item_type = ContentType.objects.get_for_model(self.version.object) item_type = ContentType.objects.get_for_model(self.version.object)
old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type) old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type)
new_item_versions = self.version.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(models.EventAuthorisation)) new_item_versions = self.version.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(EventAuthorisation))
comparisonParams = {'excluded_keys': ['id', 'event', 'order', 'checklist', 'level', '_order', 'last_login']} comparisonParams = {'excluded_keys': ['id', 'event', 'order', 'checklist', 'level', '_order', 'date_joined']}
# Build some dicts of what we have # Build some dicts of what we have
item_dict = {} # build a list of items, key is the item_pk item_dict = {} # build a list of items, key is the item_pk
@@ -170,7 +201,7 @@ class RIGSVersionManager(VersionQuerySet):
for model in model_array: for model in model_array:
content_types.append(ContentType.objects.get_for_model(model)) content_types.append(ContentType.objects.get_for_model(model))
return self.filter(content_type__in=content_types).select_related("revision").order_by( return self.filter(content_type__in=content_types).select_related("revision",).order_by(
"-revision__date_created") "-revision__date_created")