Compare commits
9 Commits
f8ee1ffb0b
...
imgbot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5dc879733 | ||
|
c537118037
|
|||
|
466a9a9693
|
|||
| d25381b2de | |||
|
|
eaf891daf7 | ||
|
|
801d2e8a7d | ||
|
|
3d329219b8 | ||
|
2ddc8923ba
|
|||
|
276a86c5be
|
5
Pipfile
@@ -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
@@ -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": [
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
default_app_config = 'RIGS.apps.RIGSAppConfig'
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
18
RIGS/migrations/0044_profile_is_supervisor.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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?")
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 6.3 MiB After Width: | Height: | Size: 5.4 MiB |
|
Before Width: | Height: | Size: 852 KiB After Width: | Height: | Size: 852 KiB |
@@ -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'
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
default_app_config = 'assets.apps.AssetsAppConfig'
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
17
assets/migrations/0022_alter_cabletype_unique_together.py
Normal 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')},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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})
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 18 KiB |
@@ -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>
|
||||||
|
|||||||
@@ -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')))
|
||||||
|
|||||||
@@ -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...
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
17
training/migrations/0002_alter_traininglevel_options.py
Normal 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']},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
37
training/tests/conftest.py
Normal 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()
|
||||||
@@ -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
|
||||||
|
|||||||
46
training/tests/test_interaction.py
Normal 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
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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']})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||