Compare commits

..

115 Commits

Author SHA1 Message Date
f8ee1ffb0b Mmm 'responsive' tables 2022-01-04 22:37:42 +00:00
af963ac0eb Only show crown on first page 2022-01-04 22:31:49 +00:00
42ea30931e Fix spelling mistake 2022-01-04 22:26:48 +00:00
b8dc56d129 Fix for new django 2022-01-04 20:31:22 +00:00
f333aa2c18 >.> 2022-01-04 20:20:02 +00:00
9bc2c2b509 Squash migrations 2022-01-04 20:09:04 +00:00
9abb76d4fa That didn't work. Skip those for now 2022-01-04 20:04:54 +00:00
e6af66a964 Potentially fix tests 2022-01-04 19:55:11 +00:00
6c484b984c Revert "Let's try this"
This reverts commit 2a66342cd3.
2022-01-04 19:05:06 +00:00
2a66342cd3 Let's try this 2022-01-04 18:59:50 +00:00
7bd32b3ac5 My kingdom for a space 2022-01-04 18:43:10 +00:00
2a61ee2896 Hmmm 2022-01-04 18:36:27 +00:00
3bac6050f1 Fix broken js in assetaudit
Weird that didn't get picked up before...
2022-01-04 12:36:28 +00:00
7aa37c225d Fix sample data command 2022-01-04 12:28:25 +00:00
215c51e718 Merge branch 'master' into training 2022-01-04 12:08:39 +00:00
fbe4d7271f pep8 2022-01-04 12:08:17 +00:00
2a2f010028 Permissions work 2022-01-04 12:04:38 +00:00
6b19d0e8b8 Fix has_required_levels logic being backward 2022-01-03 15:31:54 +00:00
3e8cfe4f11 Repair confirmation logic 2022-01-03 15:28:17 +00:00
7a70270dfd Add ability to view other users progress on a level
That's kind of important huh :p
2022-01-03 14:59:30 +00:00
5160eb7f78 Add confirm button stuff 2022-01-03 14:38:43 +00:00
99e05d91bb Simpler training level list display 2022-01-02 20:55:32 +00:00
f094ace862 Make level description a text field 2022-01-02 20:53:33 +00:00
945fb393c0 Display prerequisite item requirements on level detail 2022-01-02 20:31:23 +00:00
c6157d3e2b Various fixes 2022-01-02 19:15:44 +00:00
2767777d0e Add ability to edit past training records 2022-01-02 15:39:10 +00:00
19e6585e26 Fix importer not working for notes 2022-01-02 15:03:11 +00:00
747575b968 Add ability to search detailed training record 2022-01-02 14:52:07 +00:00
046d0e461d Fix is_supervisor returning true if user has any levels
Whoops!
2022-01-02 11:15:03 +00:00
b73b8401b6 Add ordering to training level qualification 2022-01-02 11:06:58 +00:00
3fe388af26 Fix importer trying to set pk for qualifications
This doesn't work because of the old DB structure
2022-01-02 11:05:08 +00:00
22dc83d595 Do not display qualified levels also as started 2022-01-02 10:37:48 +00:00
70d4c42676 Much versioning work 2022-01-01 19:53:03 +00:00
0727a23236 Some reversion fiddling 2021-12-31 18:23:38 +00:00
f0b3a6daf3 Filter for active training items
Can't easily filter by supervisor, its not a database field, argh...
2021-12-29 13:07:30 +00:00
3c5f6da363 Fix selectpickers disappearing on modal errors 2021-12-29 12:48:34 +00:00
ee9be86465 Do not display items on trainee detail
That's what the detailed view is for...

And definitely nowt to do with my horrifically optimised SQL
2021-12-28 22:23:17 +00:00
5554edf977 Cleanup 2021-12-28 21:58:56 +00:00
14b73f6f50 Somewhat optimised SQL on level detail 2021-12-28 21:35:21 +00:00
732affa0b2 SQL optimisation of detailed training record 2021-12-28 12:13:08 +00:00
7c830ee7e5 Fix sorting of items
W.T.F past self. Char field for a reference number?!
2021-12-28 11:46:52 +00:00
d47d00d79b Rework item list display 2021-12-28 11:36:46 +00:00
3b5b3b84d4 Significant improvements to level list
Added search
Ordered by qualification count
Added display for technician qualifications
2021-12-27 14:59:30 +00:00
aa8be6a6d0 Rework level list display 2021-12-23 13:29:04 +00:00
640362c203 Importer sets up level heirarchy 2021-12-23 13:28:56 +00:00
71a8823ac2 Markdown support on level desc 2021-12-23 11:31:44 +00:00
c6de3dc9e2 Merge branch 'master' into training 2021-12-22 21:52:04 +00:00
8696bf5d94 SQL query optimisation 2021-12-22 11:09:54 +00:00
0d9bf89180 Merge branch 'master' into training
# Conflicts:
#	templates/base.html
2021-12-21 19:53:17 +00:00
f4f2fbdc03 Tweak training level display 2021-12-21 19:07:39 +00:00
67aaada9e8 Import confirmation date for training level qualifications 2021-12-21 19:05:25 +00:00
fcae39c93c Add constraint that training items must have unique reference numbers 2021-12-21 15:51:44 +00:00
d1970edfb3 Change display of 'users with this level' 2021-12-21 15:12:11 +00:00
ce5efff268 Initial work on requirements importer 2021-12-21 13:38:00 +00:00
522837c64e Importer works. Doesn't yet compensate for crap data quality. 2021-10-27 16:42:52 +01:00
e78decdf92 finshed inport old db untested 2021-10-27 14:04:01 +01:00
84a3c9db24 made database importer untested 2021-10-27 13:18:28 +01:00
280a1d9604 begin work on perms 2021-10-22 16:13:08 +01:00
a184bbfa26 Much template wrangling 2021-10-22 15:57:20 +01:00
4416e5bfcb Add RevisionMixin in the right places 2021-10-20 21:15:59 +01:00
21276bcca0 You may not confirm your own training 2021-10-20 21:06:59 +01:00
bc465d67e9 Convert requirement addition to a modal 2021-10-20 21:02:19 +01:00
a644735cd6 Merge branch 'master' into training 2021-10-20 20:22:59 +01:00
10326f884f Fix the modal fuckery 2021-10-20 20:15:13 +01:00
0a0c9f15af Common competencies also do not count for being a supervisor 2021-10-09 10:38:40 +01:00
dbb9e3e530 Add 'is van driver' to trainee list, haulage super doesn't count as a supervisor 2021-10-09 10:29:03 +01:00
55558d1a4a Merge branch 'master' into training
# Conflicts:
#	RIGS/templates/risk_assessment_form.html
#	templates/base.html
2021-10-08 18:40:39 +01:00
081c33ebc8 Various tweaks 2021-09-13 01:07:17 +01:00
75410db752 Refactor is_supervisor 2021-09-13 00:49:02 +01:00
06c6b9a36e Change homepage links to match header ones 2021-09-12 20:26:05 +01:00
13b1cea28b Fancy training level list layout 2021-09-12 18:08:13 +01:00
cddb76bf7e Order training items by number 2021-09-08 20:44:02 +01:00
f4f1fb66a2 Leaderboard of qualifications obtained 2021-09-08 20:44:02 +01:00
d80aeca01f Add training level list
Plus various other fettling
2021-09-03 22:34:25 +01:00
45dfe2db51 Work on trainee reversion 2021-09-02 10:23:53 +01:00
de5997b9da Reversion working for training level 2021-08-29 22:19:30 +01:00
4a121964dc Start training navbar 2021-08-21 11:42:31 +01:00
df5e4c8e0a Level detail tweaking 2021-08-21 01:44:26 +01:00
3601c14ab7 Oops 2021-08-21 01:36:35 +01:00
adde6496f5 Add loads more sample training items
Hopefully the generator won't make levels with no requirements anymore now
2021-08-21 00:45:35 +01:00
ad734d94b2 Display pre pre requisite levels on level detail 2021-08-21 00:32:04 +01:00
7d3ada822d Common competencies sample data 2021-08-21 00:28:24 +01:00
732af53fda Groundwork stuff for common competencies + other fixes 2021-08-21 00:09:00 +01:00
4fb0529cc0 Modalify the training record addition form 2021-08-20 21:52:33 +01:00
aa23b1cd09 Add sharepoint link to new homepage
Good at scope, me
2021-08-20 21:52:17 +01:00
0c4228da57 Add a view for a trainee's item record 2021-08-20 14:26:32 +01:00
246a52d19e Don't try and create existing level qualifications 2021-08-20 13:48:30 +01:00
8b10aaf700 Display users with level on level detail page 2021-08-20 12:48:54 +01:00
4d0d4f02aa Generate a sample supervisor 2021-08-20 12:38:30 +01:00
af987c1ebb Make TrainingLevelRequirement the correct level of unique
Also updates generateSampleData to match
2021-08-19 18:47:38 +01:00
d406a911bb Initial refactoring of profile detail 2021-08-19 18:27:52 +01:00
63c5a68933 Goddamnit 2021-08-19 18:12:15 +01:00
66f7f830db Forgot that needed migrations generating 2021-08-19 18:08:57 +01:00
9590c2066d Validate that only supervisors may be supervisors 2021-08-19 16:19:46 +01:00
8b48b02ca7 Force trainingitemqualifications to be unique 2021-08-19 16:00:31 +01:00
68e7ec2a0d Merge branch 'master' into training 2021-08-19 15:49:16 +01:00
5779ebdf7e Merge branch 'master' into training
# Conflicts:
#	templates/base.html
2021-08-17 21:35:10 +01:00
be648c20d5 Level confirmation works 2021-07-29 23:41:35 +01:00
b6ef7c1d89 Force traininglevelqualifications to be unique 2021-07-29 23:15:09 +01:00
85f40b358a Some attempts at optimising SQL queries
New high score!
2021-07-29 22:49:27 +01:00
2698798035 Percentage complete works
Ain't half slow though!
2021-07-16 04:05:55 +01:00
dbaab5cf8c Autofire of traininglevelqualification basically works 2021-07-16 02:58:42 +01:00
0a9f82e480 Fettling with level granting logic
Untested as all of my forms broke I guess
2021-07-07 17:38:44 +01:00
54f2bd36bd UI for editing training level requirements 2021-07-06 22:10:15 +01:00
e836195fef mild polishing 2021-07-06 14:51:43 +01:00
68a424d62b Some sample data and UX work 2021-07-06 12:16:43 +01:00
5e15b8bb59 Basic UX for adding requirements to training levels 2021-07-06 11:37:04 +01:00
d26c1b535e item ui vaguely working 2021-07-06 00:09:46 +01:00
dff5ac2308 Whee broken HEAD 2021-07-05 23:24:13 +01:00
a3729fa930 Session log form work 2021-07-05 18:24:24 +01:00
458a734331 Machine switch 2021-07-01 09:50:13 +01:00
b1646d556c Start work on sample data command 2021-06-30 15:56:28 +01:00
f8624d3b7a Restructure based on actual thought put in by @mattysmith22 2021-06-30 15:17:00 +01:00
f6836fdab6 Merge branch 'master' into training 2021-06-29 17:17:48 +01:00
b3949f2903 Initial sketching 2021-06-29 17:13:36 +01:00
52 changed files with 378 additions and 660 deletions

View File

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

254
Pipfile.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,6 @@ class Profile(AbstractUser):
# 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)
dark_theme = models.BooleanField(default=False)
is_supervisor = models.BooleanField(default=False)
reversion_hide = True
@@ -57,6 +56,11 @@ class Profile(AbstractUser):
def latest_events(self):
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
def admins(cls):
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
@@ -69,7 +73,7 @@ class Profile(AbstractUser):
return self.name
class RevisionMixin:
class RevisionMixin(object):
@property
def is_first_version(self):
versions = Version.objects.get_for_object(self)
@@ -99,7 +103,7 @@ class RevisionMixin:
version = self.current_version
if version is None:
return None
return f"V{version.pk} | R{version.revision.pk}"
return "V{0} | R{1}".format(version.pk, version.revision.pk)
class Person(models.Model, RevisionMixin):
@@ -207,7 +211,7 @@ class VatRate(models.Model, RevisionMixin):
get_latest_by = 'start_at'
def __str__(self):
return f"{self.comment} {self.start_at} @ {self.as_percent}%"
return self.comment + " " + str(self.start_at) + " @ " + str(self.as_percent) + "%"
class Venue(models.Model, RevisionMixin):
@@ -347,10 +351,10 @@ class Event(models.Model, RevisionMixin):
if self.pk:
if self.is_rig:
return str("N%05d" % self.pk)
return self.pk
return "????"
else:
return self.pk
else:
return "????"
# Calculated values
"""
@@ -475,7 +479,7 @@ class Event(models.Model, RevisionMixin):
return reverse('event_detail', kwargs={'pk': self.pk})
def __str__(self):
return f"{self.display_id}: {self.name}"
return "{}: {}".format(self.display_id, self.name)
def clean(self):
errdict = {}
@@ -521,11 +525,11 @@ class EventItem(models.Model, RevisionMixin):
ordering = ['order']
def __str__(self):
return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
return "{}.{}: {} | {}".format(self.event_id, self.order, self.event.name, self.name)
@property
def activity_feed_string(self):
return f"item {self.name}"
return str("item {}".format(self.name))
@reversion.register
@@ -543,7 +547,7 @@ class EventAuthorisation(models.Model, RevisionMixin):
@property
def activity_feed_string(self):
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
return "{} (requested by {})".format(self.event.display_id, self.sent_by.initials)
class InvoiceManager(models.Manager):
@@ -671,6 +675,7 @@ class RiskAssessment(models.Model, RevisionMixin):
# 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?")
# 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,
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?")

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

After

Width:  |  Height:  |  Size: 852 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -61,45 +61,25 @@
{% block content %}
<div class="row">
<div class="col px-0">
<form id="asset-search-form" method="GET">
<div class="form-row">
<div class="col">
<div class="input-group px-1 mb-2 mb-sm-0 flex-nowrap">
{% render_field form.q|add_class:'form-control' placeholder='Enter Asset ID/Desc/Serial' %}
<label for="q" class="sr-only">Asset ID/Description/Serial Number:</label>
<span class="input-group-append">{% button 'search' id="id_search" %}</span>
</div>
</div>
</div>
<div class="form-row mt-2">
<div class="col">
<div id="category-group" class="form-group px-1" style="margin-bottom: 0;">
<label for="category" class="sr-only">Category</label>
{% 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 class="col">
<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 id="asset-search-form" method="GET" class="form-inline justify-content-end">
<div class="input-group px-1 mb-2 mb-sm-0 flex-nowrap">
{% render_field form.q|add_class:'form-control' placeholder='Enter Asset ID/Desc/Serial' %}
<label for="q" class="sr-only">Asset ID/Description/Serial Number:</label>
<span class="input-group-append">{% button 'search' id="id_search" %}</span>
</div>
<div id="category-group" class="form-group px-1" style="margin-bottom: 0;">
<label for="category" class="sr-only">Category</label>
{% 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 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 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>
<button id="filter-submit" type="submit" class="btn btn-secondary" style="width: 6em">Filter</button>
</form>
</div>
</div>

View File

@@ -180,7 +180,7 @@ class TestAssetForm(AutoLoginTest):
def test_asset_edit(self):
self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
self.assertIsNotNone(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly'))
self.assertTrue(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly') is not None)
new_description = "Big Shelf"
self.page.description = new_description
@@ -335,7 +335,7 @@ class TestAssetAudit(AutoLoginTest):
self.assertNotIn(self.asset.asset_id, self.page.assets)
def test_audit_list(self):
self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets))
self.assertEqual(len(models.Asset.objects.filter(last_audited_at=None)), len(self.page.assets))
asset_row = self.page.assets[0]
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')))

View File

@@ -105,6 +105,7 @@ def test_asset_edit(admin_client, test_asset):
def test_cable_edit(admin_client, test_cable):
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})
# TODO Can't figure out how to select the 'none' option...

View File

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

View File

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

29
package-lock.json generated
View File

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

View File

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

View File

@@ -2,4 +2,4 @@ from PyRIGS.decorators import user_passes_test_with_403
def has_perm_or_supervisor(perm, login_url=None, oembed_view=None):
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)
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)

View File

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

View File

@@ -48,7 +48,6 @@ class Command(BaseCommand):
if profile:
self.id_map[child.find('ID').text] = profile.pk
print(f"Found existing user {profile}, matching data")
tally[0] += 1
else:
# PYTHONIC, BABY
@@ -60,7 +59,6 @@ class Command(BaseCommand):
initials=initials)
self.id_map[child.find('ID').text] = new_profile.pk
tally[1] += 1
print(f"No match found, creating new user {new_profile}")
except AttributeError: # W.T.F
print("Trainee #{} is FUBAR".format(child.find('ID').text))
@@ -227,16 +225,17 @@ class Command(BaseCommand):
for child in root:
try:
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
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))
if child.find('Training_x0020_Level_x0020_ID') is None:
print('Training Level Qualification #{} does not qualify in any level. How?'.format(child.find('ID').text))
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:
obj.confirmed_on = make_aware(datetime.datetime.strptime(child.find('Date_x0020_Level_x0020_Awarded').text.split('T')[0], "%Y-%m-%d"))

View File

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

View File

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

View File

@@ -22,13 +22,6 @@
{% block content %}
{% if form.errors %}
{% 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 %}
<form id="requirement-form" action="{{ form.action|default:request.path }}" method="post">{% csrf_token %}
{% render_field form.level|attr:'hidden' value=form.level.initial %}

View File

@@ -42,7 +42,7 @@
</div>
<div class="form-group form-row">
<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&filters=is_supervisor" 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" required>
{% if object.supervisor %}
<option value="{{object.supervisor.pk}}" selected>{{object.supervisor}}</option>
{% endif %}

View File

@@ -44,7 +44,7 @@
{% endblock %}
{% block content %}
{% if request.user.is_supervisor or perms.training.add_traininglevelrequirement %}
{% if request.user.as_trainee.is_supervisor or perms.training.add_traininglevelrequirement %}
<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">
<span class="fas fa-plus"></span> Add New Requirement
@@ -79,9 +79,9 @@
{% endfor %}
<tr><th colspan="3" class="text-center">{{object}}</th></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.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.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.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.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.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>
</tr>
</tbody>
</table>

View File

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

View File

@@ -1,4 +1,4 @@
{% if request.user.is_supervisor or perms.training.add_trainingitemqualification %}
{% if request.user.as_trainee.is_supervisor or perms.training.add_trainingitemqualification %}
<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
</a>

View File

@@ -19,7 +19,7 @@
<th>Date</th>
<th>Supervisor</th>
<th>Notes</th>
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
{% if request.user.as_trainee.is_supervisor or perms.training.change_trainingitemqualification %}
<th></th>
{% endif %}
</tr>
@@ -32,7 +32,7 @@
<td>{{ object.date }}</td>
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
<td>{{ object.notes }}</td>
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
{% if request.user.as_trainee.is_supervisor or perms.training.change_trainingitemqualification %}
<td>{% button 'edit' 'edit_qualification' trainee.pk %}</td>
{% endif %}
</tr>
@@ -46,9 +46,4 @@
</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 %}

View File

@@ -33,6 +33,11 @@ def 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
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)

View File

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

View File

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

View File

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

View File

@@ -1,38 +1,5 @@
import datetime
import pytest
from django.utils import timezone
from django.urls import reverse
from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains
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")
pytestmark = pytest.mark.django_db

View File

@@ -13,7 +13,7 @@ urlpatterns = [
has_perm_or_supervisor('RIGS.view_profile')(views.TraineeDetail.as_view()),
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>/add_qualification/', has_perm_or_supervisor('training.add_trainingitemqualification')(views.AddQualification.as_view()),
path('trainee/<int:pk>/add_qualification/', has_perm_or_supervisor('training.add_trainingitemqualificaiton')(views.AddQualification.as_view()),
name='add_qualification'),
path('trainee/<int:pk>/edit_qualification/', has_perm_or_supervisor('training.change_trainingitemqualification')(views.EditQualification.as_view()),
name='edit_qualification'),

View File

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

View File

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

View File

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