Compare commits

...

67 Commits

Author SHA1 Message Date
github-actions[bot]
9b16cf6333 Merge dependabot/npm_and_yarn/engine.io-and-browser-sync-6.2.0 into combine-prs-branch 2022-11-14 18:21:26 +00:00
dependabot[bot]
7798f5c368 Build(deps): Bump engine.io and browser-sync
Bumps [engine.io](https://github.com/socketio/engine.io) to 6.2.0 and updates ancestor dependency [browser-sync](https://github.com/BrowserSync/browser-sync). These dependencies need to be updated together.


Updates `engine.io` from 3.5.0 to 6.2.0
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/3.5.0...6.2.0)

Updates `browser-sync` from 2.27.7 to 2.27.10
- [Release notes](https://github.com/BrowserSync/browser-sync/releases)
- [Changelog](https://github.com/BrowserSync/browser-sync/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BrowserSync/browser-sync/compare/v2.27.7...v2.27.10)

---
updated-dependencies:
- dependency-name: engine.io
  dependency-type: indirect
- dependency-name: browser-sync
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 15:14:28 +00:00
dependabot[bot]
5c2e8b391c Build(deps): Bump moment from 2.29.2 to 2.29.4 (#505)
Bumps [moment](https://github.com/moment/moment) from 2.29.2 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.2...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-14 15:08:05 +00:00
dependabot[bot]
548bc1df81 Build(deps): Bump socket.io-parser from 3.3.2 to 3.3.3 (#503)
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/3.3.2...3.3.3)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-13 16:17:08 +00:00
dependabot[bot]
c1d2bce8fb Build(deps): Bump minimatch from 3.0.4 to 3.0.8 (#504)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.0.8.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.0.8)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-13 16:16:51 +00:00
c71beab278 Change: Only supervisors have edit access to the training database 2022-10-24 16:23:46 +01:00
259932a548 FIX #502: Possibility to choose 'no selection' in session log form
Ref #501...may help/fix this...uncertain yet. Need to finish writing the relevant test!
2022-10-23 10:56:54 +01:00
7526485837 FEAT: Add periodic cleanup command
Currently performs two functions:
1. Inactivates users that have not logged in for at least one year. Closes #478 (Need to circle back round to full deletion SoonTM)
2. Ensures the supervisor database flag is set correctly for each user

This is run automatically by the Heroku Scheduler addon at midnight daily.
2022-10-21 00:05:20 +01:00
39ed5aefb4 Set printed PDF title == filename
Should fix #497
2022-10-18 12:48:25 +01:00
e7e760de2e Fix copy to clipboard buttons on authorisation request form 2022-10-15 17:58:07 +01:00
9091197639 Minor audit fix 2022-06-02 15:36:53 +01:00
4f4baa62c1 Fix tests 2022-05-30 22:33:55 +01:00
b9f8621e1a Migrations fix 2022-05-26 16:23:51 +01:00
4b1dc37a7f Rename 'salvage value' to 'replacement cost'
This more accurately reflects historical use of the field, and what the insurers actually want. Ref #439
2022-05-26 10:29:53 +01:00
dependabot[bot]
9273ca35cf Build(deps): Bump django from 3.2.12 to 3.2.13 (#493)
Bumps [django](https://github.com/django/django) from 3.2.12 to 3.2.13.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.12...3.2.13)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-16 18:35:20 +01:00
dependabot[bot]
4a4b7fa30d Build(deps): Bump pypdf2 from 1.26.0 to 1.27.5 (#492)
Bumps [pypdf2](https://github.com/py-pdf/PyPDF2) from 1.26.0 to 1.27.5.
- [Release notes](https://github.com/py-pdf/PyPDF2/releases)
- [Changelog](https://github.com/py-pdf/PyPDF2/blob/main/CHANGELOG)
- [Commits](https://github.com/py-pdf/PyPDF2/compare/1.26.0...1.27.5)

---
updated-dependencies:
- dependency-name: pypdf2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Arona Jones <aj@aronajones.com>
2022-05-16 18:14:04 +01:00
a44a532c7d Create manual deployment Github Action 2022-05-16 16:54:47 +01:00
3a2e5c943b Fix typo II 2022-05-16 16:19:26 +01:00
426a9088cc Fix typo 2022-05-03 16:33:30 +01:00
dependabot[bot]
1369a2f978 Build(deps): Bump moment from 2.29.1 to 2.29.2 (#491)
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-13 09:43:32 +01:00
38eafbced3 Only require item prerequisites on competency assessed 2022-03-07 16:43:00 +00:00
900002bf71 Modalise training item qualification edit
Also fixed some stuff
2022-03-06 18:30:03 +00:00
2869c9fcc3 FIX(T): 500 error editing training item qualifications
For the second time with this piece of functionality...how did that ever work?
2022-03-06 17:30:11 +00:00
00eb4e0e27 FEAT(T): Add ability to view users passed out in a certain training item 2022-03-03 19:31:04 +00:00
23e17b0e34 FIX(T): Training level requirement changes spamming recent changes 2022-03-03 19:14:20 +00:00
bf268a4566 Minor branding tweak 2022-03-03 19:10:05 +00:00
dedb8d81fe FEAT(T): Add ability to log items at various depths during a session
Also fixes inability to search by reference number
2022-03-01 18:36:35 +00:00
7d785f4f1b Hotfix: Unable to add training item in requirements form 2022-02-27 22:16:39 +00:00
5eb113156b FEAT(T): First version of the 'session log' form 2022-02-27 21:20:34 +00:00
ab03ad081a Markdownify event description in tables
TODO: Restrict max size of headers so they can't go larger than other elements in the table.
2022-02-24 20:20:57 +00:00
cd5889f60e Wrong place... 2022-02-24 14:52:52 +00:00
f18bf3b077 Add total asset number to list page 2022-02-24 14:52:19 +00:00
3d36d986a4 Add basic validation of item prerequisites
Currently throws the worlds most unhelpful error message...
2022-02-23 16:01:00 +00:00
41f5a23ef0 Redesign label sheet to have variable (csa based) label size 2022-02-22 23:24:27 +00:00
09f48f740d Redesign cable label to increase asset number readability 2022-02-22 20:54:38 +00:00
805d77af20 Move access/meet times to above start/end in event list 2022-02-22 15:00:39 +00:00
fabab87e23 Add moved templates to git
Not sure why that wasn't automatic.
2022-02-16 15:21:57 +00:00
a95779e04e FEAT: Add ability to generate RA printouts 2022-02-16 15:01:38 +00:00
24e6ba540d Embolden item headers on paperwork
Closes #481
2022-02-16 13:14:53 +00:00
14d3522b81 Do not require supervisor consultation for big power
Only requires power tech
2022-02-16 13:14:25 +00:00
e4cfaba57d Markdown enable generic note fields 2022-02-15 12:13:50 +00:00
d9664422c5 Fix asset sample data command 2022-02-15 02:40:20 +00:00
27bb3f1d8e Port/fix asset tests 2022-02-15 02:28:19 +00:00
151ac8b3bd Add fallback initial asset id 2022-02-15 01:58:40 +00:00
c2dcd86d5d Calendar changes
Closes #437
(Probably) Closes #469 but also puts us out of spec...
2022-02-15 01:00:47 +00:00
6c14b30c13 Disable auth request button for authorised events
Overrides and closes #377
2022-02-15 00:29:29 +00:00
5215af349a Tweak asset ID autogeneration
Closes #443, admittedly with a different approach.
2022-02-15 00:18:19 +00:00
a5e888fef5 Make salvage value a required field on asset
Closes #439
2022-02-15 00:18:19 +00:00
dependabot[bot]
2ae4e4142c Build(deps): Bump follow-redirects from 1.14.7 to 1.14.8 (#489)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-15 00:18:13 +00:00
8799f822bb HOTFIX: Supplier admin broken 2022-02-14 16:27:02 +00:00
2dd3d306b4 Remove a bunch of rounded corners
Closer to the Forum UI, and also just generally less 2015
2022-02-14 15:45:11 +00:00
042004e1ae Some cable list work 2022-02-14 15:28:43 +00:00
733ea69cc5 Add tooling for mass editing 2022-02-14 15:14:00 +00:00
bbea47e8ec Change purchased_from options so supplier deletion does not delete assets 2022-02-14 15:13:55 +00:00
c4aafbd7e5 First version of the cable list 2022-02-14 11:50:16 +00:00
ccdc13df93 FIX #486: Make training item edit work 2022-02-14 11:19:44 +00:00
aa19ceaf18 FIX #488: Unable to filter detailed training record by item ID 2022-02-14 11:14:32 +00:00
05d280172d Order detailed training record by reference number 2022-02-13 11:52:41 +00:00
2f51b7b1d3 pep8 2022-02-08 17:29:38 +00:00
8d1edb54ea HOTFIX Trainee Search broken 2022-02-08 17:22:09 +00:00
54c90a7be4 Refactor search logic to a create an 'omnisearch' (#484) 2022-02-08 15:01:01 +00:00
3e1e0079d8 Fix traininglevelqualification display
Closes #470 for the time being
2022-02-03 18:47:16 +00:00
b6952aeb52 Various fixings 2022-01-30 12:31:54 +00:00
d33a4231fb Fix selectpickers showing up slightly wonky 2022-01-30 12:14:26 +00:00
8dea6aeab0 Allow to search training items by (full) reference number) 2022-01-30 12:03:40 +00:00
34c03e379d Switch to using competency assessed for leaderboard 2022-01-30 11:23:54 +00:00
988fb78b45 Rewrite admin merge functionality. Should close #473 2022-01-29 14:58:15 +00:00
103 changed files with 2854 additions and 1815 deletions

14
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: Manual Deploy
on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: akhileshns/heroku-deploy@v3.12.12 # This is the action
with:
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
heroku_app_name: "pyrigs" #Must be unique in Heroku
heroku_email: "aj@aronajones.com"

View File

@@ -44,7 +44,7 @@ psutil = "~=5.8.0"
psycopg2 = "~=2.8.6" psycopg2 = "~=2.8.6"
Pygments = "~=2.7.4" Pygments = "~=2.7.4"
pyparsing = "~=2.4.7" pyparsing = "~=2.4.7"
PyPDF2 = "~=1.26.0" PyPDF2 = "~=1.27.5"
PyPOM = "~=2.2.0" PyPOM = "~=2.2.0"
python-dateutil = "~=2.8.1" python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21" pytoml = "~=0.1.21"
@@ -80,6 +80,8 @@ importlib-metadata = "*"
django-hcaptcha = "*" django-hcaptcha = "*"
"z3c.rml" = "*" "z3c.rml" = "*"
pikepdf = "*" pikepdf = "*"
django-queryable-properties = "*"
django-mass-edit = "*"
[dev-packages] [dev-packages]
selenium = "~=3.141.0" selenium = "~=3.141.0"

649
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "841781f4c4d3c12a34c0ff8ef3fd58171baf657478d5c339d4f6fc79d5830978" "sha256": "8ba92145c54db8b765e3d517f000ce0c42281586260c032bf89f181bb886d61b"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -37,6 +37,7 @@
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144" "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
], ],
"markers": "python_version >= '3.5'",
"version": "==1.10" "version": "==1.10"
}, },
"attrs": { "attrs": {
@@ -44,6 +45,7 @@
"sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4",
"sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.4.0" "version": "==21.4.0"
}, },
"backports.tempfile": { "backports.tempfile": {
@@ -236,28 +238,30 @@
}, },
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3", "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804",
"sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31", "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178",
"sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac", "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717",
"sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf", "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982",
"sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316", "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004",
"sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca", "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe",
"sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638", "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452",
"sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94", "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336",
"sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12", "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4",
"sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173", "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15",
"sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b", "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d",
"sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a", "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c",
"sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f", "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0",
"sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2", "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06",
"sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9", "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9",
"sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46", "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1",
"sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903", "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023",
"sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3", "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de",
"sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1", "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f",
"sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee" "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181",
"sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e",
"sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"
], ],
"version": "==36.0.1" "version": "==37.0.2"
}, },
"cssselect": { "cssselect": {
"hashes": [ "hashes": [
@@ -300,11 +304,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:0a0a37f0b93aef30c4bf3a839c187e1175bcdeb7e177341da0cb7b8194416891", "sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6",
"sha256:69c94abe5d6b1b088bf475e09b7b74403f943e34da107e798465d2045da27e75" "sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.2.11" "version": "==3.2.13"
}, },
"django-debug-toolbar": { "django-debug-toolbar": {
"hashes": [ "hashes": [
@@ -345,6 +349,22 @@
"index": "pypi", "index": "pypi",
"version": "==1.7.3" "version": "==1.7.3"
}, },
"django-mass-edit": {
"hashes": [
"sha256:03e5adabfc9bf89ae4edee80d63957e86a18e0f4564076779750d30e4b3650d6",
"sha256:e05ef51e02b952f9ca517964d302ab75991886332d212d46697ad9debb38dfd6"
],
"index": "pypi",
"version": "==3.4.1"
},
"django-queryable-properties": {
"hashes": [
"sha256:2112efc9ef92298753a628b5f2a3b3570cdbfd8e890503da9e722fd4a171c09d",
"sha256:6332eada8bdd6b820526f50023cd4d37c2625a5a80fa4ce65ee8d8908b56135a"
],
"index": "pypi",
"version": "==1.8.0"
},
"django-recurrence": { "django-recurrence": {
"hashes": [ "hashes": [
"sha256:715f681f6af029ff3a8d73c7b1460abd8cbc5d5a5001efcb127032e84d9cb963", "sha256:715f681f6af029ff3a8d73c7b1460abd8cbc5d5a5001efcb127032e84d9cb963",
@@ -404,6 +424,7 @@
"sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06", "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06",
"sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442" "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"
], ],
"markers": "python_version >= '3.6'",
"version": "==0.13.0" "version": "==0.13.0"
}, },
"html5lib": { "html5lib": {
@@ -411,6 +432,7 @@
"sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d",
"sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f" "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.1" "version": "==1.1"
}, },
"icalendar": { "icalendar": {
@@ -431,12 +453,11 @@
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6", "sha256:6affcdb3aec542dd98df8211e730bba6c5f2bec8288d47bacacde898f548c9ad",
"sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e" "sha256:9e5e553bbba1843cb4a00823014b907616be46ee503d2b9ba001d214a8da218f"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version < '3.10'", "version": "==4.11.0"
"version": "==4.10.1"
}, },
"lxml": { "lxml": {
"hashes": [ "hashes": [
@@ -557,6 +578,7 @@
"sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958", "sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958",
"sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967" "sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967"
], ],
"markers": "python_version >= '3.6'",
"version": "==1.1.0" "version": "==1.1.0"
}, },
"packaging": { "packaging": {
@@ -564,6 +586,7 @@
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
], ],
"markers": "python_version >= '3.6'",
"version": "==21.3" "version": "==21.3"
}, },
"pep517": { "pep517": {
@@ -576,80 +599,82 @@
}, },
"pikepdf": { "pikepdf": {
"hashes": [ "hashes": [
"sha256:11a9c17f6262113d37454638d61c6102eff298309ebfcf4b6c96a3fe3dd57785", "sha256:21c7abcdaa33be32513009d84d8c9cf8a4485281c973e8e21dadac461dd1a2f2",
"sha256:15cf594b41ba10415181c22cd9e1aab288929bd1b382a534a05f82293b0eac3a", "sha256:2a373b8f76db612578e8fa606f8d543f9f8badeaf48449a607982ff545851f7a",
"sha256:19fbfce2b7b7cefd6227f4cd067611d0026dd5e8ef4c42b7f49e4e0016b1cf1a", "sha256:2d71d93d31557462b5253292f3977cc90c5a6744dd572c5ea122a9487340b390",
"sha256:2250f45865a177688e7a225f76db4fad7fb9af46e43fad77081ca41c74307874", "sha256:300ddce9294d8b5d362125398066c555567832542114c81ac52e9f98abe30af1",
"sha256:27a034849fa052e97b262be97efed65f8b1bf681214a754846faeccacd51a61d", "sha256:3c0bd9d678d1ee769dba3fef90ff8bebe292417bd11ef9663f07f3df9d3c4a73",
"sha256:394d93eafa7688efb4f1c6365ec540fa8768888c041396354209386f72849eb2", "sha256:45b3d440448938b7b34e03ca84525b72149f36e5a1381128f9f86d9983b8df20",
"sha256:3dae8f11be19f55d3cf4b3eaf2b257aaf39f8f8bfd7eaab134c60c0f3438ec5c", "sha256:476d567999f6ed2c83c7e62f59fe819e9b1a1671bfbc6694d24b64b88ac3c1d0",
"sha256:3f0c8421ae131846a33d970a388b77a6d7a02c7496ee92660247803247bad52e", "sha256:678ec03d4a37987052f62099662664ecdeafb0bdcd1fbc562bb2d0d285faa134",
"sha256:40d7d330cfe064b92dca1d8d8f25730ea5cadec9be185f811b96704e02065edd", "sha256:740245bc799adfab15a6819e3b485c7da5680ce69a2fb8becf2e1069f87588fd",
"sha256:414d7b4bbd7cf3a9553e2254b1631c5ace59a716afa8d461bd76863255738504", "sha256:7800d2a51ce53b0fbb09bef225a6477671b124915b645027ba181e5559efbd03",
"sha256:441157ce3165f77478bad5724fe2aa0428f58728e31152d8d8c0626015c51dd0", "sha256:89647dd14994961ad6844b48e3f10158094eef21c9318d817e09166f99af5838",
"sha256:4daf357f2436758213f164c71fa5aa2c835b4d1fb0b71247b0392198dd480de8", "sha256:961a84364e7ce13b9ab8ef305bdf237b28451005abcdbb084a7808ce6bd726ac",
"sha256:4dce15f0f0aec4ed6840383c0897020eae1aa4382dbff5d18b5efbe2a99d09b4", "sha256:973da09bdb82c0df8fc3056f63d92ab67556395354c77469c0ab1e319bb0c792",
"sha256:5a4cb65ef1f0fcc8c1b3daf2ef000bcb2318cb19961e6a4bcb7404bf37c78f10", "sha256:a0c7c54f12098a023c0f70fcdeb2cf25ad782620ecd16c71d63aaee981175f0b",
"sha256:5bbcf6bc5d1ecc63f6c6c54c631d59340e29c89a300487517d0dad3630afa24c", "sha256:a0dfd11f936f541f50c50f0e5dd7f8b425913efd9043337981a104f18aae5ded",
"sha256:5edd87f1fb31f05b8251480fbc1e05e956589d1be36189234fd40d480dcc32f1", "sha256:b40566fb0b9d858e3a6621b8bff0296902fe082d70530cfbab82c4762bffdf4e",
"sha256:6646f05057e88773844c7fefb6b5329e5215a235c692cbf70d5ed66d8c69b7e6", "sha256:bd9ef14591559ab1bf2a448acd1467dd8a90c339425f80a65fe8d554ce69fbe9",
"sha256:792b9f670975fdf6dd47350129d1bf5f27ef1ecafaab6d9aa6be15ac58ceb8ae", "sha256:c4b1928c22879d84bdee9ebd163dca8e58f131c78eb5700c232f3dbad65d92b2",
"sha256:7dd41620f1dbbd719ca9f5ee4a8219fce35691a01d88274752078c2717bb354b", "sha256:c862ffd4aa2e17bc2ed50b357b6ecbe442e5a8bff84b8a5070f8e509440a52a4",
"sha256:8836e6060534c7245c8d736e93600667b928c767a012a73bb567f56bbe3d985c", "sha256:c8bc88ee5af1ad2c68fb8491a79d29d4476dc89ae3ecba471bb00ea8c43cfa35",
"sha256:9bc477d142785ae4663bf200b929bfde4536e428c653553a5e62d32ea29148b1", "sha256:ca3202916f036222071db5ac77ee2767ab4a36f60ed740ff5200f1a8fda717c7",
"sha256:b2da6732a711ff217ee4d84f328c23db0476cfdd5c321b75bad727f481cc670f", "sha256:cb4c7b8a83948ffa2b02acd6a4587c3a53501ccd18149ec1493bc240e07a3ac0",
"sha256:bcd68a15d06987b519148a09ff1e6840ee71249130bde59ffdf374825dd5826d", "sha256:ea223ad320b1f96f28def3f3c7521d8e1616f4f2f0b946fa31c960bf0bf65b97",
"sha256:beef92deb39a04c08a7401eebbe99dbec44b136e0a4f31fe3670159755feea38", "sha256:fd66f37547d6faa616978ed30f7e64fcc032eb3a531df8224bc2fb39d282af68",
"sha256:c714685a0868f277fdf36afeb84a2aa696dab0182eaef4bb91cf3e6b776ba468", "sha256:fe39256cc251d8563ec8268542b127e7e1482a615a18812d02f93d7970c4b033"
"sha256:cd575cf0131683a7b661357bfd777b27c3c6c0d0fb7ef27e627f521122f75536",
"sha256:fb3d7fb390192cfb1e287503dbc03229c1c77fe9820cf084546bb63fa997fd87"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.3.1" "version": "==4.5.0"
}, },
"pillow": { "pillow": {
"hashes": [ "hashes": [
"sha256:03b27b197deb4ee400ed57d8d4e572d2d8d80f825b6634daf6e2c18c3c6ccfa6", "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97",
"sha256:0b281fcadbb688607ea6ece7649c5d59d4bbd574e90db6cd030e9e85bde9fecc", "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049",
"sha256:0ebd8b9137630a7bbbff8c4b31e774ff05bbb90f7911d93ea2c9371e41039b52", "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c",
"sha256:113723312215b25c22df1fdf0e2da7a3b9c357a7d24a93ebbe80bfda4f37a8d4", "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae",
"sha256:2d16b6196fb7a54aff6b5e3ecd00f7c0bab1b56eee39214b2b223a9d938c50af", "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28",
"sha256:2fd8053e1f8ff1844419842fd474fc359676b2e2a2b66b11cc59f4fa0a301315", "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030",
"sha256:31b265496e603985fad54d52d11970383e317d11e18e856971bdbb86af7242a4", "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56",
"sha256:3586e12d874ce2f1bc875a3ffba98732ebb12e18fb6d97be482bd62b56803281", "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976",
"sha256:47f5cf60bcb9fbc46011f75c9b45a8b5ad077ca352a78185bd3e7f1d294b98bb", "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e",
"sha256:490e52e99224858f154975db61c060686df8a6b3f0212a678e5d2e2ce24675c9", "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e",
"sha256:500d397ddf4bbf2ca42e198399ac13e7841956c72645513e8ddf243b31ad2128", "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f",
"sha256:52abae4c96b5da630a8b4247de5428f593465291e5b239f3f843a911a3cf0105", "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b",
"sha256:6579f9ba84a3d4f1807c4aab4be06f373017fc65fff43498885ac50a9b47a553", "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a",
"sha256:68e06f8b2248f6dc8b899c3e7ecf02c9f413aab622f4d6190df53a78b93d97a5", "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e",
"sha256:6c5439bfb35a89cac50e81c751317faea647b9a3ec11c039900cd6915831064d", "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa",
"sha256:72c3110228944019e5f27232296c5923398496b28be42535e3b2dc7297b6e8b6", "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7",
"sha256:72f649d93d4cc4d8cf79c91ebc25137c358718ad75f99e99e043325ea7d56100", "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00",
"sha256:7aaf07085c756f6cb1c692ee0d5a86c531703b6e8c9cae581b31b562c16b98ce", "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838",
"sha256:80fe92813d208ce8aa7d76da878bdc84b90809f79ccbad2a288e9bcbeac1d9bd", "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360",
"sha256:95545137fc56ce8c10de646074d242001a112a92de169986abd8c88c27566a05", "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b",
"sha256:97b6d21771da41497b81652d44191489296555b761684f82b7b544c49989110f", "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a",
"sha256:98cb63ca63cb61f594511c06218ab4394bf80388b3d66cd61d0b1f63ee0ea69f", "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd",
"sha256:9f3b4522148586d35e78313db4db0df4b759ddd7649ef70002b6c3767d0fdeb7", "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4",
"sha256:a09a9d4ec2b7887f7a088bbaacfd5c07160e746e3d47ec5e8050ae3b2a229e9f", "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70",
"sha256:b5050d681bcf5c9f2570b93bee5d3ec8ae4cf23158812f91ed57f7126df91762", "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204",
"sha256:bb47a548cea95b86494a26c89d153fd31122ed65255db5dcbc421a2d28eb3379", "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc",
"sha256:bc462d24500ba707e9cbdef436c16e5c8cbf29908278af053008d9f689f56dee", "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b",
"sha256:c2067b3bb0781f14059b112c9da5a91c80a600a97915b4f48b37f197895dd925", "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669",
"sha256:d154ed971a4cc04b93a6d5b47f37948d1f621f25de3e8fa0c26b2d44f24e3e8f", "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7",
"sha256:d5dcea1387331c905405b09cdbfb34611050cc52c865d71f2362f354faee1e9f", "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e",
"sha256:ee6e2963e92762923956fe5d3479b1fdc3b76c83f290aad131a2f98c3df0593e", "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c",
"sha256:fd0e5062f11cb3e730450a7d9f323f4051b532781026395c4323b8ad055523c4" "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092",
"sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c",
"sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5",
"sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"
], ],
"index": "pypi", "index": "pypi",
"version": "==9.0.0" "version": "==9.0.1"
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
], ],
"markers": "python_version >= '3.6'",
"version": "==1.0.0" "version": "==1.0.0"
}, },
"premailer": { "premailer": {
@@ -739,10 +764,10 @@
}, },
"pyopenssl": { "pyopenssl": {
"hashes": [ "hashes": [
"sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3", "sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf",
"sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6" "sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0"
], ],
"version": "==21.0.0" "version": "==22.0.0"
}, },
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
@@ -754,10 +779,11 @@
}, },
"pypdf2": { "pypdf2": {
"hashes": [ "hashes": [
"sha256:e28f902f2f0a1603ea95ebe21dff311ef09be3d0f0ef29a3e44a932729564385" "sha256:7c18c1d48e56547b1c33f772dc15d6adbd1f4020b62e64bb4a0bc0ee2ab94511",
"sha256:9c81fc06be50fbf2e6199e8c2eac05f3eaafae4b3905ecca41200f5b02ac43d7"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.26.0" "version": "==1.27.5"
}, },
"pypom": { "pypom": {
"hashes": [ "hashes": [
@@ -767,6 +793,14 @@
"index": "pypi", "index": "pypi",
"version": "==2.2.3" "version": "==2.2.3"
}, },
"pysocks": {
"hashes": [
"sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
"sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
"sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
],
"version": "==1.7.1"
},
"python-barcode": { "python-barcode": {
"hashes": [ "hashes": [
"sha256:daa32fb999a843812fbb1c75ff909638811af7c465f0a991e9e41d26d2a44a24", "sha256:daa32fb999a843812fbb1c75ff909638811af7c465f0a991e9e41d26d2a44a24",
@@ -801,53 +835,45 @@
}, },
"reportlab": { "reportlab": {
"hashes": [ "hashes": [
"sha256:0430cfe397415759839ef89abee6db82e8a8f9bb5831a3c93e7763915c755345", "sha256:22c28e593e2c37110f79df9bb31ba7782dc8c0002f33d8070c6d18e1c7380bfc",
"sha256:13072e33e8cbac6fd6e776fecabdefafb0261886b2ab7cb3b874a9384f1b0ffe", "sha256:238f1088b1ce94d25790774546fc52e3efd909eafe0c56f71d286996dd2d2db0",
"sha256:1767106d03320e76a708d2c40488fe1785580a0d7abac7715e01a3cc910c1179", "sha256:2668687baf0a6c64f90193eca74dfa69bf172bf38e436c7be91e0b13867132ec",
"sha256:17f35a856bbf46989d557d4016822bcdd3ada88d3afb567de03a4b29676aa52e", "sha256:2adb9c53c86b30290b407a24b88cf07b09c3b325866b5125b4dca4aa7996021e",
"sha256:19414f4357287a7573a60bcb76a092c9ea82bf09f01d04b3afb5c1bd3c660df2", "sha256:2ca0c987433bf63d765a9dcc9cb54695e617725ee81058af615f8d42fc29c0d8",
"sha256:1d8d9674eb6ba1b6c3d6a8e3d5d4e4231b3576db653d1b1fdac2538afee54c7a", "sha256:2cf111835bd4b9afbdf8568c4031e2727cdc64a914bbd68e60aa190672f70d34",
"sha256:23236dc70598b688e979444c4840c5cec88a2a12fe81ba6f8cc807120a2cad33", "sha256:30d75931893f6c5beb14a93b0a3701cf14a6353c0b48acefa6b4c2391464b861",
"sha256:28c339d25eab804a8bd004dfaa5a80c7568178561741f4ce6e69dae05d38041f", "sha256:44c62615504f669a92a62431e847a11c281072ec3a4820a8880dab7338cad53c",
"sha256:2c93a551b60c7fd3b17942772847f7c4ee2f08ae74c87ef8f325fe8083d2aa6e", "sha256:473680fb899aed897963ddbf4536b377e40c7ea6fba83337e7f544e3040df956",
"sha256:2e80045f36dd4b9b63b19fc073149f70857fe8590027ab3658db80ac6235ecd0", "sha256:502ae45775ddf6ed10f23253f8a7768b52b9517ac590babcb92aab0336a2a13a",
"sha256:38aa912301d93e2267861d820cb3f6eebed8deb58d0df429421578b9ba033eee", "sha256:5a681047247a6d896ed7ec18b95054c9c139c0269417beb066985244b8d18f75",
"sha256:47587ce01cf9ac25f6d187116a9f9cef710dc58ccea001024d950c4f5a504643", "sha256:5d62c8341a426984d488fadab2e2b35c4e3e4f5c6ceb2e6b57d7fc41cb7ba992",
"sha256:587b3d8ce0a065a00975516013aebb062e6161fba3cf399b22f270e4d9a3db1e", "sha256:6910eb0152a72be5ebe8984472f9b2eeb1a5dc3db20a591cbcf179b14c2757a8",
"sha256:5a650284cc09caa32b5845c055bf035cb76949b87d57e9eed56d98f863613417", "sha256:6e9f42099141bb35013297b8de8b7329946d94e881cbd72c3d76f44d5a9df705",
"sha256:5e113c630b6109efe0285230706c8423bff1b82c2e2824e441401a467a1215b7", "sha256:7ac03370a672c9df9e691da4870f5db79d6227f37a6faf7d17a822890d42de60",
"sha256:68e339411cc9329ff50982a7c1d55eabd53ac9be24d4442088af58328bae54d3", "sha256:8a49fec7ea0c410dc84c88ac8c965605a3e6d50a9b81afb9539175168c7deaf7",
"sha256:6ae1fb03faf4b6710e2c081d5208416a5d557e0cc00ff24fc124dd42a7158114", "sha256:97b5ab874e8d74f3dbe3b48a531df7df269acb35c3e5eed9d41b3579bef9ad77",
"sha256:6f363e09aacaa7aaff232197fddb667d899822aa57d10091aea4fbb1f56b7fa7", "sha256:9db71af717229dad72fe5f4dfb587eb952a07f7c1bcd83df402b676c78a334f5",
"sha256:70841d7eb4aa2f8ad4afacce07711481a0dcd9d01679da5627173443131a33a2", "sha256:a089addc73b770d159615fc4c90cd06226b0c071d30c63e8addf57b9533049ee",
"sha256:71d91002878c4d2a17a6bd7208c59373e6148977fe674bb79eec3eb9e63aa20f", "sha256:a09acda69357664190a02f239abb01505d519a2563ba89d57d6fb55ca14ade72",
"sha256:7a09e5bf9c8e02c373e5e558cc5c2cfbc5d3c68560a406c6d16254363cfa989e", "sha256:ae252b718fb6de4da766d2b4b3402592923e327641dfa0a1b3cfecaa8a95229f",
"sha256:85095ef9f3697859064cb1b22f19659bf4ba25e7dadb9c6be65f322cd68ba88f", "sha256:b109d8594a5140f8c0e93c0d091e16c6274267027077cddbc590d4bff7acb35c",
"sha256:8dafdcdde7243f0864d6d11dd9bfffbd1e6bce6c3e668fe992f56ae48377c822", "sha256:b1d4940ff5f573f54855507c2d2ddfeb9a034ad3f040fa5168cf235717531b78",
"sha256:9a822486a98fe002bbe248fdf3f126739c1ad29032b54b71a3f67b6364a77677", "sha256:b44a59e75a2c20912e21960df45c0644ded4538300becbb1df5b4cceea2afa11",
"sha256:aa57dc0818e066fdced9457b9e6c6fb269d63e2d96902001c7dbe010bce6ebcc", "sha256:b84c0c3ad09eb9183fb2e54e44da92d84436d9f3a3263d1456e463c723c54906",
"sha256:b0836c6cdee4b88e2366e0ff152c1327578149e09850b7cab6016444c5b3eb26", "sha256:d05603fcf2acee5d01eb814d36b212aafbd82cafb9ae861dff41daaf893f95f1",
"sha256:b2988ffc33032096e808e7a4a36f5b453fcc9587873c85c1b44bc6846bbbd09c", "sha256:d42a442f4593ab5e196debc32aff0c36fcbf4031f068e1c9435d4137f47d7990",
"sha256:bd38d58895b359ef429df3c97dc00c3fef0ab57f45556de416ba9b7d7fc71ae2", "sha256:dd1cdb62dc123f5859ca514eb639f70660bdc818c95fb0ee2370a175a0e20ce4",
"sha256:be87dca9253efd3cd0f351b785530c02e67664e284e3c4a97cdd0c7dd806d39a", "sha256:e6d3affa0e484fb55e1061bbdf778797c68a648127f91102b1f0a6173ecb590e",
"sha256:c21bdb11d7fccea28bf08eac13d9d031836e335c5e0620eae1d4336f193e9a03", "sha256:e7ca3699612efc278c666193aa340937066d8045cde247c4b409c8f416e0811e",
"sha256:c43f847f2598b5c2fc9b63871d7da641c0b90e384d8da8018d4d7173a0b82cd4", "sha256:e80ed55cbbaf905635a2673d439495e1b1925b8379ea56aa2fc859a00e41af9f",
"sha256:c780cc5208c67b25bdddd08480f874614cd0ec0bed39e1a848448543f2093945", "sha256:edab6b0fc5984051b9b74d33579b7e3d228b70a5801904aa645828a95efb8486",
"sha256:c9bcf696bc8935ff90ecb50c7644e2af01f63a444d4b4bd39d41d2abdd7bb224", "sha256:ef659caf2f2824ab0bdf9e98a3886272232bcb1c756be4eb4f5c3c60a9519092",
"sha256:cb48b71088f5c9eff5715dde0bd4d5372d4713ffa92247acf0f04fd17ab2078d", "sha256:f00e0218854e168bd8d5379d07f0e138285c34b5fe3878c8d5d4f691e280d95e",
"sha256:d48f638893b3eb4c9b2afeec2de4f95a4b57fb8c398e3d7f9a7fb4b4d9546820", "sha256:f2bc48fc45f13d9ccc123462ab3bfd18a78e4bd58d027f9d4a226110c78adc3c",
"sha256:d8fe27ad312671c9347cf5997f7c1017833fac17233f33296281ba9fa0de189a", "sha256:f2be927d8717c5947e7968f089492c088a4103bfe6287ee01a001e0b9a84545b"
"sha256:d98b759661070f5588b30152d0caaf16ac387f60372f8fa2568c9ad4014cd7f3",
"sha256:e2022ad36409e7616ed6311f7ab113f236cac66ba0d22be4f53bf7e77654b143",
"sha256:e45159f4d19304f5e79be13283fe53bdd006c4fd4d93ff3cb6ac082ca017c418",
"sha256:eb3ef5394b4b2c904ab467dbbe1efcfbe046e1395c2d3064420ccef89806570e",
"sha256:f326b04a3fb3c7c58b799bd23b60790b181893f052fe5a8011c9cd9984e24a43",
"sha256:f401ed014ea861dea2ae621f7810fb15b3bc021e6487dee97b32f175bbf1b7eb",
"sha256:f4d4eb3a949ccb0782e4d6560fcd5ee6f34636d1ee24f1d2a2b1f530af89481a",
"sha256:fdc3dc1242be557f6a8bb9e21751296cc721f60b8e2b684690049e656d798520"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.6.5" "version": "==3.6.6"
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
@@ -866,17 +892,26 @@
}, },
"selenium": { "selenium": {
"hashes": [ "hashes": [
"sha256:27e7b64df961d609f3d57237caa0df123abbbe22d038f2ec9e332fb90ec1a939" "sha256:866b6dd6c459210662bff922ee7c33162d21920fbf6811e8e5a52be3866a687f"
], ],
"version": "==4.1.0" "markers": "python_version ~= '3.7'",
"version": "==4.1.5"
}, },
"sentry-sdk": { "sentry-sdk": {
"hashes": [ "hashes": [
"sha256:141da032f0fa4c56f9af6b361fda57360af1789576285bd1944561f9c274f9c0", "sha256:3817274fba2498c8ebf6b896ee98ac916c5598706340573268c07bf2bb30d831",
"sha256:9aeff2a47f4038460296b920bf4d269284e8454e1c67547ee002ccafd9c2442b" "sha256:98fd155fa5d5fec1dbabed32a1a4ae2705f1edaa5dae4e7f7b62a384ba30e759"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.5.3" "version": "==1.5.5"
},
"setuptools": {
"hashes": [
"sha256:5534570b9980fc650d45c62877ff603c7aaaf24893371708736cc016bd221c3c",
"sha256:ca6ba73b7fd5f734ae70ece8c4c1f7062b07f3352f6428f6277e27c8f5c64237"
],
"markers": "python_version >= '3.7'",
"version": "==62.2.0"
}, },
"simplejson": { "simplejson": {
"hashes": [ "hashes": [
@@ -958,6 +993,7 @@
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
], ],
"markers": "python_version >= '3.5'",
"version": "==1.2.0" "version": "==1.2.0"
}, },
"sortedcontainers": { "sortedcontainers": {
@@ -973,7 +1009,6 @@
"sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9" "sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.0'",
"version": "==2.3.1" "version": "==2.3.1"
}, },
"sqlparse": { "sqlparse": {
@@ -1011,6 +1046,7 @@
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2" "version": "==0.10.2"
}, },
"tornado": { "tornado": {
@@ -1062,16 +1098,18 @@
}, },
"trio": { "trio": {
"hashes": [ "hashes": [
"sha256:895e318e5ec5e8cea9f60b473b6edb95b215e82d99556a03eb2d20c5e027efe1", "sha256:670a52d3115d0e879e1ac838a4eb999af32f858163e3a704fe4839de2a676070",
"sha256:c27c231e66336183c484fbfe080fa6cc954149366c15dc21db8b7290081ec7b8" "sha256:fb2d48e4eab0dfb786a472cd514aaadc71e3445b203bc300bad93daa75d77c1a"
], ],
"version": "==0.19.0" "markers": "python_version >= '3.7'",
"version": "==0.20.0"
}, },
"trio-websocket": { "trio-websocket": {
"hashes": [ "hashes": [
"sha256:5b558f6e83cc20a37c3b61202476c5295d1addf57bd65543364e0337e37ed2bc", "sha256:5b558f6e83cc20a37c3b61202476c5295d1addf57bd65543364e0337e37ed2bc",
"sha256:a3d34de8fac26023eee701ed1e7bf4da9a8326b61a62934ec9e53b64970fd8fe" "sha256:a3d34de8fac26023eee701ed1e7bf4da9a8326b61a62934ec9e53b64970fd8fe"
], ],
"markers": "python_version >= '3.5'",
"version": "==0.9.2" "version": "==0.9.2"
}, },
"urllib3": { "urllib3": {
@@ -1099,10 +1137,11 @@
}, },
"wsproto": { "wsproto": {
"hashes": [ "hashes": [
"sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38", "sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b",
"sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f" "sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"
], ],
"version": "==1.0.0" "markers": "python_version >= '3.7'",
"version": "==1.1.0"
}, },
"yolk": { "yolk": {
"hashes": [ "hashes": [
@@ -1318,11 +1357,20 @@
} }
}, },
"develop": { "develop": {
"async-generator": {
"hashes": [
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
],
"markers": "python_version >= '3.5'",
"version": "==1.10"
},
"attrs": { "attrs": {
"hashes": [ "hashes": [
"sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4",
"sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.4.0" "version": "==21.4.0"
}, },
"certifi": { "certifi": {
@@ -1333,65 +1381,115 @@
"index": "pypi", "index": "pypi",
"version": "==2020.12.5" "version": "==2020.12.5"
}, },
"cffi": {
"hashes": [
"sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3",
"sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2",
"sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636",
"sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20",
"sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728",
"sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27",
"sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66",
"sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443",
"sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0",
"sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7",
"sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39",
"sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605",
"sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a",
"sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37",
"sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029",
"sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139",
"sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc",
"sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df",
"sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14",
"sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880",
"sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2",
"sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a",
"sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e",
"sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474",
"sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024",
"sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8",
"sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0",
"sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e",
"sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a",
"sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e",
"sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032",
"sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6",
"sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e",
"sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b",
"sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e",
"sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954",
"sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962",
"sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c",
"sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4",
"sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55",
"sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962",
"sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023",
"sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c",
"sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6",
"sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8",
"sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382",
"sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7",
"sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc",
"sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997",
"sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"
],
"version": "==1.15.0"
},
"charset-normalizer": { "charset-normalizer": {
"hashes": [ "hashes": [
"sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd", "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455" "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
], ],
"markers": "python_version >= '3'", "markers": "python_version >= '3'",
"version": "==2.0.10" "version": "==2.0.12"
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
"sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0", "sha256:06f54765cdbce99901871d50fe9f41d58213f18e98b170a30ca34f47de7dd5e8",
"sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd", "sha256:114944e6061b68a801c5da5427b9173a0dd9d32cd5fcc18a13de90352843737d",
"sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884", "sha256:1414e8b124611bf4df8d77215bd32cba6e3425da8ce9c1f1046149615e3a9a31",
"sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48", "sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879",
"sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76", "sha256:2ab88a01cd180b5640ccc9c47232e31924d5f9967ab7edd7e5c91c68eee47a69",
"sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0", "sha256:338c417613f15596af9eb7a39353b60abec9d8ce1080aedba5ecee6a5d85f8d3",
"sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64", "sha256:3401b0d2ed9f726fadbfa35102e00d1b3547b73772a1de5508ef3bdbcb36afe7",
"sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685", "sha256:462105283de203df8de58a68c1bb4ba2a8a164097c2379f664fa81d6baf94b81",
"sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47", "sha256:4cd696aa712e6cd16898d63cf66139dc70d998f8121ab558f0e1936396dbc579",
"sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d", "sha256:4d06380e777dd6b35ee936f333d55b53dc4a8271036ff884c909cf6e94be8b6c",
"sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840", "sha256:61f4fbf3633cb0713437291b8848634ea97f89c7e849c2be17a665611e433f53",
"sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f", "sha256:6d4a6f30f611e657495cc81a07ff7aa8cd949144e7667c5d3e680d73ba7a70e4",
"sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971", "sha256:6f5fee77ec3384b934797f1873758f796dfb4f167e1296dc00f8b2e023ce6ee9",
"sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c", "sha256:75b5dbffc334e0beb4f6c503fb95e6d422770fd2d1b40a64898ea26d6c02742d",
"sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a", "sha256:7835f76a081787f0ca62a53504361b3869840a1620049b56d803a8cb3a9eeea3",
"sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de", "sha256:79bf405432428e989cad7b8bc60581963238f7645ae8a404f5dce90236cc0293",
"sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17", "sha256:8329635c0781927a2c6ae068461e19674c564e05b86736ab8eb29c420ee7dc20",
"sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4", "sha256:8586b177b4407f988731eb7f41967415b2197f35e2a6ee1a9b9b561f6323c8e9",
"sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521", "sha256:892e7fe32191960da559a14536768a62e83e87bbb867e1b9c643e7e0fbce2579",
"sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57", "sha256:91502bf27cbd5c83c95cfea291ef387469f2387508645602e1ca0fd8a4ba7548",
"sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b", "sha256:93b16b08f94c92cab88073ffd185070cdcb29f1b98df8b28e6649145b7f2c90d",
"sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282", "sha256:9c9441d57b0963cf8340268ad62fc83de61f1613034b79c2b1053046af0c5284",
"sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644", "sha256:ad8f9068f5972a46d50fe5f32c09d6ee11da69c560fcb1b4c3baea246ca4109b",
"sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475", "sha256:afb03f981fadb5aed1ac6e3dd34f0488e1a0875623d557b6fad09b97a942b38a",
"sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d", "sha256:b5ba058610e8289a07db2a57bce45a1793ec0d3d11db28c047aae2aa1a832572",
"sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da", "sha256:baa8be8aba3dd1e976e68677be68a960a633a6d44c325757aefaa4d66175050f",
"sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953", "sha256:c06455121a089252b5943ea682187a4e0a5cf0a3fb980eb8e7ce394b144430a9",
"sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2", "sha256:c1a9942e282cc9d3ed522cd3e3cab081149b27ea3bda72d6f61f84eaf88c1a63",
"sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e", "sha256:c488db059848702aff30aa1d90ef87928d4e72e4f00717343800546fdbff0a94",
"sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c", "sha256:cb5311d6ccbd22578c80028c5e292a7ab9adb91bd62c1982087fad75abe2e63d",
"sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc", "sha256:cbe91bc84be4e5ef0b1480d15c7b18e29c73bdfa33e07d3725da7d18e1b0aff2",
"sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64", "sha256:cc692c9ee18f0dd3214843779ba6b275ee4bb9b9a5745ba64265bce911aefd1a",
"sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74", "sha256:cc972d829ad5ef4d4c5fcabd2bbe2add84ce8236f64ba1c0c72185da3a273130",
"sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617", "sha256:ceb6534fcdfb5c503affb6b1130db7b5bfc8a0f77fa34880146f7a5c117987d0",
"sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3", "sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe",
"sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d", "sha256:db094a6a4ae6329ed322a8973f83630b12715654c197dd392410400a5bfa1a73",
"sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa", "sha256:df32ee0f4935a101e4b9a5f07b617d884a531ed5666671ff6ac66d2e8e8246d8",
"sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739", "sha256:e5af1feee71099ae2e3b086ec04f57f9950e1be9ecf6c420696fea7977b84738",
"sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8", "sha256:e814a4a5a1d95223b08cdb0f4f57029e8eab22ffdbae2f97107aeef28554517e",
"sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8", "sha256:f8cabc5fd0091976ab7b020f5708335033e422de25e20ddf9416bdce2b7e07d8",
"sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781", "sha256:fbc86ae8cc129c801e7baaafe3addf3c8d49c9c1597c44bdf2d78139707c3c62"
"sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58",
"sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9",
"sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c",
"sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd",
"sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e",
"sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"
], ],
"version": "==6.2" "markers": "python_version >= '3.7'",
"version": "==6.3.3"
}, },
"coveralls": { "coveralls": {
"hashes": [ "hashes": [
@@ -1401,6 +1499,33 @@
"index": "pypi", "index": "pypi",
"version": "==3.3.1" "version": "==3.3.1"
}, },
"cryptography": {
"hashes": [
"sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804",
"sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178",
"sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717",
"sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982",
"sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004",
"sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe",
"sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452",
"sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336",
"sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4",
"sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15",
"sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d",
"sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c",
"sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0",
"sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06",
"sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9",
"sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1",
"sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023",
"sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de",
"sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f",
"sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181",
"sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e",
"sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"
],
"version": "==37.0.2"
},
"django-coverage-plugin": { "django-coverage-plugin": {
"hashes": [ "hashes": [
"sha256:4206c85ffba0301f83aecc38e5b01b1b9a4b45a545d9456a827e3fabea18d952", "sha256:4206c85ffba0301f83aecc38e5b01b1b9a4b45a545d9456a827e3fabea18d952",
@@ -1420,8 +1545,17 @@
"sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5", "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5",
"sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142" "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.9.0" "version": "==1.9.0"
}, },
"h11": {
"hashes": [
"sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06",
"sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"
],
"markers": "python_version >= '3.6'",
"version": "==0.13.0"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
@@ -1437,11 +1571,20 @@
], ],
"version": "==1.1.1" "version": "==1.1.1"
}, },
"outcome": {
"hashes": [
"sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958",
"sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967"
],
"markers": "python_version >= '3.6'",
"version": "==1.1.0"
},
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
], ],
"markers": "python_version >= '3.6'",
"version": "==21.3" "version": "==21.3"
}, },
"pluggy": { "pluggy": {
@@ -1449,6 +1592,7 @@
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
], ],
"markers": "python_version >= '3.6'",
"version": "==1.0.0" "version": "==1.0.0"
}, },
"psutil": { "psutil": {
@@ -1490,6 +1634,7 @@
"sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
"sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.11.0" "version": "==1.11.0"
}, },
"pycodestyle": { "pycodestyle": {
@@ -1500,6 +1645,20 @@
"index": "pypi", "index": "pypi",
"version": "==2.8.0" "version": "==2.8.0"
}, },
"pycparser": {
"hashes": [
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
"sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
],
"version": "==2.21"
},
"pyopenssl": {
"hashes": [
"sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf",
"sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0"
],
"version": "==22.0.0"
},
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
@@ -1516,13 +1675,21 @@
"index": "pypi", "index": "pypi",
"version": "==2.2.3" "version": "==2.2.3"
}, },
"pysocks": {
"hashes": [
"sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
"sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
"sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
],
"version": "==1.7.1"
},
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db",
"sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.2.5" "version": "==7.0.1"
}, },
"pytest-cov": { "pytest-cov": {
"hashes": [ "hashes": [
@@ -1545,6 +1712,7 @@
"sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e", "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e",
"sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8" "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"
], ],
"markers": "python_version >= '3.6'",
"version": "==1.4.0" "version": "==1.4.0"
}, },
"pytest-reverse": { "pytest-reverse": {
@@ -1563,6 +1731,9 @@
"version": "==3.3.1" "version": "==3.3.1"
}, },
"pytest-xdist": { "pytest-xdist": {
"extras": [
"psutil"
],
"hashes": [ "hashes": [
"sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf", "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf",
"sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65" "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"
@@ -1580,9 +1751,18 @@
}, },
"selenium": { "selenium": {
"hashes": [ "hashes": [
"sha256:27e7b64df961d609f3d57237caa0df123abbbe22d038f2ec9e332fb90ec1a939" "sha256:866b6dd6c459210662bff922ee7c33162d21920fbf6811e8e5a52be3866a687f"
], ],
"version": "==4.1.0" "markers": "python_version ~= '3.7'",
"version": "==4.1.5"
},
"setuptools": {
"hashes": [
"sha256:5534570b9980fc650d45c62877ff603c7aaaf24893371708736cc016bd221c3c",
"sha256:ca6ba73b7fd5f734ae70ece8c4c1f7062b07f3352f6428f6277e27c8f5c64237"
],
"markers": "python_version >= '3.7'",
"version": "==62.2.0"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@@ -1592,6 +1772,21 @@
"index": "pypi", "index": "pypi",
"version": "==1.15.0" "version": "==1.15.0"
}, },
"sniffio": {
"hashes": [
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
],
"markers": "python_version >= '3.5'",
"version": "==1.2.0"
},
"sortedcontainers": {
"hashes": [
"sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
"sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
],
"version": "==2.4.0"
},
"splinter": { "splinter": {
"hashes": [ "hashes": [
"sha256:0fef9b632a0e220fe9cad7df1e2d5119932b6d2baf7843b80334248d6fda62d9", "sha256:0fef9b632a0e220fe9cad7df1e2d5119932b6d2baf7843b80334248d6fda62d9",
@@ -1599,19 +1794,29 @@
], ],
"version": "==0.17.0" "version": "==0.17.0"
}, },
"toml": {
"hashes": [
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"version": "==0.10.2"
},
"tomli": { "tomli": {
"hashes": [ "hashes": [
"sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224", "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1" "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
], ],
"version": "==2.0.0" "markers": "python_version >= '3.7'",
"version": "==2.0.1"
},
"trio": {
"hashes": [
"sha256:670a52d3115d0e879e1ac838a4eb999af32f858163e3a704fe4839de2a676070",
"sha256:fb2d48e4eab0dfb786a472cd514aaadc71e3445b203bc300bad93daa75d77c1a"
],
"markers": "python_version >= '3.7'",
"version": "==0.20.0"
},
"trio-websocket": {
"hashes": [
"sha256:5b558f6e83cc20a37c3b61202476c5295d1addf57bd65543364e0337e37ed2bc",
"sha256:a3d34de8fac26023eee701ed1e7bf4da9a8326b61a62934ec9e53b64970fd8fe"
],
"markers": "python_version >= '3.5'",
"version": "==0.9.2"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
@@ -1621,6 +1826,14 @@
"index": "pypi", "index": "pypi",
"version": "==1.26.8" "version": "==1.26.8"
}, },
"wsproto": {
"hashes": [
"sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b",
"sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"
],
"markers": "python_version >= '3.7'",
"version": "==1.1.0"
},
"zope.component": { "zope.component": {
"hashes": [ "hashes": [
"sha256:607628e4c84f7887a69a958542b5c304663e726b73aba0882e3a3f059bff14f3", "sha256:607628e4c84f7887a69a958542b5c304663e726b73aba0882e3a3f059bff14f3",

View File

@@ -9,9 +9,8 @@ from RIGS import models
def get_oembed(login_url, request, oembed_view, kwargs): def get_oembed(login_url, request, oembed_view, kwargs):
context = {} context = {}
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], context['oembed_url'] = f"{request.scheme}://{request.META['HTTP_HOST']}{reverse(oembed_view, kwargs=kwargs)}"
reverse(oembed_view, kwargs=kwargs)) context['login_url'] = f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}"
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
resp = render(request, 'login_redirect.html', context=context) resp = render(request, 'login_redirect.html', context=context)
return resp return resp
@@ -25,7 +24,7 @@ def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
if oembed_view is not None: if oembed_view is not None:
return get_oembed(login_url, request, oembed_view, kwargs) return get_oembed(login_url, request, oembed_view, kwargs)
else: else:
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path())) return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
_checklogin.__doc__ = view_func.__doc__ _checklogin.__doc__ = view_func.__doc__
_checklogin.__dict__ = view_func.__dict__ _checklogin.__dict__ = view_func.__dict__
@@ -55,7 +54,7 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
if oembed_view is not None: if oembed_view is not None:
return get_oembed(login_url, request, oembed_view, kwargs) return get_oembed(login_url, request, oembed_view, kwargs)
else: else:
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path())) return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
else: else:
resp = render(request, '403.html') resp = render(request, '403.html')
resp.status_code = 403 resp.status_code = 403

View File

@@ -68,6 +68,7 @@ INSTALLED_APPS = (
'reversion', 'reversion',
'widget_tweaks', 'widget_tweaks',
'hcaptcha', 'hcaptcha',
'massadmin',
) )
MIDDLEWARE = ( MIDDLEWARE = (

View File

@@ -10,6 +10,7 @@ from pytest_django.asserts import assertTemplateUsed, assertInHTML
from PyRIGS import urls from PyRIGS import urls
from RIGS.models import Event, Profile from RIGS.models import Event, Profile
from assets.models import Asset from assets.models import Asset
from training.tests.test_unit import get_response
from django.db import connection from django.db import connection
from django.template.defaultfilters import striptags from django.template.defaultfilters import striptags
from django.urls.exceptions import NoReverseMatch from django.urls.exceptions import NoReverseMatch
@@ -135,3 +136,11 @@ def test_keyholder_access(client):
assertContains(response, 'View Revision History') assertContains(response, 'View Revision History')
client.logout() client.logout()
call_command('deleteSampleData') call_command('deleteSampleData')
def test_search(admin_client, admin_user):
url = reverse('search')
response = admin_client.get(url, {'q': "Definetelynothingfoundifwesearchthis"})
assertContains(response, "No results found")
response = admin_client.get(url, {'q': admin_user.first_name})
assertContains(response, admin_user.first_name)

View File

@@ -23,10 +23,12 @@ urlpatterns = [
name="api_secure"), name="api_secure"),
path('closemodal/', views.CloseModal.as_view(), name='closemodal'), path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
path('search/', login_required(views.Search.as_view()), name='search'),
path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'), path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'),
path('', include('users.urls')), path('', include('users.urls')),
path('admin/', include('massadmin.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")), path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
] ]

View File

@@ -1,7 +1,18 @@
import datetime import datetime
import operator import operator
from functools import reduce import re
import urllib.error
import urllib.parse
import urllib.request
from functools import reduce
from itertools import chain
from io import BytesIO
from PyPDF2 import PdfFileMerger, PdfFileReader
from z3c.rml import rml2pdf
from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.core import serializers from django.core import serializers
@@ -12,6 +23,8 @@ from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy, reverse, NoReverseMatch from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.views import generic from django.views import generic
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from django.template.loader import get_template
from django.utils import timezone
from RIGS import models from RIGS import models
from assets import models as asset_models from assets import models as asset_models
@@ -22,6 +35,13 @@ def is_ajax(request):
return request.headers.get('x-requested-with') == 'XMLHttpRequest' return request.headers.get('x-requested-with') == 'XMLHttpRequest'
def get_related(form, context): # Get some other objects to include in the form. Used when there are errors but also nice and quick.
for field, model in form.related_models.items():
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
class Index(generic.TemplateView): # Displays the current rig count along with a few other bits and pieces class Index(generic.TemplateView): # Displays the current rig count along with a few other bits and pieces
template_name = 'index.html' template_name = 'index.html'
@@ -120,7 +140,7 @@ class SecureAPIRequest(generic.View):
'text': o.name, 'text': o.name,
} }
try: # See if there is a valid update URL try: # See if there is a valid update URL
data['update'] = reverse("%s_update" % model, kwargs={'pk': o.pk}) data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
except NoReverseMatch: except NoReverseMatch:
pass pass
results.append(data) results.append(data)
@@ -182,20 +202,7 @@ class GenericListView(generic.ListView):
return context return context
def get_queryset(self): def get_queryset(self):
q = self.request.GET.get('q', "") object_list = self.model.objects.search(query=self.request.GET.get('q', ""))
filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(
phone__startswith=q) | Q(phone__endswith=q)
# try and parse an int
try:
val = int(q)
filter = filter | Q(pk=val)
except: # noqa
# not an integer
pass
object_list = self.model.objects.filter(filter)
orderBy = self.request.GET.get('orderBy', "name") orderBy = self.request.GET.get('orderBy', "name")
if orderBy != "": if orderBy != "":
@@ -236,6 +243,53 @@ class GenericCreateView(generic.CreateView):
return context return context
class Search(generic.ListView):
template_name = 'search_results.html'
paginate_by = 20
count = 0
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['count'] = self.count or 0
context['query'] = self.request.GET.get('q')
context['page_title'] = f"{context['count']} search results for <b>{context['query']}</b>"
return context
def get_queryset(self):
request = self.request
query = request.GET.get('q', None)
if query is not None:
event_results = models.Event.objects.search(query)
person_results = models.Person.objects.search(query)
organisation_results = models.Organisation.objects.search(query)
venue_results = models.Venue.objects.search(query)
invoice_results = models.Invoice.objects.search(query)
asset_results = asset_models.Asset.objects.search(query)
supplier_results = asset_models.Supplier.objects.search(query)
trainee_results = training_models.Trainee.objects.search(query)
training_item_results = training_models.TrainingItem.objects.search(query)
# combine querysets
queryset_chain = chain(
event_results,
person_results,
organisation_results,
venue_results,
invoice_results,
asset_results,
supplier_results,
trainee_results,
training_item_results,
)
qs = sorted(queryset_chain,
key=lambda instance: instance.pk,
reverse=True)
self.count = len(qs) # since qs is actually a list
return qs
return models.Event.objects.none() # just an empty queryset as default
class SearchHelp(generic.TemplateView): class SearchHelp(generic.TemplateView):
template_name = 'search_help.html' template_name = 'search_help.html'
@@ -265,3 +319,47 @@ class OEmbedView(generic.View):
} }
return JsonResponse(data) return JsonResponse(data)
class PrintView(generic.View):
append_terms = False
def get_context_data(self, **kwargs):
obj = get_object_or_404(self.model, pk=self.kwargs['pk'])
user_str = f"by {self.request.user.name} " if self.request.user is not None else ""
time = timezone.now().strftime('%d/%m/%Y %H:%I')
object_name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', obj.name)
context = {
'object': obj,
'current_user': self.request.user,
'object_name': object_name,
'info_string': f"[Paperwork generated {user_str}on {time} - {obj.current_version_id}]",
}
return context
def get(self, request, pk):
template = get_template(self.template_name)
merger = PdfFileMerger()
context = self.get_context_data()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
if self.append_terms:
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(BytesIO(terms.read()))
merged = BytesIO()
merger.write(merged)
response = HttpResponse(content_type='application/pdf')
f = context['filename']
response['Content-Disposition'] = f'filename="{f}"'
response.write(merged.getvalue())
return response

View File

@@ -22,13 +22,98 @@ admin.site.register(models.EventItem, VersionAdmin)
admin.site.register(models.Invoice, VersionAdmin) admin.site.register(models.Invoice, VersionAdmin)
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
def merge_model_instances(primary_object, alias_objects):
"""
Merge several model instances into one, the `primary_object`.
Use this function to merge model objects and migrate all of the related
fields from the alias objects the primary object.
"""
# get related fields
related_fields = list(filter(
lambda x: x.is_relation is True,
primary_object._meta.get_fields()))
many_to_many_fields = list(filter(
lambda x: x.many_to_many is True, related_fields))
related_fields = list(filter(
lambda x: x.many_to_many is False, related_fields))
# Loop through all alias objects and migrate their references to the
# primary object
deleted_objects = []
deleted_objects_count = 0
for alias_object in alias_objects:
# Migrate all foreign key references from alias object to primary
# object.
for many_to_many_field in many_to_many_fields:
alias_varname = many_to_many_field.name
related_objects = getattr(alias_object, alias_varname)
for obj in related_objects.all():
try:
# Handle regular M2M relationships.
getattr(alias_object, alias_varname).remove(obj)
getattr(primary_object, alias_varname).add(obj)
except AttributeError:
# Handle M2M relationships with a 'through' model.
# This does not delete the 'through model.
# TODO: Allow the user to delete a duplicate 'through' model.
through_model = getattr(alias_object, alias_varname).through
kwargs = {
many_to_many_field.m2m_reverse_field_name(): obj,
many_to_many_field.m2m_field_name(): alias_object,
}
through_model_instances = through_model.objects.filter(**kwargs)
for instance in through_model_instances:
# Re-attach the through model to the primary_object
setattr(
instance,
many_to_many_field.m2m_field_name(),
primary_object)
instance.save()
# TODO: Here, try to delete duplicate instances that are
# disallowed by a unique_together constraint
for related_field in related_fields:
if related_field.one_to_many:
with transaction.atomic():
try:
alias_varname = related_field.get_accessor_name()
related_objects = getattr(alias_object, alias_varname)
for obj in related_objects.all():
field_name = related_field.field.name
setattr(obj, field_name, primary_object)
obj.save()
except IntegrityError:
pass # Skip to avoid integrity error from unique_together
elif related_field.one_to_one or related_field.many_to_one:
alias_varname = related_field.name
if hasattr(alias_object, alias_varname):
related_object = getattr(alias_object, alias_varname)
primary_related_object = getattr(primary_object, alias_varname)
if primary_related_object is None:
setattr(primary_object, alias_varname, related_object)
primary_object.save()
elif related_field.one_to_one:
related_object.delete()
if alias_object.id:
deleted_objects += [alias_object]
alias_object.delete()
deleted_objects_count += 1
return primary_object, deleted_objects, deleted_objects_count
class AssociateAdmin(VersionAdmin): class AssociateAdmin(VersionAdmin):
search_fields = ['id', 'name'] search_fields = ['id', 'name']
list_display_links = ['id', 'name'] list_display_links = ['id', 'name']
actions = ['merge'] actions = ['merge']
def get_queryset(self, request): def get_queryset(self, request):
return super(AssociateAdmin, self).get_queryset(request).annotate(event_count=Count('event')) return super().get_queryset(request).annotate(event_count=Count('event'))
def number_of_events(self, obj): def number_of_events(self, obj):
return obj.latest_events.count() return obj.latest_events.count()
@@ -44,38 +129,10 @@ class AssociateAdmin(VersionAdmin):
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR) self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
return return
with transaction.atomic(), reversion.create_revision(): primary_object, deleted_objects, deleted_objects_count = merge_model_instances(master_object, queryset.exclude(pk=master_object_pk).all())
for obj in queryset.exclude(pk=master_object_pk): reversion.set_comment('Merging Objects')
# If we're merging profiles, merge their training information self.message_user(request, f"Objects successfully merged. {deleted_objects_count} old objects deleted.")
if hasattr(obj, 'event_mic'):
events = obj.event_mic.all()
for event in events:
master_object.event_mic.add(event)
for qual in obj.qualifications_obtained.all():
try:
with transaction.atomic():
master_object.qualifications_obtained.add(qual)
except IntegrityError:
existing_qual = master_object.qualifications_obtained.get(item=qual.item, depth=qual.depth)
existing_qual.notes += qual.notes
existing_qual.save()
for level in obj.level_qualifications.all():
try:
with transaction.atomic():
master_object.level_qualifications.add(level)
except IntegrityError:
continue # Exists, oh well
else:
events = obj.event_set.all()
for event in events:
master_object.event_set.add(event)
obj.delete()
reversion.set_comment('Merging Objects')
self.message_user(request, "Objects successfully merged.")
return
else: # Present the confirmation screen else: # Present the confirmation screen
class TempForm(ModelForm): class TempForm(ModelForm):
class Meta: class Meta:
model = queryset.model model = queryset.model

View File

@@ -153,6 +153,10 @@ class EventAuthorisationRequestForm(forms.Form):
class EventRiskAssessmentForm(forms.ModelForm): class EventRiskAssessmentForm(forms.ModelForm):
related_models = {
'power_mic': models.Profile,
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for name, field in self.fields.items(): for name, field in self.fields.items():
@@ -172,9 +176,9 @@ class EventRiskAssessmentForm(forms.ModelForm):
unexpected_values = [] unexpected_values = []
for field, value in models.RiskAssessment.expected_values.items(): for field, value in models.RiskAssessment.expected_values.items():
if self.cleaned_data.get(field) != value: if self.cleaned_data.get(field) != value:
unexpected_values.append("<li>{}</li>".format(self._meta.model._meta.get_field(field).help_text)) unexpected_values.append(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>")
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'): if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
raise forms.ValidationError("Your answers to these questions: <ul>{}</ul> require consulting with a supervisor.".format(''.join([str(elem) for elem in unexpected_values])), code='unusual_answers') raise forms.ValidationError(f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.", code='unusual_answers')
return super(EventRiskAssessmentForm, self).clean() return super(EventRiskAssessmentForm, self).clean()
class Meta: class Meta:
@@ -235,9 +239,9 @@ class EventChecklistForm(forms.ModelForm):
pk = int(key.split('_')[1]) pk = int(key.split('_')[1])
for field in other_fields: for field in other_fields:
value = self.data['{}_{}'.format(field, pk)] value = self.data[f'{field}_{pk}']
if value == '': if value == '':
raise forms.ValidationError('Add a {} to crewmember {}'.format(field, pk), code='{}_mismatch'.format(field)) raise forms.ValidationError(f'Add a {field} to crewmember {pk}', code=f'{field}_mismatch')
try: try:
item = models.EventChecklistCrew.objects.get(pk=pk) item = models.EventChecklistCrew.objects.get(pk=pk)

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-10-20 23:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0044_profile_is_supervisor'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='is_approved',
field=models.BooleanField(default=False, help_text='Designates whether a staff member has approved this user.', verbose_name='Approval Status'),
),
]

View File

@@ -8,6 +8,7 @@ from urllib.parse import urlparse
import pytz import pytz
from django import forms from django import forms
from django.db.models import Q, F
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -20,11 +21,22 @@ from reversion.models import Version
from versioning.versioning import RevisionMixin from versioning.versioning import RevisionMixin
def filter_by_pk(filt, query):
# try and parse an int
try:
val = int(query)
filt = filt | Q(pk=val)
except: # noqa
# not an integer
pass
return filt
class Profile(AbstractUser): class Profile(AbstractUser):
initials = models.CharField(max_length=5, null=True, blank=False) initials = models.CharField(max_length=5, null=True, blank=False)
phone = models.CharField(max_length=13, blank=True, default='') phone = models.CharField(max_length=13, blank=True, default='')
api_key = models.CharField(max_length=40, blank=True, editable=False, default='') api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
is_approved = models.BooleanField(default=False) is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.")
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that... # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
last_emailed = models.DateTimeField(blank=True, null=True) last_emailed = models.DateTimeField(blank=True, null=True)
dark_theme = models.BooleanField(default=False) dark_theme = models.BooleanField(default=False)
@@ -51,7 +63,7 @@ class Profile(AbstractUser):
def name(self): def name(self):
name = self.get_full_name() name = self.get_full_name()
if self.initials: if self.initials:
name += ' "{}"'.format(self.initials) name += f' "{self.initials}"'
return name return name
@property @property
@@ -70,15 +82,28 @@ class Profile(AbstractUser):
return self.name return self.name
class ContactableManager(models.Manager):
def search(self, query=None):
qs = self.get_queryset()
if query is not None:
or_lookup = Q(name__icontains=query) | Q(email__icontains=query) | Q(address__icontains=query) | Q(notes__icontains=query) | Q(
phone__startswith=query) | Q(phone__endswith=query)
or_lookup = filter_by_pk(or_lookup, query)
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
class Person(models.Model, RevisionMixin): class Person(models.Model, RevisionMixin):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, default='') phone = models.CharField(max_length=15, blank=True, default='')
email = models.EmailField(blank=True, default='') email = models.EmailField(blank=True, default='')
address = models.TextField(blank=True, default='') address = models.TextField(blank=True, default='')
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, default='')
objects = ContactableManager()
def __str__(self): def __str__(self):
string = self.name string = self.name
if self.notes is not None: if self.notes is not None:
@@ -110,12 +135,12 @@ class Organisation(models.Model, RevisionMixin):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, default='') phone = models.CharField(max_length=15, blank=True, default='')
email = models.EmailField(blank=True, default='') email = models.EmailField(blank=True, default='')
address = models.TextField(blank=True, default='') address = models.TextField(blank=True, default='')
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, default='')
union_account = models.BooleanField(default=False) union_account = models.BooleanField(default=False)
objects = ContactableManager()
def __str__(self): def __str__(self):
string = self.name string = self.name
if self.notes is not None: if self.notes is not None:
@@ -184,9 +209,10 @@ class Venue(models.Model, RevisionMixin):
email = models.EmailField(blank=True, default='') email = models.EmailField(blank=True, default='')
three_phase_available = models.BooleanField(default=False) three_phase_available = models.BooleanField(default=False)
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, default='')
address = models.TextField(blank=True, default='') address = models.TextField(blank=True, default='')
objects = ContactableManager()
def __str__(self): def __str__(self):
string = self.name string = self.name
if self.notes and len(self.notes) > 0: if self.notes and len(self.notes) > 0:
@@ -260,6 +286,23 @@ class EventManager(models.Manager):
return events return events
def search(self, query=None):
qs = self.get_queryset()
if query is not None:
or_lookup = Q(name__icontains=query) | Q(description__icontains=query) | Q(notes__icontains=query)
or_lookup = filter_by_pk(or_lookup, query)
try:
if query[0] == "N":
val = int(query[1:])
or_lookup = Q(pk=val) # If string is N###### then do a simple PK filter
except: # noqa
pass
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
@reversion.register(follow=['items']) @reversion.register(follow=['items'])
class Event(models.Model, RevisionMixin): class Event(models.Model, RevisionMixin):
@@ -314,10 +357,8 @@ class Event(models.Model, RevisionMixin):
def display_id(self): def display_id(self):
if self.pk: if self.pk:
if self.is_rig: if self.is_rig:
return str("N%05d" % self.pk) return f"N{self.pk:05d}"
return self.pk return self.pk
return "????" return "????"
# Calculated values # Calculated values
@@ -530,6 +571,34 @@ class InvoiceManager(models.Manager):
query = self.raw(sql) query = self.raw(sql)
return query return query
def search(self, query=None):
qs = self.get_queryset()
if query is not None:
or_lookup = Q(event__name__icontains=query)
or_lookup = filter_by_pk(or_lookup, query)
# try and parse an int
try:
val = int(query)
or_lookup = or_lookup | Q(event__pk=val)
except: # noqa
# not an integer
pass
try:
if query[0] == "N":
val = int(query[1:])
or_lookup = Q(event__pk=val) # If string is Nxxxxx then filter by event number
elif query[0] == "#":
val = int(query[1:])
or_lookup = Q(pk=val) # If string is #xxxxx then filter by invoice number
except: # noqa
pass
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
@reversion.register(follow=['payment_set']) @reversion.register(follow=['payment_set'])
class Invoice(models.Model, RevisionMixin): class Invoice(models.Model, RevisionMixin):
@@ -569,14 +638,14 @@ class Invoice(models.Model, RevisionMixin):
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return f"#{self.display_id} for Event {self.event.display_id}" return f"{self.display_id} for Event {self.event.display_id}"
def __str__(self): def __str__(self):
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance) return f"{self.display_id}: {self.event}{self.balance:.2f})"
@property @property
def display_id(self): def display_id(self):
return "{:05d}".format(self.pk) return f"#{self.pk:05d}"
class Meta: class Meta:
ordering = ['-invoice_date'] ordering = ['-invoice_date']
@@ -681,7 +750,7 @@ class RiskAssessment(models.Model, RevisionMixin):
'contractors': False, 'contractors': False,
'other_companies': False, 'other_companies': False,
'crew_fatigue': False, 'crew_fatigue': False,
'big_power': False, # 'big_power': False Doesn't require checking with a super either way
'generators': False, 'generators': False,
'other_companies_power': False, 'other_companies_power': False,
'nonstandard_equipment_power': False, 'nonstandard_equipment_power': False,
@@ -723,15 +792,22 @@ class RiskAssessment(models.Model, RevisionMixin):
else: else:
return self.SMALL[0] return self.SMALL[0]
def get_event_size_display(self):
return self.SIZES[self.event_size][1] + " Event"
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return str(self.event) return str(self.event)
@property
def name(self):
return str(self)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('ra_detail', kwargs={'pk': self.pk}) return reverse('ra_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return "%i - %s" % (self.pk, self.event) return f"{self.pk} | {self.event}"
@reversion.register(follow=['vehicles', 'crew']) @reversion.register(follow=['vehicles', 'crew'])
@@ -813,7 +889,7 @@ class EventChecklist(models.Model, RevisionMixin):
return reverse('ec_detail', kwargs={'pk': self.pk}) return reverse('ec_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return "%i - %s" % (self.pk, self.event) return f"{self.pk} - {self.event}"
@reversion.register @reversion.register
@@ -825,7 +901,7 @@ class EventChecklistVehicle(models.Model, RevisionMixin):
reversion_hide = True reversion_hide = True
def __str__(self): def __str__(self):
return "{} driven by {}".format(self.vehicle, str(self.driver)) return f"{self.vehicle} driven by {self.driver}"
@reversion.register @reversion.register
@@ -843,4 +919,4 @@ class EventChecklistCrew(models.Model, RevisionMixin):
raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.') raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.')
def __str__(self): def __str__(self):
return "{} ({})".format(str(self.crewmember), self.role) return f"{self.crewmember} ({self.role})"

View File

@@ -54,7 +54,7 @@ def send_eventauthorisation_success_email(instance):
elif instance.event.organisation is not None and instance.email == instance.event.organisation.email: elif instance.event.organisation is not None and instance.email == instance.event.organisation.email:
context['to_name'] = instance.event.organisation.name context['to_name'] = instance.event.organisation.name
subject = "N%05d | %s - Event Authorised" % (instance.event.pk, instance.event.name) subject = f"{instance.event.display_id} | {instance.event.name} - Event Authorised"
client_email = EmailMultiAlternatives( client_email = EmailMultiAlternatives(
subject, subject,
@@ -70,7 +70,7 @@ def send_eventauthorisation_success_email(instance):
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name) escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName), client_email.attach(f'{instance.event.display_id} - {escapedEventName} - CONFIRMATION.pdf',
merged.getvalue(), merged.getvalue(),
'application/pdf' 'application/pdf'
) )
@@ -116,7 +116,7 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
} }
email = EmailMultiAlternatives( email = EmailMultiAlternatives(
"%s new users awaiting approval on RIGS" % (context['number_of_users']), f"{context['number_of_users']} new users awaiting approval on RIGS",
get_template("admin_awaiting_approval.txt").render(context), get_template("admin_awaiting_approval.txt").render(context),
to=[admin.email], to=[admin.email],
reply_to=[user.email], reply_to=[user.email],

View File

@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE document SYSTEM "rml.dtd">
<document filename="{{filename}}">
<docinit>
<registerTTFont faceName="OpenSans" fileName="static/fonts/OpenSans-Regular.tff"/>
<registerTTFont faceName="OpenSans-Bold" fileName="static/fonts/OpenSans-Bold.tff"/>
<registerFontFamily name="OpenSans" bold="OpenSans-Bold" boldItalic="OpenSans-Bold"/>
</docinit>
<stylesheet>
<initialize>
<color id="LightGray" RGB="#D3D3D3"/>
<color id="DarkGray" RGB="#707070"/>
</initialize>
<paraStyle name="style.para" fontName="OpenSans" />
<paraStyle name="blockPara" spaceAfter="5" spaceBefore="5"/>
<paraStyle name="style.Heading1" fontName="OpenSans" fontSize="16" leading="18" spaceAfter="0"/>
<paraStyle name="style.Heading2" fontName="OpenSans-Bold" fontSize="10" spaceAfter="2"/>
<paraStyle name="style.Heading3" fontName="OpenSans" fontSize="10" spaceAfter="0"/>
<paraStyle name="center" alignment="center"/>
<paraStyle name="page-head" alignment="center" fontName="OpenSans-Bold" fontSize="16" leading="18" spaceAfter="0"/>
<paraStyle name="style.event_description" fontName="OpenSans" textColor="DarkGray" />
<paraStyle name="style.item_description" fontName="OpenSans" textColor="DarkGray" leftIndent="10" />
<paraStyle name="style.specific_description" fontName="OpenSans" textColor="DarkGray" fontSize="10" />
<paraStyle name="style.times" fontName="OpenSans" fontSize="10" />
<paraStyle name="style.head_titles" fontName="OpenSans-Bold" fontSize="10" />
<paraStyle name="style.head_numbers" fontName="OpenSans" fontSize="10" />
<blockTableStyle id="eventSpecifics">
<blockValign value="top"/>
<lineStyle kind="LINEAFTER" colorName="LightGrey" start="0,0" stop="1,0" thickness="1"/>
</blockTableStyle>
<blockTableStyle id="headLayout">
<blockValign value="top"/>
</blockTableStyle>
<blockTableStyle id="eventDetails">
<blockValign value="top"/>
<blockTopPadding start="0,0" stop="-1,0" length="0"/>
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
</blockTableStyle>
<blockTableStyle id="itemTable">
<blockValign value="top"/>
<lineStyle kind="LINEBELOW" colorName="LightGrey" start="0,0" stop="-1,-1" thickness="1"/>
{#<lineStyle kind="box" colorName="black" thickness="1" start="0,0" stop="-1,-1"/>#}
</blockTableStyle>
<blockTableStyle id="totalTable">
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
<lineStyle kind="LINEBELOW" colorName="LightGrey" start="-2,0" stop="-1,-1" thickness="1"/>
{# <lineStyle cap="default" kind="grid" colorName="black" thickness="1" start="1,0" stop="-1,-1"/> #}
</blockTableStyle>
<blockTableStyle id="infoTable" keepWithNext="true">
<blockLeftPadding start="0,0" stop="-1,-1" length="0"/>
</blockTableStyle>
<blockTableStyle id="paymentTable">
<blockBackground colorName="LightGray" start="0,1" stop="3,1"/>
<blockFont name="OpenSans-Bold" start="0,1" stop="0,1"/>
<blockFont name="OpenSans-Bold" start="2,1" stop="2,1"/>
<lineStyle kind="outline" colorName="black" thickness="1" start="0,1" stop="3,1"/>
</blockTableStyle>
<blockTableStyle id="signatureTable">
<blockTopPadding length="20" />
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
<lineStyle kind="linebelow" start="1,0" stop="1,0" colorName="black"/>
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
</blockTableStyle>
<listStyle name="ol"
bulletFormat="%s."
bulletFontSize="10" />
<listStyle name="ul"
start="bulletchar"
bulletFontSize="10"/>
</stylesheet>
<template title="{{filename}}"> {# Note: page is 595x842 points (1 point=1/72in) #}
<pageTemplate id="Headed" >
<pageGraphics>
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
<image file="static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
{# logo positioned 42 from left, 33 from top #}
<image file="static/imgs/paperwork/tec-logo.jpg" x="42" y="719" height="90" width="84"/>
<setFont name="OpenSans-Bold" size="22.5" leading="10"/>
<drawString x="137" y="780">TEC PA &amp; Lighting</drawString>
<setFont name="OpenSans" size="9"/>
<drawString x="137" y="760">Portland Building, University Park, Nottingham, NG7 2RD</drawString>
<drawString x="137" y="746">www.nottinghamtec.co.uk</drawString>
<drawString x="265" y="746">info@nottinghamtec.co.uk</drawString>
<drawString x="137" y="732">Phone: (0115) 846 8720</drawString>
<setFont name="OpenSans" size="10" />
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
<setFont name="OpenSans" size="7" />
<drawCenteredString x="302.5" y="26">
{{info_string}}
</drawCenteredString>
</pageGraphics>
<frame id="main" x1="50" y1="65" width="495" height="645"/>
</pageTemplate>
<pageTemplate id="Main">
<pageGraphics>
<image file="static/imgs/paperwork/corner-tr.jpg" x="395" y="642" height="200" width="200"/>
<image file="static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
<setFont name="OpenSans" size="10"/>
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
<setFont name="OpenSans" size="7" />
<drawCenteredString x="302.5" y="26">
{{info_string}}
</drawCenteredString>
</pageGraphics>
<frame id="main" x1="50" y1="65" width="495" height="727"/>
</pageTemplate>
</template>
<story firstPageTemplate="Headed">
<setNextFrame name="main"/>
<nextFrame/>
{% block content %}
{% endblock %}
</story>
</document>

View File

@@ -27,15 +27,12 @@
calendar = new FullCalendar.Calendar(calendarEl, { calendar = new FullCalendar.Calendar(calendarEl, {
themeSystem: 'bootstrap', themeSystem: 'bootstrap',
//defaultView: 'dayGridMonth', This is now default
aspectRatio: 1.5, aspectRatio: 1.5,
eventTimeFormat: { eventTimeFormat: {
'hour': '2-digit', 'hour': '2-digit',
'minute': '2-digit', 'minute': '2-digit',
'hour12': false 'hour12': false
}, },
//nowIndicator: true,
//firstDay: 1,
headerToolbar: false, headerToolbar: false,
editable: false, editable: false,
dayMaxEventRows: true, // allow "more" link when too many events dayMaxEventRows: true, // allow "more" link when too many events
@@ -58,8 +55,10 @@
}; };
$(doc).each(function() { $(doc).each(function() {
end = $(this).attr('latest') end = $(this).attr('latest')
allDay = false
if(end.indexOf("T") < 0){ //If latest does not contain a time if(end.indexOf("T") < 0){ //If latest does not contain a time
end = moment(end).add(1, 'days') //End date is non-inclusive, so add a day end = moment(end + " 23:59").format("YYYY-MM-DD[T]HH:mm:ss")
allDay = true
} }
thisEvent = { thisEvent = {
@@ -67,7 +66,8 @@
'end': end, 'end': end,
'className': 'modal-href', 'className': 'modal-href',
'title': $(this).attr('title'), 'title': $(this).attr('title'),
'url': $(this).attr('url') 'url': $(this).attr('url'),
'allDay': allDay
} }
if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){ if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){

View File

@@ -122,7 +122,7 @@
<div class="col-sm-8"> <div class="col-sm-8">
<div class="row"> <div class="row">
<div class="col-sm-9 col-md-7 col-lg-8"> <div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}"> <select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}">
{% if person %} {% if person %}
<option value="{{form.person.value}}" selected="selected" data-update_url="{% url 'person_update' form.person.value %}">{{ person }}</option> <option value="{{form.person.value}}" selected="selected" data-update_url="{% url 'person_update' form.person.value %}">{{ person }}</option>
{% endif %} {% endif %}
@@ -149,7 +149,7 @@
<div class="col-sm-8"> <div class="col-sm-8">
<div class="row"> <div class="row">
<div class="col-sm-9 col-md-7 col-lg-8"> <div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.organisation.id_for_label }}" name="{{ form.organisation.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}" > <select id="{{ form.organisation.id_for_label }}" name="{{ form.organisation.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}" >
{% if organisation %} {% if organisation %}
<option value="{{form.organisation.value}}" selected="selected" data-update_url="{% url 'organisation_update' form.organisation.value %}">{{ organisation }}</option> <option value="{{form.organisation.value}}" selected="selected" data-update_url="{% url 'organisation_update' form.organisation.value %}">{{ organisation }}</option>
{% endif %} {% endif %}
@@ -207,7 +207,7 @@
<div class="col-sm-8"> <div class="col-sm-8">
<div class="row"> <div class="row">
<div class="col-sm-9 col-md-7 col-lg-8"> <div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}"> <select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %} {% if venue %}
<option value="{{form.venue.value}}" selected="selected" data-update_url="{% url 'venue_update' form.venue.value %}">{{ venue }}</option> <option value="{{form.venue.value}}" selected="selected" data-update_url="{% url 'venue_update' form.venue.value %}">{{ venue }}</option>
{% endif %} {% endif %}
@@ -277,10 +277,8 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-offset-4 col-sm-8"> <div class="col-sm-offset-4 col-sm-8">
<div class="checkbox">
<label data-toggle="tooltip" title="Mark this event as a dry-hire, so it needs to be checked in at the end"> <label data-toggle="tooltip" title="Mark this event as a dry-hire, so it needs to be checked in at the end">
{% render_field form.dry_hire %}{{ form.dry_hire.label }} {{ form.dry_hire.label }} {% render_field form.dry_hire %}
</label>
</div> </div>
</div> </div>
</div> </div>
@@ -302,7 +300,7 @@
class="col-sm-4 col-form-label">{{ form.mic.label }}</label> class="col-sm-4 col-form-label">{{ form.mic.label }}</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select id="{{ form.mic.id_for_label }}" name="{{ form.mic.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials"> <select id="{{ form.mic.id_for_label }}" name="{{ form.mic.name }}" class="px-0 selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if mic %} {% if mic %}
<option value="{{form.mic.value}}" selected="selected" >{{ mic.name }}</option> <option value="{{form.mic.value}}" selected="selected" >{{ mic.name }}</option>
{% endif %} {% endif %}
@@ -316,7 +314,7 @@
class="col-sm-4 col-form-label">{{ form.checked_in_by.label }}</label> class="col-sm-4 col-form-label">{{ form.checked_in_by.label }}</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select id="{{ form.checked_in_by.id_for_label }}" name="{{ form.checked_in_by.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials"> <select id="{{ form.checked_in_by.id_for_label }}" name="{{ form.checked_in_by.name }}" class="px-0 selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if checked_in_by %} {% if checked_in_by %}
<option value="{{form.checked_in_by.value}}" selected="selected" >{{ checked_in_by.name }}</option> <option value="{{form.checked_in_by.value}}" selected="selected" >{{ checked_in_by.name }}</option>
{% endif %} {% endif %}

View File

@@ -1,136 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?> {% extends 'base_print.xml' %}
<!DOCTYPE document SYSTEM "rml.dtd">
<document filename="{{filename}}">
<docinit>
<registerTTFont faceName="OpenSans" fileName="static/fonts/OpenSans-Regular.tff"/>
<registerTTFont faceName="OpenSans-Bold" fileName="static/fonts/OpenSans-Bold.tff"/>
<registerFontFamily name="OpenSans" bold="OpenSans-Bold" boldItalic="OpenSans-Bold"/>
</docinit>
<stylesheet> {% block content %}
<initialize> {% include "event_print_page.xml" %}
<color id="LightGray" RGB="#D3D3D3"/> {% endblock %}
<color id="DarkGray" RGB="#707070"/>
</initialize>
<paraStyle name="style.para" fontName="OpenSans" />
<paraStyle name="blockPara" spaceAfter="5" spaceBefore="5"/>
<paraStyle name="style.Heading1" fontName="OpenSans" fontSize="16" leading="18" spaceAfter="0"/>
<paraStyle name="style.Heading2" fontName="OpenSans-Bold" fontSize="10" spaceAfter="2"/>
<paraStyle name="style.Heading3" fontName="OpenSans" fontSize="10" spaceAfter="0"/>
<paraStyle name="center" alignment="center"/>
<paraStyle name="page-head" alignment="center" fontName="OpenSans-Bold" fontSize="16" leading="18" spaceAfter="0"/>
<paraStyle name="style.event_description" fontName="OpenSans" textColor="DarkGray" />
<paraStyle name="style.item_description" fontName="OpenSans" textColor="DarkGray" leftIndent="10" />
<paraStyle name="style.specific_description" fontName="OpenSans" textColor="DarkGray" fontSize="10" />
<paraStyle name="style.times" fontName="OpenSans" fontSize="10" />
<paraStyle name="style.head_titles" fontName="OpenSans-Bold" fontSize="10" />
<paraStyle name="style.head_numbers" fontName="OpenSans" fontSize="10" />
<blockTableStyle id="eventSpecifics">
<blockValign value="top"/>
<lineStyle kind="LINEAFTER" colorName="LightGrey" start="0,0" stop="1,0" thickness="1"/>
</blockTableStyle>
<blockTableStyle id="headLayout">
<blockValign value="top"/>
</blockTableStyle>
<blockTableStyle id="eventDetails">
<blockValign value="top"/>
<blockTopPadding start="0,0" stop="-1,0" length="0"/>
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
</blockTableStyle>
<blockTableStyle id="itemTable">
<blockValign value="top"/>
<lineStyle kind="LINEBELOW" colorName="LightGrey" start="0,0" stop="-1,-1" thickness="1"/>
{#<lineStyle kind="box" colorName="black" thickness="1" start="0,0" stop="-1,-1"/>#}
</blockTableStyle>
<blockTableStyle id="totalTable">
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
<lineStyle kind="LINEBELOW" colorName="LightGrey" start="-2,0" stop="-1,-1" thickness="1"/>
{# <lineStyle cap="default" kind="grid" colorName="black" thickness="1" start="1,0" stop="-1,-1"/> #}
</blockTableStyle>
<blockTableStyle id="infoTable" keepWithNext="true">
<blockLeftPadding start="0,0" stop="-1,-1" length="0"/>
</blockTableStyle>
<blockTableStyle id="paymentTable">
<blockBackground colorName="LightGray" start="0,1" stop="3,1"/>
<blockFont name="OpenSans-Bold" start="0,1" stop="0,1"/>
<blockFont name="OpenSans-Bold" start="2,1" stop="2,1"/>
<lineStyle kind="outline" colorName="black" thickness="1" start="0,1" stop="3,1"/>
</blockTableStyle>
<blockTableStyle id="signatureTable">
<blockTopPadding length="20" />
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
<lineStyle kind="linebelow" start="1,0" stop="1,0" colorName="black"/>
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
</blockTableStyle>
<listStyle name="ol"
bulletFormat="%s."
bulletFontSize="10" />
<listStyle name="ul"
start="bulletchar"
bulletFontSize="10"/>
</stylesheet>
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
<pageTemplate id="Headed" >
<pageGraphics>
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
<image file="static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
{# logo positioned 42 from left, 33 from top #}
<image file="static/imgs/paperwork/tec-logo.jpg" x="42" y="719" height="90" width="84"/>
<setFont name="OpenSans-Bold" size="22.5" leading="10"/>
<drawString x="137" y="780">TEC PA &amp; Lighting</drawString>
<setFont name="OpenSans" size="9"/>
<drawString x="137" y="760">Portland Building, University Park, Nottingham, NG7 2RD</drawString>
<drawString x="137" y="746">www.nottinghamtec.co.uk</drawString>
<drawString x="265" y="746">info@nottinghamtec.co.uk</drawString>
<drawString x="137" y="732">Phone: (0115) 846 8720</drawString>
<setFont name="OpenSans" size="10" />
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
<setFont name="OpenSans" size="7" />
<drawCenteredString x="302.5" y="26">
{{info_string}}
</drawCenteredString>
</pageGraphics>
<frame id="main" x1="50" y1="65" width="495" height="645"/>
</pageTemplate>
<pageTemplate id="Main">
<pageGraphics>
<image file="static/imgs/paperwork/corner-tr.jpg" x="395" y="642" height="200" width="200"/>
<image file="static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
<setFont name="OpenSans" size="10"/>
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
<setFont name="OpenSans" size="7" />
<drawCenteredString x="302.5" y="26">
{{info_string}}
</drawCenteredString>
</pageGraphics>
<frame id="main" x1="50" y1="65" width="495" height="727"/>
</pageTemplate>
</template>
<story firstPageTemplate="Headed">
{% include "event_print_page.xml" %}
</story>
</document>

View File

@@ -1,12 +1,10 @@
{% load markdown_tags %} {% load markdown_tags %}
{% load filters %} {% load filters %}
<setNextFrame name="main"/>
<nextFrame/>
<blockTable style="headLayout" colWidths="330,165"> <blockTable style="headLayout" colWidths="330,165">
<tr> <tr>
<td> <td>
<h1><b>N{{ object.pk|stringformat:"05d" }}:</b> '{{ object.name }}'<small></small></h1> <h1><b>N{{ object.pk|stringformat:"05d" }}:</b> '{{ object.name }}'</h1>
<para style="style.event_description"> <para style="style.event_description">
<b>{{object.start_date|date:"D jS N Y"}}</b> <b>{{object.start_date|date:"D jS N Y"}}</b>
@@ -180,7 +178,7 @@
{% for item in object.items.all %} {% for item in object.items.all %}
<tr> <tr>
<td> <td>
<para>{{ item.name }}</para> <para><b>{{ item.name }}</b></para>
{% if item.description %} {% if item.description %}
{{ item.description|markdown:"rml" }} {{ item.description|markdown:"rml" }}
{% endif %} {% endif %}

View File

@@ -5,21 +5,6 @@
{% block title %}Request Authorisation{% endblock %} {% block title %}Request Authorisation{% endblock %}
{% block js %}
<script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/popover.js' %}"></script>
<script src="{% static 'js/clipboard.min.js' %}"></script>
<script>
var clipboard = new ClipboardJS('.btn');
clipboard.on('success', function(e) {
$(e.trigger).popover('show');
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
e.clearSelection();
});
</script>
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
@@ -33,11 +18,11 @@
<dl class="dl-horizontal"> <dl class="dl-horizontal">
{% if object.person.email %} {% if object.person.email %}
<dt>Person Email</dt> <dt>Person Email</dt>
<dd><span id="person-email">{{ object.person.email }}</span>{% button 'copy' id='#person-email' %}</dd> <dd><span id="person-email" class="pr-1">{{ object.person.email }}</span> {% button 'copy' id='#person-email' %}</dd>
{% endif %} {% endif %}
{% if object.organisation.email %} {% if object.organisation.email %}
<dt>Organisation Email</dt> <dt>Organisation Email</dt>
<dd><span id="org-email">{{ object.organisation.email }}</span>{% button 'copy' id='#org-email' %}</dd> <dd><span id="org-email" class="pr-1">{{ object.organisation.email }}</span> {% button 'copy' id='#org-email' %}</dd>
{% endif %} {% endif %}
</dl> </dl>
{% else %} {% else %}
@@ -57,11 +42,20 @@
</form> </form>
</div> </div>
</div> </div>
<script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/popover.js' %}"></script>
<script src="{% static 'js/clipboard.min.js' %}"></script>
<script> <script>
$('#auth-request-form').on('submit', function () { $('#auth-request-form').on('submit', function () {
$('#auth-request-form button').attr('disabled', true); $('#auth-request-form button').attr('disabled', true);
}); });
var clipboard = new ClipboardJS('.btn');
clipboard.on('success', function(e) {
$(e.trigger).popover('show');
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
e.clearSelection();
});
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,135 @@
{% extends 'base_print.xml' %}
{% load filters %}
{% block content %}
<spacer length="15"/>
<h1>Event Specific Risk Assessment for <strong>{{ object.event }}</strong></h1>
<spacer length="15"/>
<h2>Client: {{ object.event.person|default:object.event.organisation }} | Venue: {{ object.event.venue }} | MIC: {{ object.event.mic }}</h2>
<spacer length="15"/>
<hr/>
<blockTable colWidths="425,100" spaceAfter="15">
<tr>
<td colspan="2"><h3><strong>General</strong></h3></td>
</tr>
<tr>
<td><para>{{ object|help_text:'nonstandard_equipment'|striptags }}</para></td>
<td>{{ object.nonstandard_equipment|yesno|capfirst }}</td>
</tr>
<tr>
<td><para>{{ object|help_text:'nonstandard_use'|striptags }}</para></td>
<td>{{ object.nonstandard_use|yesno|capfirst }}</td>
</tr>
<tr>
<td><para>{{ object|help_text:'contractors'|striptags }}</para></td>
<td>{{ object.contractors|yesno|capfirst }}</td>
</tr>
<tr>
<td><para>{{ object|help_text:'other_companies'|striptags }}</para></td>
<td>{{ object.other_companies|yesno|capfirst }}</td>
</tr>
<tr>
<td><para>{{ object|help_text:'crew_fatigue'|striptags }}</para></td>
<td>{{ object.crew_fatigue|yesno|capfirst }}</td>
</tr>
<tr>
<td><para>{{ object|help_text:'general_notes'|striptags }}</para></td>
<td><para>{{ object.general_notes|default:'No' }}</para></td>
</tr>
<tr>
<td colspan="2"><h3><strong>Power</strong></h3><spacer length="4"/><para textColor="white" backColor={% if object.event_size == 0 %}"green"{% elif object.event_size == 1 %}"yellow"{% else %}"red"{% endif %} borderPadding="3">{{ object.get_event_size_display }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'big_power'|striptags }}</para></td>
<td><para>{{ object.big_power|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'power_mic'|striptags }}</para></td>
<td><para>{{ object.power_mic|default:object.event.mic }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'outside'|striptags }}</para></td>
<td><para>{{ object.outside|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'generators'|striptags }}</para></td>
<td><para>{{ object.generators|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'other_companies_power'|striptags }}</para></td>
<td><para>{{ object.other_companies_power|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'nonstandard_equipment_power'|striptags }}</para></td>
<td><para>{{ object.nonstandard_equipment_power|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'multiple_electrical_environments'|striptags }}</para></td>
<td><para>{{ object.multiple_electrical_environments|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'power_notes'|striptags }}</para></td>
<td><para>{{ object.power_notes|default:'No' }}</para></td>
</tr>
<tr>
<td colspan="2"><h3><strong>Sound</strong></h3></td>
</tr>
<tr>
<td><para>{{ object|help_text:'noise_monitoring'|striptags }}</para></td>
<td><para>{{ object.noise_monitoring|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'sound_notes'|striptags }}</para></td>
<td><para>{{ object.sound_notes|default:'No' }}</para></td>
</tr>
<tr>
<td colspan="2"><h3><strong>Site Details</strong></h3></td>
</tr>
<tr>
<td><para>{{ object|help_text:'known_venue'|striptags }}</para></td>
<td><para>{{ object.known_venue|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'safe_loading'|striptags }}</para></td>
<td><para>{{ object.safe_loading|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'safe_storage'|striptags }}</para></td>
<td><para>{{ object.safe_storage|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'area_outside_of_control'|striptags }}</para></td>
<td><para>{{ object.area_outside_of_control|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'barrier_required'|striptags }}</para></td>
<td><para>{{ object.barrier_required|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'nonstandard_emergency_procedure'|striptags }}</para></td>
<td><para>{{ object.nonstandard_emergency_procedure|yesno|capfirst }}</para></td>
</tr>
<tr>
<td colspan="2"><h3><strong>Structures</strong></h3></td>
</tr>
<tr>
<td><para>{{ object|help_text:'special_structures'|striptags }}</para></td>
<td><para>{{ object.special_structures|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'suspended_structures'|striptags }}</para></td>
<td><para>{{ object.suspended_structures|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'persons_responsible_structures'|striptags }}</para></td>
<td><para>{{ object.persons_responsible_structures|default:'N/A' }}</para></td>
</tr>
</blockTable>
<spacer length="15"/>\
<hr/>
<spacer length="15"/>
<para><em>Assessment completed by {{ object.last_edited_by }} on {{ object.last_edited_at }}</em></para>
{% if object.reviewed_by %}
<para><em>Reviewed by {{ object.reviewed_by }} on {{ object.reviewed.at }}</em></para>
{% endif %}
{% endblock %}

View File

@@ -1,7 +1,5 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %} {% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load help_text from filters %} {% load filters %}
{% load yesnoi from filters %}
{% load linkornone from filters %}
{% block content %} {% block content %}
<div class="row py-3"> <div class="row py-3">
@@ -47,7 +45,7 @@
</dd> </dd>
<dt class="col-sm-6">{{ object|help_text:'power_mic'|safe }}</dt> <dt class="col-sm-6">{{ object|help_text:'power_mic'|safe }}</dt>
<dd class="col-sm-6"> <dd class="col-sm-6">
{{ object.power_mic.name|default:'None' }} {{ object.power_mic.name|default:object.event.mic }}
</dd> </dd>
<dt class="col-sm-6">{{ object|help_text:'outside' }}</dt> <dt class="col-sm-6">{{ object|help_text:'outside' }}</dt>
<dd class="col-sm-6"> <dd class="col-sm-6">
@@ -144,7 +142,7 @@
</dd> </dd>
<dt class="col-12">{{ object|help_text:'persons_responsible_structures' }}</dt> <dt class="col-12">{{ object|help_text:'persons_responsible_structures' }}</dt>
<dd class="col-12"> <dd class="col-12">
{{ object.persons_responsible_structures.name|default:'N/A'|linebreaks }} {{ object.persons_responsible_structures|default:'N/A'|linebreaks }}
</dd> </dd>
<dt class="col-12">{{ object|help_text:'rigging_plan'|safe }}</dt> <dt class="col-12">{{ object|help_text:'rigging_plan'|safe }}</dt>
<dd class="col-12"> <dd class="col-12">
@@ -157,6 +155,7 @@
</div> </div>
</div> </div>
<div class="col-12 text-right"> <div class="col-12 text-right">
{% button 'print' 'ra_print' object.pk %}
<a href="{% url 'ra_edit' object.pk %}" class="btn btn-warning my-3"><span class="fas fa-edit"></span> <span <a href="{% url 'ra_edit' object.pk %}" class="btn btn-warning my-3"><span class="fas fa-edit"></span> <span
class="d-none d-sm-inline">Edit</span></a> class="d-none d-sm-inline">Edit</span></a>
<a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View Event</a> <a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View Event</a>

View File

@@ -98,9 +98,9 @@
<label for="{{ form.power_mic.id_for_label }}" <label for="{{ form.power_mic.id_for_label }}"
class="col col-form-label">{{ form.power_mic.help_text|safe }}</label> class="col col-form-label">{{ form.power_mic.help_text|safe }}</label>
<div class="col-6"> <div class="col-6">
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials"> <select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if object.power_mic %} {% if power_mic %}
<option value="{{object.power_mic.pk}}" selected="selected">{{ object.power_mic.name }}</option> <option value="{{form.power_mic.value}}" selected="selected">{{ power_mic }}</option>
{% endif %} {% endif %}
</select> </select>
</div> </div>

View File

@@ -40,7 +40,7 @@
<dt class="col-sm-6">Phone Number</dt> <dt class="col-sm-6">Phone Number</dt>
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</dd> <dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</dd>
<dt class="col-sm-6">Has SU Account</dt> <dt class="col-sm-6">Has SU Account</dt>
<dd class="col-sm-6">{{ event.organisation.union_account|yesno|capfirst }}</dd> <dd class="col-sm-6">{{ object.organisation.union_account|yesno|capfirst }}</dd>
</dl> </dl>
</div> </div>
</div> </div>

View File

@@ -9,7 +9,7 @@
{% if event.internal %} {% if event.internal %}
<a class="btn item-add modal-href event-authorise-request <a class="btn item-add modal-href event-authorise-request
{% if event.authorised %} {% if event.authorised %}
btn-success active btn-success active disabled
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %} {% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
btn-warning btn-warning
{% elif event.auth_request_to %} {% elif event.auth_request_to %}
@@ -18,7 +18,7 @@
btn-secondary btn-secondary
{% endif %} {% endif %}
" "
href="{% url 'event_authorise_request' object.pk %}"> {% if event.authorised %}aria-disabled="true"{% else %}href="{% url 'event_authorise_request' object.pk %}"{% endif %}>
<span class="fas fa-paper-plane"></span> <span class="fas fa-paper-plane"></span>
<span class="d-none d-sm-inline"> <span class="d-none d-sm-inline">
{% if event.authorised %} {% if event.authorised %}

View File

@@ -15,7 +15,7 @@
{% if object.venue %} {% if object.venue %}
<dt class="col-sm-6">Venue Notes</dt> <dt class="col-sm-6">Venue Notes</dt>
<dd class="col-sm-6"> <dd class="col-sm-6">
{{ object.venue.notes }}{% if object.venue.three_phase_available %}<br>(Three phase available){%endif%} {{ object.venue.notes|markdown }}{% if object.venue.three_phase_available %}<br>(Three phase available){%endif%}
</dd> </dd>
{% endif %} {% endif %}

View File

@@ -16,7 +16,7 @@
{% endif %} {% endif %}
{% if not event.dry_hire %} {% if not event.dry_hire %}
{% if event.riskassessment %} {% if event.riskassessment %}
<span class="badge badge-success">RA: <span class="fas fa-check"></span>{%if event.riskassessment.reviewed_by%}<span class="fas fa-check"></span>{%endif%}</span> <a href="{{ event.riskassessment.get_absolute_url }}"><span class="badge badge-success">RA: <span class="fas fa-check{% if event.riskassessment.reviewed_by %}-double{%endif%}"></span></a>
{% else %} {% else %}
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span> <span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>
{% endif %} {% endif %}

View File

@@ -1,4 +1,5 @@
{% load namewithnotes from filters %} {% load namewithnotes from filters %}
{% load markdown_tags %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table mb-0" id="event_table"> <table class="table mb-0" id="event_table">
<thead> <thead>
@@ -29,7 +30,15 @@
<!---Number--> <!---Number-->
<th scope="row" id="event_number">{{ event.display_id }}</th> <th scope="row" id="event_number">{{ event.display_id }}</th>
<!--Dates & Times--> <!--Dates & Times-->
<td id="event_dates"> <td id="event_dates" style="text-align: justify;">
{% if not event.cancelled %}
{% if event.meet_at %}
<span class="text-nowrap">Meet: <strong>{{ event.meet_at|date:"D d/m/Y H:i" }}</strong></span>
{% endif %}
{% if event.access_at %}
<br><span class="text-nowrap">Access: <strong>{{ event.access_at|date:"D d/m/Y H:i" }}</strong></span>
{% endif %}
{% endif %}
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }} <span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}
{% if event.has_start_time %} {% if event.has_start_time %}
{{ event.start_time|date:"H:i" }} {{ event.start_time|date:"H:i" }}
@@ -43,14 +52,6 @@
{% endif %}</strong> {% endif %}</strong>
</span> </span>
{% endif %} {% endif %}
{% if not event.cancelled %}
{% if event.meet_at %}
<br><span class="text-nowrap">Meet: <strong>{{ event.meet_at|date:"D d/m/Y H:i" }}</strong></span>
{% endif %}
{% if event.access_at %}
<br><span class="text-nowrap">Access: <strong>{{ event.access_at|date:" D d/m/Y H:i" }}</strong></span>
{% endif %}
{% endif %}
</td> </td>
<!---Details--> <!---Details-->
<td id="event_details" class="w-100"> <td id="event_details" class="w-100">
@@ -74,7 +75,7 @@
</h5> </h5>
{% endif %} {% endif %}
{% if not event.cancelled and event.description %} {% if not event.cancelled and event.description %}
<p>{{ event.description|linebreaksbr }}</p> <p>{{ event.description|markdown }}</p>
{% endif %} {% endif %}
{% include 'partials/event_status.html' %} {% include 'partials/event_status.html' %}
</td> </td>

View File

@@ -216,6 +216,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style} return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style}
elif type == 'submit': elif type == 'submit':
return {'submit': True, 'class': 'btn-primary', 'icon': 'fa-save', 'text': 'Save', 'id': id, 'style': style} return {'submit': True, 'class': 'btn-primary', 'icon': 'fa-save', 'text': 'Save', 'id': id, 'style': style}
elif type == 'today':
return {'today': True, 'id': id}
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style} return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}

View File

@@ -211,7 +211,7 @@ class TestEventCreate(BaseRigboardTest):
self.assertEqual("Test Item 1", testitem['name']) self.assertEqual("Test Item 1", testitem['name'])
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
total = self.driver.find_element_by_id('total') total = self.driver.find_element(By.ID, 'total')
ActionChains(self.driver).move_to_element(total).perform() ActionChains(self.driver).move_to_element(total).perform()
# See new item appear in table # See new item appear in table
@@ -224,9 +224,9 @@ class TestEventCreate(BaseRigboardTest):
self.assertEqual('47.90', row.subtotal) self.assertEqual('47.90', row.subtotal)
# Check totals TODO convert to page properties # Check totals TODO convert to page properties
self.assertEqual("47.90", self.driver.find_element_by_id('sumtotal').text) self.assertEqual("47.90", self.driver.find_element(By.ID, 'sumtotal').text)
self.assertIn("(TBC)", self.driver.find_element_by_id('vat-rate').text) self.assertIn("(TBC)", self.driver.find_element(By.ID, 'vat-rate').text)
self.assertEqual("9.58", self.driver.find_element_by_id('vat').text) self.assertEqual("9.58", self.driver.find_element(By.ID, 'vat').text)
self.assertEqual("57.48", total.text) self.assertEqual("57.48", total.text)
self.page.submit() self.page.submit()

View File

@@ -83,6 +83,7 @@ urlpatterns = [
name='ra_list'), name='ra_list'),
path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(views.EventRiskAssessmentReview.as_view()), path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(views.EventRiskAssessmentReview.as_view()),
name='ra_review'), name='ra_review'),
path('event/ra/<int:pk>/print/', permission_required_with_403('RIGS.view_riskassessment')(views.RAPrint.as_view()), name='ra_print'),
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()), path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
name='event_ec'), name='event_ec'),

View File

@@ -28,7 +28,8 @@ class InvoiceIndex(generic.ListView):
total = 0 total = 0
for i in context['object_list']: for i in context['object_list']:
total += i.balance total += i.balance
context['page_title'] = "Outstanding Invoices ({} Events, £{:.2f})".format(len(list(context['object_list'])), total) event_count = len(list(context['object_list']))
context['page_title'] = f"Outstanding Invoices ({event_count} Events, £{total:.2f})"
context['description'] = "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger" context['description'] = "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger"
return context return context
@@ -43,7 +44,7 @@ class InvoiceDetail(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
invoice_date = self.object.invoice_date.strftime("%d/%m/%Y") invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date}) " context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date})"
if self.object.void: if self.object.void:
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>" context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
elif self.object.is_closed: elif self.object.is_closed:
@@ -59,11 +60,14 @@ class InvoicePrint(generic.View):
object = invoice.event object = invoice.event
template = get_template('event_print.xml') template = get_template('event_print.xml')
name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
filename = f"Invoice {invoice.display_id} for {object.display_id} {name}.pdf"
context = { context = {
'object': object, 'object': object,
'invoice': invoice, 'invoice': invoice,
'current_user': request.user, 'current_user': request.user,
'filename': 'Invoice {} for {} {}.pdf'.format(invoice.display_id, object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)) 'filename': filename
} }
rml = template.render(context) rml = template.render(context)
@@ -73,7 +77,7 @@ class InvoicePrint(generic.View):
pdfData = buffer.read() pdfData = buffer.read()
response = HttpResponse(content_type='application/pdf') response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'filename="{}"'.format(context['filename']) response['Content-Disposition'] = f'filename="{filename}"'
response.write(pdfData) response.write(pdfData)
return response return response
@@ -124,32 +128,7 @@ class InvoiceArchive(generic.ListView):
return context return context
def get_queryset(self): def get_queryset(self):
q = self.request.GET.get('q', "") return self.model.objects.search(self.request.GET.get('q')).order_by('-invoice_date')
filter = Q(event__name__icontains=q)
# try and parse an int
try:
val = int(q)
filter = filter | Q(pk=val)
filter = filter | Q(event__pk=val)
except: # noqa
# not an integer
pass
try:
if q[0] == "N":
val = int(q[1:])
filter = Q(event__pk=val) # If string is Nxxxxx then filter by event number
elif q[0] == "#":
val = int(q[1:])
filter = Q(pk=val) # If string is #xxxxx then filter by invoice number
except: # noqa
pass
object_list = self.model.objects.filter(filter).order_by('-invoice_date')
return object_list
class InvoiceWaiting(generic.ListView): class InvoiceWaiting(generic.ListView):
@@ -163,7 +142,7 @@ class InvoiceWaiting(generic.ListView):
objects = self.get_queryset() objects = self.get_queryset()
for obj in objects: for obj in objects:
total += obj.sum_total total += obj.sum_total
context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(objects), total) context['page_title'] = f"Events for Invoice ({len(objects)} Events, £{total:.2f})"
return context return context
def get_queryset(self): def get_queryset(self):

View File

@@ -6,11 +6,13 @@ from django.views import generic
from reversion import revisions as reversion from reversion import revisions as reversion
from RIGS import models, forms from RIGS import models, forms
from RIGS.views.rigboard import get_related
from PyRIGS.views import PrintView
class EventRiskAssessmentCreate(generic.CreateView): class EventRiskAssessmentCreate(generic.CreateView):
model = models.RiskAssessment model = models.RiskAssessment
template_name = 'risk_assessment_form.html' template_name = 'hs/risk_assessment_form.html'
form_class = forms.EventRiskAssessmentForm form_class = forms.EventRiskAssessmentForm
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
@@ -37,7 +39,8 @@ class EventRiskAssessmentCreate(generic.CreateView):
epk = self.kwargs.get('pk') epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk) event = models.Event.objects.get(pk=epk)
context['event'] = event context['event'] = event
context['page_title'] = 'Create Risk Assessment for Event {}'.format(event.display_id) context['page_title'] = f'Create Risk Assessment for Event {event.display_id}'
get_related(context['form'], context)
return context return context
def get_success_url(self): def get_success_url(self):
@@ -46,7 +49,7 @@ class EventRiskAssessmentCreate(generic.CreateView):
class EventRiskAssessmentEdit(generic.UpdateView): class EventRiskAssessmentEdit(generic.UpdateView):
model = models.RiskAssessment model = models.RiskAssessment
template_name = 'risk_assessment_form.html' template_name = 'hs/risk_assessment_form.html'
form_class = forms.EventRiskAssessmentForm form_class = forms.EventRiskAssessmentForm
def get_success_url(self): def get_success_url(self):
@@ -62,24 +65,25 @@ class EventRiskAssessmentEdit(generic.UpdateView):
ra = models.RiskAssessment.objects.get(pk=rpk) ra = models.RiskAssessment.objects.get(pk=rpk)
context['event'] = ra.event context['event'] = ra.event
context['edit'] = True context['edit'] = True
context['page_title'] = 'Edit Risk Assessment for Event {}'.format(ra.event.display_id) context['page_title'] = f'Edit Risk Assessment for Event {ra.event.display_id}'
get_related(context['form'], context)
return context return context
class EventRiskAssessmentDetail(generic.DetailView): class EventRiskAssessmentDetail(generic.DetailView):
model = models.RiskAssessment model = models.RiskAssessment
template_name = 'risk_assessment_detail.html' template_name = 'hs/risk_assessment_detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventRiskAssessmentDetail, self).get_context_data(**kwargs) context = super(EventRiskAssessmentDetail, self).get_context_data(**kwargs)
context['page_title'] = "Risk Assessment for Event <a href='{}'>{} {}</a>".format(self.object.event.get_absolute_url(), self.object.event.display_id, self.object.event.name) context['page_title'] = f"Risk Assessment for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
return context return context
class EventRiskAssessmentList(generic.ListView): class EventRiskAssessmentList(generic.ListView):
paginate_by = 20 paginate_by = 20
model = models.RiskAssessment model = models.RiskAssessment
template_name = 'hs_object_list.html' template_name = 'hs/hs_object_list.html'
def get_queryset(self): def get_queryset(self):
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event') return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
@@ -108,17 +112,17 @@ class EventRiskAssessmentReview(generic.View):
class EventChecklistDetail(generic.DetailView): class EventChecklistDetail(generic.DetailView):
model = models.EventChecklist model = models.EventChecklist
template_name = 'event_checklist_detail.html' template_name = 'hs/event_checklist_detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventChecklistDetail, self).get_context_data(**kwargs) context = super(EventChecklistDetail, self).get_context_data(**kwargs)
context['page_title'] = "Event Checklist for Event <a href='{}'>{} {}</a>".format(self.object.event.get_absolute_url(), self.object.event.display_id, self.object.event.name) context['page_title'] = f"Event Checklist for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
return context return context
class EventChecklistEdit(generic.UpdateView): class EventChecklistEdit(generic.UpdateView):
model = models.EventChecklist model = models.EventChecklist
template_name = 'event_checklist_form.html' template_name = 'hs/event_checklist_form.html'
form_class = forms.EventChecklistForm form_class = forms.EventChecklistForm
def get_success_url(self): def get_success_url(self):
@@ -134,19 +138,14 @@ class EventChecklistEdit(generic.UpdateView):
ec = models.EventChecklist.objects.get(pk=pk) ec = models.EventChecklist.objects.get(pk=pk)
context['event'] = ec.event context['event'] = ec.event
context['edit'] = True context['edit'] = True
context['page_title'] = 'Edit Event Checklist for Event {}'.format(ec.event.display_id) context['page_title'] = f'Edit Event Checklist for Event {ec.event.display_id}'
form = context['form'] get_related(context['form'], context)
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
for field, model in form.related_models.items():
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
return context return context
class EventChecklistCreate(generic.CreateView): class EventChecklistCreate(generic.CreateView):
model = models.EventChecklist model = models.EventChecklist
template_name = 'event_checklist_form.html' template_name = 'hs/event_checklist_form.html'
form_class = forms.EventChecklistForm form_class = forms.EventChecklistForm
# From both business logic and programming POVs, RAs must exist before ECs! # From both business logic and programming POVs, RAs must exist before ECs!
@@ -158,7 +157,7 @@ class EventChecklistCreate(generic.CreateView):
ra = models.RiskAssessment.objects.filter(event=event).first() ra = models.RiskAssessment.objects.filter(event=event).first()
if ra is None: if ra is None:
messages.error(self.request, 'A Risk Assessment must exist prior to creating any Event Checklists for {}! Please create one now.'.format(event)) messages.error(self.request, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.')
return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk})) return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
return super(EventChecklistCreate, self).get(self) return super(EventChecklistCreate, self).get(self)
@@ -175,7 +174,7 @@ class EventChecklistCreate(generic.CreateView):
epk = self.kwargs.get('pk') epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk) event = models.Event.objects.get(pk=epk)
context['event'] = event context['event'] = event
context['page_title'] = 'Create Event Checklist for Event {}'.format(event.display_id) context['page_title'] = f'Create Event Checklist for Event {event.display_id}'
return context return context
def get_success_url(self): def get_success_url(self):
@@ -185,7 +184,7 @@ class EventChecklistCreate(generic.CreateView):
class EventChecklistList(generic.ListView): class EventChecklistList(generic.ListView):
paginate_by = 20 paginate_by = 20
model = models.EventChecklist model = models.EventChecklist
template_name = 'hs_object_list.html' template_name = 'hs/hs_object_list.html'
def get_queryset(self): def get_queryset(self):
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event') return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
@@ -215,7 +214,7 @@ class EventChecklistReview(generic.View):
class HSList(generic.ListView): class HSList(generic.ListView):
paginate_by = 20 paginate_by = 20
model = models.Event model = models.Event
template_name = 'hs_list.html' template_name = 'hs/hs_list.html'
def get_queryset(self): def get_queryset(self):
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists') return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
@@ -224,3 +223,13 @@ class HSList(generic.ListView):
context = super(HSList, self).get_context_data(**kwargs) context = super(HSList, self).get_context_data(**kwargs)
context['page_title'] = 'H&S Overview' context['page_title'] = 'H&S Overview'
return context return context
class RAPrint(PrintView):
model = models.RiskAssessment
template_name = 'hs/ra_print.xml'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filename'] = f"EventSpecificRiskAssessment_for_{context['object'].event.display_id}.pdf"
return context

View File

@@ -93,7 +93,7 @@ class CalendarICS(ICalFeed):
title += item.name title += item.name
# Add the status # Add the status
title += ' (' + str(item.get_status_display()) + ')' title += f' ({item.get_status_display()})'
return title return title
@@ -101,9 +101,8 @@ class CalendarICS(ICalFeed):
return item.earliest_time return item.earliest_time
def item_end_datetime(self, item): def item_end_datetime(self, item):
if isinstance(item.latest_time, datetime.date): # Ical end_datetime is non-inclusive, so add a day # if isinstance(item.latest_time, datetime.date): # Ical end_datetime is non-inclusive, so add a day
return item.latest_time + datetime.timedelta(days=1) # return item.latest_time + datetime.timedelta(days=1)
return item.latest_time return item.latest_time
def item_location(self, item): def item_location(self, item):
@@ -115,13 +114,13 @@ class CalendarICS(ICalFeed):
tz = pytz.timezone(self.timezone) tz = pytz.timezone(self.timezone)
desc = 'Rig ID = ' + str(item.pk) + '\n' desc = f'Rig ID = {item.display_id}\n'
desc += 'Event = ' + item.name + '\n' desc += f'Event = {item.name}\n'
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n' desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
if item.is_rig and item.person: if item.is_rig and item.person:
desc += 'Client = ' + item.person.name + ( desc += 'Client = ' + item.person.name + (
(' for ' + item.organisation.name) if item.organisation else '') + '\n' (' for ' + item.organisation.name) if item.organisation else '') + '\n'
desc += 'Status = ' + str(item.get_status_display()) + '\n' desc += f'Status = {item.get_status_display()}\n'
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n' desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
desc += '\n' desc += '\n'
@@ -140,23 +139,18 @@ class CalendarICS(ICalFeed):
desc += '\n' desc += '\n'
if item.description: if item.description:
desc += 'Event Description:\n' + item.description + '\n\n' desc += f'Event Description:\n{item.description}\n\n'
# if item.notes: // Need to add proper keyholder checks before this gets put back # if item.notes: // Need to add proper keyholder checks before this gets put back
# desc += 'Notes:\n'+item.notes+'\n\n' # desc += 'Notes:\n'+item.notes+'\n\n'
base_url = "https://rigs.nottinghamtec.co.uk" desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}'
desc += 'URL = ' + base_url + str(item.get_absolute_url())
return desc return desc
def item_link(self, item): def item_link(self, item):
# Make a link to the event in the web interface # Make a link to the event in the web interface
# base_url = "https://pyrigs.nottinghamtec.co.uk"
return item.get_absolute_url() return item.get_absolute_url()
# def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) - not really necessary though
# return ''
def item_updated(self, item): # some ical clients will display this def item_updated(self, item): # some ical clients will display this
return item.last_edited_at return item.last_edited_at

View File

@@ -1,14 +1,9 @@
import copy import copy
import datetime import datetime
import re import re
import urllib.error
import urllib.parse
import urllib.request
from io import BytesIO
import premailer import premailer
import simplejson import simplejson
from PyPDF2 import PdfFileMerger, PdfFileReader
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
@@ -24,10 +19,9 @@ from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import generic from django.views import generic
from z3c.rml import rml2pdf
from PyRIGS import decorators from PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
from RIGS import models, forms from RIGS import models, forms
__author__ = 'ghost' __author__ = 'ghost'
@@ -98,11 +92,8 @@ class EventCreate(generic.CreateView):
if hasattr(form, 'items_json') and re.search(r'"-\d+"', form['items_json'].value()): if hasattr(form, 'items_json') and re.search(r'"-\d+"', form['items_json'].value()):
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.") messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
# Get some other objects to include in the form. Used when there are errors but also nice and quick. get_related(form, context)
for field, model in form.related_models.items():
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
return context return context
def get_success_url(self): def get_success_url(self):
@@ -121,11 +112,7 @@ class EventUpdate(generic.UpdateView):
form = context['form'] form = context['form']
# Get some other objects to include in the form. Used when there are errors but also nice and quick. get_related(form, context)
for field, model in form.related_models.items():
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
return context return context
@@ -178,39 +165,16 @@ class EventDuplicate(EventUpdate):
return context return context
class EventPrint(generic.View): class EventPrint(PrintView):
def get(self, request, pk): model = models.Event
object = get_object_or_404(models.Event, pk=pk) template_name = 'event_print.xml'
template = get_template('event_print.xml') append_terms = True
merger = PdfFileMerger() def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user_str = f"by {request.user.name} " if request.user is not None else "" context['quote'] = True
time = timezone.now().strftime('%d/%m/%Y %H:%I') context['filename'] = f"Event_{context['object'].display_id}_{context['object_name']}_{context['object'].start_date}.pdf"
return context
context = {
'object': object,
'quote': True,
'current_user': request.user,
'filename': 'Event_{}_{}_{}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date),
'info_string': f"[Paperwork generated {user_str}on {time} - {object.current_version_id}]",
}
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(BytesIO(terms.read()))
merged = BytesIO()
merger.write(merged)
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
response.write(merged.getvalue())
return response
class EventArchive(generic.ListView): class EventArchive(generic.ListView):
@@ -220,7 +184,6 @@ class EventArchive(generic.ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['start'] = self.request.GET.get('start', None) context['start'] = self.request.GET.get('start', None)
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d')) context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
context['statuses'] = models.Event.EVENT_STATUS_CHOICES context['statuses'] = models.Event.EVENT_STATUS_CHOICES
@@ -244,32 +207,17 @@ class EventArchive(generic.ListView):
filter &= Q(start_date__gte=start) filter &= Q(start_date__gte=start)
q = self.request.GET.get('q', "") q = self.request.GET.get('q', "")
objects = self.model.objects.all()
if q != "": if q:
qfilter = Q(name__icontains=q) | Q(description__icontains=q) | Q(notes__icontains=q) objects = self.model.objects.search(q)
# try and parse an int
try:
val = int(q)
qfilter = qfilter | Q(pk=val)
except: # noqa not an integer
pass
try:
if q[0] == "N":
val = int(q[1:])
qfilter = Q(pk=val) # If string is N###### then do a simple PK filter
except: # noqa
pass
filter &= qfilter
status = self.request.GET.getlist('status', "") status = self.request.GET.getlist('status', "")
if len(status) > 0: if len(status) > 0:
filter &= Q(status__in=status) filter &= Q(status__in=status)
qs = self.model.objects.filter(filter).order_by('-start_date') qs = objects.filter(filter).order_by('-start_date')
# Preselect related for efficiency # Preselect related for efficiency
qs.select_related('person', 'organisation', 'venue', 'mic') qs.select_related('person', 'organisation', 'venue', 'mic')
@@ -324,7 +272,7 @@ class EventAuthorise(generic.UpdateView):
messages.add_message(self.request, messages.WARNING, messages.add_message(self.request, messages.WARNING,
"This event has already been authorised, but the amount has changed. " + "This event has already been authorised, but the amount has changed. " +
"Please check the amount and reauthorise.") "Please check the amount and reauthorise.")
return super(EventAuthorise, self).get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_form(self, **kwargs): def get_form(self, **kwargs):
form = super().get_form(**kwargs) form = super().get_form(**kwargs)
@@ -393,7 +341,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
context['to_name'] = event.organisation.name context['to_name'] = event.organisation.name
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(
"N%05d | %s - Event Authorisation Request" % (self.object.pk, self.object.name), f"{self.object.display_id} | {self.object.name} - Event Authorisation Request",
get_template("eventauthorisation_client_request.txt").render(context), get_template("eventauthorisation_client_request.txt").render(context),
to=[email], to=[email],
reply_to=[self.request.user.email], reply_to=[self.request.user.email],

View File

@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from RIGS.admin import AssociateAdmin
from assets import models as assets from assets import models as assets
@@ -17,9 +17,13 @@ class AssetStatusAdmin(admin.ModelAdmin):
@admin.register(assets.Supplier) @admin.register(assets.Supplier)
class SupplierAdmin(VersionAdmin): class SupplierAdmin(AssociateAdmin):
list_display = ['id', 'name'] list_display = ['id', 'name']
ordering = ['id'] ordering = ['id']
merge_fields = ['name', 'phone', 'email', 'address', 'notes']
def get_queryset(self, request):
return super(VersionAdmin, self).get_queryset(request)
@admin.register(assets.Asset) @admin.register(assets.Asset)

View File

@@ -33,6 +33,7 @@ class AssetSearchForm(forms.Form):
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False) category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False) status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
is_cable = forms.BooleanField(required=False) is_cable = forms.BooleanField(required=False)
cable_type = forms.ModelMultipleChoiceField(models.CableType.objects.all(), required=False)
date_acquired = forms.DateField(required=False) date_acquired = forms.DateField(required=False)

View File

@@ -2,6 +2,7 @@ import random
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db import transaction from django.db import transaction
from django.db.utils import IntegrityError
from django.utils import timezone from django.utils import timezone
from reversion import revisions as reversion from reversion import revisions as reversion
@@ -125,5 +126,9 @@ class Command(BaseCommand):
if i % 3 == 0: if i % 3 == 0:
asset.purchased_from = random.choice(self.suppliers) asset.purchased_from = random.choice(self.suppliers)
asset.clean() with transaction.atomic():
asset.save() try:
asset.clean()
asset.save()
except IntegrityError:
pass

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.12 on 2022-02-14 15:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0022_alter_cabletype_unique_together'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='purchased_from',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.supplier'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-02-14 23:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0023_alter_asset_purchased_from'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='salvage_value',
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-05-26 09:22
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0024_alter_asset_salvage_value'),
]
operations = [
migrations.RenameField(
model_name='asset',
old_name='salvage_value',
new_name='replacement_cost',
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.2.12 on 2022-05-26 15:23
import assets.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0025_rename_salvage_value_asset_replacement_cost'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='purchase_price',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, validators=[assets.models.validate_positive]),
),
migrations.AlterField(
model_name='asset',
name='replacement_cost',
field=models.DecimalField(decimal_places=2, max_digits=10, null=True, validators=[assets.models.validate_positive]),
),
]

View File

@@ -2,11 +2,12 @@ import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models, connection from django.db import models, connection
from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from reversion import revisions as reversion from reversion import revisions as reversion
from reversion.models import Version from reversion.models import Version
from RIGS.models import Profile from RIGS.models import Profile, ContactableManager
from versioning.versioning import RevisionMixin from versioning.versioning import RevisionMixin
@@ -46,6 +47,8 @@ class Supplier(models.Model, RevisionMixin):
notes = models.TextField(blank=True, default="") notes = models.TextField(blank=True, default="")
objects = ContactableManager()
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@@ -88,23 +91,23 @@ class CableType(models.Model):
return reverse('cable_type_detail', kwargs={'pk': self.pk}) return reverse('cable_type_detail', kwargs={'pk': self.pk})
class AssetManager(models.Manager):
def search(self, query=None):
qs = self.get_queryset()
if query is not None:
or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query))
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
def get_available_asset_id(wanted_prefix=""): def get_available_asset_id(wanted_prefix=""):
sql = """ last_asset = Asset.objects.filter(asset_id_prefix=wanted_prefix).last()
SELECT a.asset_id_number+1 return 9000 if last_asset is None else wanted_prefix + str(last_asset.asset_id_number + 1)
FROM assets_asset a
LEFT OUTER JOIN assets_asset b ON
(a.asset_id_number + 1 = b.asset_id_number AND def validate_positive(value):
a.asset_id_prefix = b.asset_id_prefix) if value < 0:
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s; raise ValidationError("A price cannot be negative")
"""
with connection.cursor() as cursor:
cursor.execute(sql, [9000, wanted_prefix])
row = cursor.fetchone()
if row is None or row[0] is None:
return 9000
else:
return row[0]
cursor.close()
@reversion.register @reversion.register
@@ -116,11 +119,11 @@ class Asset(models.Model, RevisionMixin):
category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE) category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE)
status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE) status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE)
serial_number = models.CharField(max_length=150, blank=True) serial_number = models.CharField(max_length=150, blank=True)
purchased_from = models.ForeignKey(to=Supplier, on_delete=models.CASCADE, blank=True, null=True, related_name="assets") purchased_from = models.ForeignKey(to=Supplier, on_delete=models.SET_NULL, blank=True, null=True, related_name="assets")
date_acquired = models.DateField() date_acquired = models.DateField()
date_sold = models.DateField(blank=True, null=True) date_sold = models.DateField(blank=True, null=True)
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10) purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
salvage_value = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10) replacement_cost = models.DecimalField(null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
comments = models.TextField(blank=True) comments = models.TextField(blank=True)
# Audit # Audit
@@ -142,6 +145,8 @@ class Asset(models.Model, RevisionMixin):
reversion_perm = 'assets.asset_finance' reversion_perm = 'assets.asset_finance'
objects = AssetManager()
class Meta: class Meta:
ordering = ['asset_id_prefix', 'asset_id_number'] ordering = ['asset_id_prefix', 'asset_id_number']
permissions = [ permissions = [
@@ -165,12 +170,6 @@ class Asset(models.Model, RevisionMixin):
errdict["asset_id"] = [ errdict["asset_id"] = [
"An Asset ID can only consist of letters and numbers, with a final number"] "An Asset ID can only consist of letters and numbers, with a final number"]
if self.purchase_price and self.purchase_price < 0:
errdict["purchase_price"] = ["A price cannot be negative"]
if self.salvage_value and self.salvage_value < 0:
errdict["salvage_value"] = ["A price cannot be negative"]
if self.is_cable: if self.is_cable:
if not self.length or self.length <= 0: if not self.length or self.length <= 0:
errdict["length"] = ["The length of a cable must be more than 0"] errdict["length"] = ["The length of a cable must be more than 0"]

View File

@@ -9,9 +9,11 @@
date = new Date(); date = new Date();
} }
$('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-')); $('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'));
return false;
} }
function setFieldValue(ID, CSA) { function setFieldValue(ID, CSA) {
$('#' + String(ID)).val(CSA); $('#' + String(ID)).val(CSA);
return false;
} }
function checkIfCableHidden() { function checkIfCableHidden() {
document.getElementById("cable-table").hidden = !document.getElementById("id_is_cable").checked; document.getElementById("cable-table").hidden = !document.getElementById("id_is_cable").checked;
@@ -39,16 +41,16 @@
</div> </div>
<div class="form-group form-row"> <div class="form-group form-row">
{% include 'partials/form_field.html' with field=form.date_acquired col="col-6" %} {% include 'partials/form_field.html' with field=form.date_acquired col="col-6" %}
<div class="col-sm-4"> <div class="col-sm-2">
<button class="btn btn-info" onclick="setAcquired(true);" tabindex="-1">Today</button> <button class="btn btn-info" onclick="return setAcquired(true);" tabindex="-1">Today</button>
<button class="btn btn-warning" onclick="setAcquired(false);" tabindex="-1">Unknown</button> <button class="btn btn-warning" onclick="return setAcquired(false);" tabindex="-1">Unknown</button>
</div> </div>
</div> </div>
<div class="form-group form-row"> <div class="form-group form-row">
{% include 'partials/form_field.html' with field=form.date_sold col="col-6" %} {% include 'partials/form_field.html' with field=form.date_sold col="col-6" %}
</div> </div>
<div class="form-group form-row"> <div class="form-group form-row">
{% include 'partials/form_field.html' with field=form.salvage_value col="col-6" prepend="£" %} {% include 'partials/form_field.html' with field=form.replacement_cost col="col-6" prepend="£" %}
</div> </div>
<hr> <hr>
<div class="form-group form-row"> <div class="form-group form-row">
@@ -64,16 +66,16 @@
<div class="form-group form-row"> <div class="form-group form-row">
{% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %} {% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %}
<div class="col-4"> <div class="col-4">
<button class="btn btn-danger" onclick="setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1" type="button">5{{ form.length.help_text }}</button> <button class="btn btn-danger" onclick="return setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1" type="button">5{{ form.length.help_text }}</button>
<button class="btn btn-success" onclick="setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ form.length.help_text }}</button> <button class="btn btn-success" onclick="return setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ form.length.help_text }}</button>
<button class="btn btn-info" onclick="setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1" type="button">20{{ form.length.help_text }}</button> <button class="btn btn-info" onclick="return setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1" type="button">20{{ form.length.help_text }}</button>
</div> </div>
</div> </div>
<div class="form-group form-row"> <div class="form-group form-row">
{% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text title='CSA' col="col-6" %} {% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text title='CSA' col="col-6" %}
<div class="col-4"> <div class="col-4">
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1" type="button">1.5{{ form.csa.help_text }}</button> <button class="btn btn-secondary" onclick="return setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1" type="button">1.5{{ form.csa.help_text }}</button>
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button> <button class="btn btn-secondary" onclick="return setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -75,13 +75,13 @@
<div class="col"> <div class="col">
<div id="category-group" class="form-group px-1" style="margin-bottom: 0;"> <div id="category-group" class="form-group px-1" style="margin-bottom: 0;">
<label for="category" class="sr-only">Category</label> <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" %} {% render_field form.category|attr:'multiple'|add_class:'selectpicker col-sm' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div id="status-group" class="form-group px-1" style="margin-bottom: 0;"> <div id="status-group" class="form-group px-1" style="margin-bottom: 0;">
<label for="status" class="sr-only">Status</label> <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" %} {% render_field form.status|attr:'multiple'|add_class:'selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
</div> </div>
</div> </div>
<div class="col mt-2"> <div class="col mt-2">
@@ -121,6 +121,7 @@
</button></span>{%endfor%}</p> </button></span>{%endfor%}</p>
</div> </div>
</div> </div>
<h3>{{ object_list.count }} assets</h3>
<div class="row"> <div class="row">
<div class="col px-0"> <div class="col px-0">
{% include 'partials/asset_list_table.html' %} {% include 'partials/asset_list_table.html' %}

View File

@@ -0,0 +1,162 @@
{% extends 'base_assets.html' %}
{% load button from filters %}
{% load ids_from_objects from asset_tags %}
{% load widget_tweaks %}
{% load static %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}" async></script>
{% endblock %}
{% block js %}
{{ block.super }}
<script>
//Get querystring value
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
//Function used to remove querystring
function removeQString(key) {
var urlValue=document.location.href;
//Get query string value
var searchUrl=location.search;
if(key!=="") {
oldValue = getParameterByName(key);
removeVal=key+"="+oldValue;
if(searchUrl.indexOf('?'+removeVal+'&')!== "-1") {
urlValue=urlValue.replace('?'+removeVal+'&','?');
}
else if(searchUrl.indexOf('&'+removeVal+'&')!== "-1") {
urlValue=urlValue.replace('&'+removeVal+'&','&');
}
else if(searchUrl.indexOf('?'+removeVal)!== "-1") {
urlValue=urlValue.replace('?'+removeVal,'');
}
else if(searchUrl.indexOf('&'+removeVal)!== "-1") {
urlValue=urlValue.replace('&'+removeVal,'');
}
}
else {
var searchUrl=location.search;
urlValue=urlValue.replace(searchUrl,'');
}
history.pushState({state:1, rand: Math.random()}, '', urlValue);
window.location.reload(true);
}
</script>
{% endblock %}
{% block content %}
<h3>{{ object_list.count }} cables with a total length of {{ total_length|default:"0" }}m</h3>
<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">
<label for="category" class="sr-only">Category</label>
{% render_field form.category|attr:'multiple'|add_class:'selectpicker col-sm pl-0' 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">
<label for="status" class="sr-only">Status</label>
{% render_field form.status|attr:'multiple'|add_class:'selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
</div>
</div>
<div class="col">
<div class="form-group d-flex flex-nowrap">
<label for="cable_type" class="sr-only">Cable Type</label>
{% render_field form.cable_type|attr:'multiple'|add_class:'selectpicker col-sm' data-none-selected-text="Cable Type" data-header="Cable Type" data-actions-box="true" %}
</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>
</div>
</div>
<div class="row my-2">
<div class="col text-right px-0">
{% button 'new' 'asset_create' style="width: 6em" %}
{% if object_list %}
<a class="btn btn-primary" href="{% url 'generate_labels' object_list|ids_from_objects %}"><span class="fas fa-barcode"></span> Generate Labels</a>
{% endif %}
</div>
</div>
<div class="row my-2">
<div class="col bg-dark text-white rounded pt-3">
{# TODO Gotta be a cleaner way to do this... #}
<p><span class="ml-2">Active Filters: </span> {% for filter in category_filters %}<span class="badge badge-info mx-1 ">{{filter}}<button type="button" class="btn btn-link p-0 ml-1 align-baseline">
<span aria-hidden="true" class="fas fa-times" onclick="removeQString('category', '{{filter.id}}')"></span>
</button></span>{%endfor%}{% for filter in status_filters %}<span class="badge badge-info mx-1 ">{{filter}}<button type="button" class="btn btn-link p-0 ml-1 align-baseline">
<span aria-hidden="true" class="fas fa-times" onclick="removeQString('status', '{{filter.id}}')"></span>
</button></span>{%endfor%}</p>
</div>
</div>
<div class="row">
<div class="col px-0">
<div class="table-responsive">
<table class="table">
<thead class="thead-dark">
<tr>
<th scope="col">Asset ID</th>
<th scope="col">Description</th>
<th scope="col">Category</th>
<th scope="col">Status</th>
<th scope="col">Length</th>
<th scope="col">Cable Type</th>
<th scope="col" class="d-none d-sm-table-cell">Quick Links</th>
</tr>
</thead>
<tbody id="asset_table_body">
{% for item in object_list %}
<tr class="table-{{ item.status.display_class|default:'' }} assetRow">
<th scope="row" class="align-middle"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></th>
<td class="assetDesc"><span class="text-truncate d-inline-block align-middle">{{ item.description }}</span></td>
<td class="assetCategory align-middle">{{ item.category }}</td>
<td class="assetStatus align-middle">{{ item.status }}</td>
<td style="background-color:{% if item.length == 20.0 %}#304486{% elif item.length == 10.0 %}green{%elif item.length == 5.0 %}red{% endif %} !important;">{{ item.length }}m</td>
<td>{{ item.cable_type }}</td>
<td class="d-none d-sm-table-cell">
{% include 'partials/asset_list_buttons.html' %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="6">Nothing found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -12,22 +12,18 @@
</template> </template>
<stylesheet> <stylesheet>
<blockTableStyle id="table"> <blockTableStyle id="table">
<!-- show a grid: this also comes in handy for debugging your tables.-->
<lineStyle kind="GRID" colorName="black" thickness="1" start="0,0" stop="-1,-1" />
</blockTableStyle> </blockTableStyle>
</stylesheet> </stylesheet>
<story> <story>
<blockTable style="table"> <blockTable style="table">
{% for i in images0 %} {% for i in images0 %}
<tr> <tr>
<td>{% with images0|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0" <td>{% with images0|index:forloop.counter0 as image %}{% if image %}<illustration width="180" height="55" borderStrokeWidth="1"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td> borderStrokeColor="black"><image file="data:image/png;base64,{{image.1}}" x="0" y="0"
<td>{% with images1|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0" {% if image.0.csa >= 4 %}width="180" height="55"{% else %}width="130" height="38"{%endif%}/></illustration>{% endif %}{% endwith %}</td>
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td> <td>{% with images1|index:forloop.counter0 as image %}{% if image %}<illustration width="180" height="55"><image file="data:image/png;base64,{{image.1}}" x="0" y="0"
<td>{% with images2|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0" {% if image.0.csa >= 4 %}width="180" height="55"{% else %}width="130" height="38"{%endif%}/></illustration>{% endif %}{% endwith %}</td>
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td> <td>{% with images2|index:forloop.counter0 as image %}{% if image %}<illustration width="180" height="55"><image file="data:image/png;base64,{{image.1}}" x="0" y="0" {% if image.0.csa >= 4 %}width="180" height="55"{% else %}width="130" height="38"{%endif%}/></illustration>{% endif %}{% endwith %}</td>
<td>{% with images3|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</blockTable> </blockTable>

View File

@@ -0,0 +1,14 @@
{% load button from filters %}
{% if audit %}
<a type="button" class="btn btn-info btn-sm modal-href" href="{% url 'asset_audit' item.asset_id %}"><i class="fas fa-certificate"></i> Audit</a>
{% else %}
<div class="btn-group" role="group">
{% button 'view' url='asset_detail' pk=item.asset_id clazz="btn-sm" %}
{% if perms.assets.change_asset %}
{% button 'edit' url='asset_update' pk=item.asset_id clazz="btn-sm" %}
{% endif %}
{% if perms.assets.add_asset %}
{% button 'duplicate' url='asset_duplicate' pk=item.asset_id clazz="btn-sm" %}
{% endif %}
</div>
{% endif %}

View File

@@ -18,19 +18,7 @@
<td class="assetCategory align-middle">{{ item.category }}</td> <td class="assetCategory align-middle">{{ item.category }}</td>
<td class="assetStatus align-middle">{{ item.status }}</td> <td class="assetStatus align-middle">{{ item.status }}</td>
<td class="d-none d-sm-table-cell"> <td class="d-none d-sm-table-cell">
{% if audit %} {% include 'partials/asset_list_buttons.html' %}
<a type="button" class="btn btn-info btn-sm modal-href" href="{% url 'asset_audit' item.asset_id %}"><i class="fas fa-certificate"></i> Audit</a>
{% else %}
<div class="btn-group" role="group">
{% button 'view' url='asset_detail' pk=item.asset_id clazz="btn-sm" %}
{% if perms.assets.change_asset %}
{% button 'edit' url='asset_update' pk=item.asset_id clazz="btn-sm" %}
{% endif %}
{% if perms.assets.add_asset %}
{% button 'duplicate' url='asset_duplicate' pk=item.asset_id clazz="btn-sm" %}
{% endif %}
</div>
{% endif %}
</td> </td>
</tr> </tr>
{% empty %} {% empty %}

View File

@@ -1,7 +1,7 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% load title_spaced from filters %} {% load title_spaced from filters %}
{% spaceless %} {% spaceless %}
<label for="{{ field.id_for_label }}" {% if col %}class="col-2 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label> <label for="{{ field.id_for_label }}" {% if col %}class="col-4 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>
{% if append or prepend %} {% if append or prepend %}
<div class="input-group {{col}}"> <div class="input-group {{col}}">
{% if prepend %} {% if prepend %}

View File

@@ -7,7 +7,7 @@
{% if create or edit or duplicate %} {% if create or edit or duplicate %}
<div class="form-group" id="parent-group"> <div class="form-group" id="parent-group">
<label for="selectpicker">Set Parent</label> <label for="selectpicker">Set Parent</label>
<select name="parent" id="parent_id" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='asset' %}?fields=asset_id,description"> <select name="parent" id="parent_id" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='asset' %}?fields=asset_id,description">
{% if object.parent %} {% if object.parent %}
<option value="{{object.parent.pk}}" selected>{{object.parent.description}}</option> <option value="{{object.parent.pk}}" selected>{{object.parent.description}}</option>
{% endif %} {% endif %}

View File

@@ -10,7 +10,7 @@
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label> <label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}"> <select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
{% if object.purchased_from %} {% if object.purchased_from %}
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option> <option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
{% endif %} {% endif %}
@@ -39,10 +39,10 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.salvage_value.id_for_label }}">Salvage Value</label> <label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label>
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"><span class="input-group-text">£</span></div> <div class="input-group-prepend"><span class="input-group-text">£</span></div>
{% render_field form.salvage_value|add_class:'form-control' value=object.salvage_value %} {% render_field form.replacement_cost|add_class:'form-control' value=object.replacement_cost %}
</div> </div>
</div> </div>
@@ -70,8 +70,8 @@
<dd>{% if object.purchased_from %}<a href="{{object.purchased_from.get_absolute_url}}">{{ object.purchased_from }}</a>{%else%}-{%endif%}</dd> <dd>{% if object.purchased_from %}<a href="{{object.purchased_from.get_absolute_url}}">{{ object.purchased_from }}</a>{%else%}-{%endif%}</dd>
<dt>Purchase Price</dt> <dt>Purchase Price</dt>
<dd>£{{ object.purchase_price|default_if_none:'-' }}</dd> <dd>£{{ object.purchase_price|default_if_none:'-' }}</dd>
<dt>Salvage Value</dt> <dt>Replacement Cost</dt>
<dd>£{{ object.salvage_value|default_if_none:'-' }}</dd> <dd>£{{ object.replacement_cost|default_if_none:'-' }}</dd>
<dt>Date Acquired</dt> <dt>Date Acquired</dt>
<dd>{{ object.date_acquired|default_if_none:'-' }}</dd> <dd>{{ object.date_acquired|default_if_none:'-' }}</dd>
{% if object.date_sold %} {% if object.date_sold %}

View File

@@ -18,18 +18,23 @@ def status(db):
@pytest.fixture @pytest.fixture
def test_cable(db, category, status): def cable_type(db):
connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3) connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=connector, socket=connector) cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=connector, socket=connector)
cable = models.Asset.objects.create(asset_id="9666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5") yield cable_type
yield cable
connector.delete() connector.delete()
cable_type.delete() cable_type.delete()
@pytest.fixture
def test_cable(db, category, status, cable_type):
cable = models.Asset.objects.create(asset_id="9666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5", replacement_cost=50)
yield cable
cable.delete() cable.delete()
@pytest.fixture @pytest.fixture
def test_asset(db, category, status): def test_asset(db, category, status):
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26)) asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100)
yield asset yield asset
asset.delete() asset.delete()

View File

@@ -79,7 +79,7 @@ class AssetForm(FormPage):
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')), 'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')), 'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')), 'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')), 'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')), 'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
'date_sold': (regions.DatePicker, (By.ID, 'id_date_sold')), 'date_sold': (regions.DatePicker, (By.ID, 'id_date_sold')),
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')), 'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
@@ -221,7 +221,7 @@ class AssetAuditList(AssetList):
'description': (regions.TextBox, (By.ID, 'id_description')), 'description': (regions.TextBox, (By.ID, 'id_description')),
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')), 'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')), 'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')), 'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')), 'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')), 'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
'status': (regions.SingleSelectPicker, (By.ID, 'id_status')), 'status': (regions.SingleSelectPicker, (By.ID, 'id_status')),

View File

@@ -94,6 +94,55 @@ class TestAssetList(AutoLoginTest):
self.assertEqual("10", asset_ids[1]) self.assertEqual("10", asset_ids[1])
def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type):
page = pages.AssetCreate(logged_in_browser.driver, live_server.url).open()
wait = WebDriverWait(logged_in_browser.driver, 20)
page.description = str(cable_type)
page.category = category.name
page.status = status.name
page.serial_number = "MELON-MELON-MELON"
page.comments = "You might need that"
page.replacement_cost = "666"
page.is_cable = True
assert logged_in_browser.driver.find_element(By.ID, 'cable-table').is_displayed()
wait.until(animation_is_finished())
page.cable_type = str(cable_type)
page.length = 10
page.csa = "1.5"
page.submit()
assert page.success
def test_asset_edit(logged_in_browser, admin_user, live_server, test_asset):
page = pages.AssetEdit(logged_in_browser.driver, live_server.url, asset_id=test_asset.asset_id).open()
assert logged_in_browser.driver.find_element(By.ID, 'id_asset_id').get_attribute('readonly') is not None
new_description = "Big Shelf"
page.description = new_description
page.submit()
assert page.success
assert models.Asset.objects.get(asset_id=test_asset.asset_id).description == new_description
def test_asset_duplicate(logged_in_browser, admin_user, live_server, test_asset):
page = pages.AssetDuplicate(logged_in_browser.driver, live_server.url, asset_id=test_asset.asset_id).open()
assert test_asset.asset_id != page.asset_id
assert test_asset.description == page.description
assert test_asset.status.name == page.status
assert test_asset.category.name == page.category
assert test_asset.date_acquired == page.date_acquired.date()
page.submit()
assert page.success
assert models.Asset.objects.last().description == test_asset.description
@screenshot_failure_cls @screenshot_failure_cls
class TestAssetForm(AutoLoginTest): class TestAssetForm(AutoLoginTest):
def setUp(self): def setUp(self):
@@ -130,7 +179,7 @@ class TestAssetForm(AutoLoginTest):
self.page.comments = comments = "This is actually a sledgehammer, not a cable..." self.page.comments = comments = "This is actually a sledgehammer, not a cable..."
self.page.purchase_price = "12.99" self.page.purchase_price = "12.99"
self.page.salvage_value = "99.12" self.page.replacement_cost = "99.12"
self.page.date_acquired = acquired = datetime.date(2020, 5, 2) self.page.date_acquired = acquired = datetime.date(2020, 5, 2)
self.page.purchased_from_selector.toggle() self.page.purchased_from_selector.toggle()
self.assertTrue(self.page.purchased_from_selector.is_open) self.assertTrue(self.page.purchased_from_selector.is_open)
@@ -160,50 +209,6 @@ class TestAssetForm(AutoLoginTest):
# This one is important as it defaults to today's date # This one is important as it defaults to today's date
self.assertEqual(asset.date_acquired, acquired) self.assertEqual(asset.date_acquired, acquired)
def test_cable_create(self):
self.page.description = "IEC -> IEC"
self.page.category = "Health & Safety"
self.page.status = "O.K."
self.page.serial_number = "MELON-MELON-MELON"
self.page.comments = "You might need that"
self.page.is_cable = True
self.assertTrue(self.driver.find_element_by_id('cable-table').is_displayed())
self.wait.until(animation_is_finished())
self.page.cable_type = "IEC → IEC"
self.page.socket = "IEC"
self.page.length = 10
self.page.csa = "1.5"
self.page.submit()
self.assertTrue(self.page.success)
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'))
new_description = "Big Shelf"
self.page.description = new_description
self.page.submit()
self.assertTrue(self.page.success)
self.assertEqual(models.Asset.objects.get(asset_id=self.parent.asset_id).description, new_description)
def test_asset_duplicate(self):
self.page = pages.AssetDuplicate(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
self.assertNotEqual(self.parent.asset_id, self.page.asset_id)
self.assertEqual(self.parent.description, self.page.description)
self.assertEqual(self.parent.status.name, self.page.status)
self.assertEqual(self.parent.category.name, self.page.category)
self.assertEqual(self.parent.date_acquired, self.page.date_acquired.date())
self.page.submit()
self.assertTrue(self.page.success)
self.assertEqual(models.Asset.objects.last().description, self.parent.description)
@screenshot_failure_cls @screenshot_failure_cls
class TestSupplierList(AutoLoginTest): class TestSupplierList(AutoLoginTest):
@@ -283,6 +288,28 @@ def test_audit_search(logged_in_browser, live_server, test_asset):
assert logged_in_browser.is_text_present("Asset with that ID does not exist!") assert logged_in_browser.is_text_present("Asset with that ID does not exist!")
def test_audit_success(logged_in_browser, admin_user, live_server, test_asset):
page = pages.AssetAuditList(logged_in_browser.driver, live_server.url).open()
wait = WebDriverWait(logged_in_browser.driver, 20)
page.set_query(test_asset.asset_id)
page.search()
wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
# Now do it properly
page.modal.description = new_desc = "A BIG hammer"
page.modal.submit()
logged_in_browser.driver.implicitly_wait(4)
wait.until(animation_is_finished())
submit_time = timezone.now()
# Check data is correct
test_asset.refresh_from_db()
assert test_asset.description in new_desc
# Make sure audit 'log' was filled out
assert admin_user.initials == test_asset.last_audited_by.initials
assert_times_almost_equal(submit_time, test_asset.last_audited_at)
# Check we've removed it from the 'needing audit' list
assert test_asset.asset_id not in page.assets
@screenshot_failure_cls @screenshot_failure_cls
class TestAssetAudit(AutoLoginTest): class TestAssetAudit(AutoLoginTest):
def setUp(self): def setUp(self):
@@ -293,14 +320,14 @@ class TestAssetAudit(AutoLoginTest):
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1, self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1,
voltage_rating=40, num_pins=13) voltage_rating=40, num_pins=13)
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status, models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status,
category=self.category, date_acquired=datetime.date(2020, 2, 1)) category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status, models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status,
category=self.category, date_acquired=datetime.date(2020, 2, 1)) category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category, models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category,
date_acquired=datetime.date(2020, 2, 1)) date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
self.asset = models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status, self.asset = models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status,
category=self.category, category=self.category,
date_acquired=datetime.date(2020, 2, 1)) date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open() self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
self.wait = WebDriverWait(self.driver, 20) self.wait = WebDriverWait(self.driver, 20)
@@ -316,25 +343,6 @@ class TestAssetAudit(AutoLoginTest):
self.driver.implicitly_wait(4) self.driver.implicitly_wait(4)
self.assertIn("This field is required.", self.page.modal.errors["Description"]) self.assertIn("This field is required.", self.page.modal.errors["Description"])
def test_audit_success(self):
self.page.set_query(self.asset.asset_id)
self.page.search()
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
# Now do it properly
self.page.modal.description = new_desc = "A BIG hammer"
self.page.modal.submit()
self.driver.implicitly_wait(4)
self.wait.until(animation_is_finished())
submit_time = timezone.now()
# Check data is correct
self.asset.refresh_from_db()
self.assertEqual(self.asset.description, new_desc)
# Make sure audit 'log' was filled out
self.assertEqual(self.profile.initials, self.asset.last_audited_by.initials)
assert_times_almost_equal(submit_time, self.asset.last_audited_at)
# Check we've removed it from the 'needing audit' list
self.assertNotIn(self.asset.asset_id, self.page.assets)
def test_audit_list(self): def test_audit_list(self):
self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets)) self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets))
asset_row = self.page.assets[0] asset_row = self.page.assets[0]

View File

@@ -84,7 +84,7 @@ def test_oembed(client, test_asset):
def test_asset_create(admin_client): def test_asset_create(admin_client):
response = admin_client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'}) response = admin_client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'replacement_cost': '-30'})
assertFormError(response, 'form', 'asset_id', 'This field is required.') assertFormError(response, 'form', 'asset_id', 'This field is required.')
assert_asset_form_errors(response) assert_asset_form_errors(response)
@@ -99,7 +99,7 @@ def test_cable_create(admin_client):
def test_asset_edit(admin_client, test_asset): def test_asset_edit(admin_client, test_asset):
url = reverse('asset_update', kwargs={'pk': test_asset.asset_id}) url = reverse('asset_update', kwargs={'pk': test_asset.asset_id})
response = admin_client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""}) response = admin_client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'replacement_cost': '-50', 'description': "", 'status': "", 'category': ""})
assert_asset_form_errors(response) assert_asset_form_errors(response)
@@ -127,4 +127,4 @@ def assert_asset_form_errors(response):
assertFormError(response, 'form', 'category', 'This field is required.') assertFormError(response, 'form', 'category', 'This field is required.')
assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired') assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative') assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative') assertFormError(response, 'form', 'replacement_cost', 'A price cannot be negative')

View File

@@ -20,8 +20,9 @@ urlpatterns = [
path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset') path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset')
(views.AssetDuplicate.as_view()), name='asset_duplicate'), (views.AssetDuplicate.as_view()), name='asset_duplicate'),
path('asset/id/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'), path('asset/id/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'),
path('asset/<list:ids>/list/label', views.GenerateLabels.as_view(), name='generate_labels'), path('asset/<list:ids>/list/label', views.GenerateLabels.as_view(), name='generate_labels'),
path('cables/list/', login_required(views.CableList.as_view()), name='cable_list'),
path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'), path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'),
path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'), path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),
path('cabletype/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'), path('cabletype/<int:pk>/update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'),

View File

@@ -6,7 +6,7 @@ from io import BytesIO
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core import serializers from django.core import serializers
from django.db.models import Q from django.db.models import Q, Sum
from django.http import Http404, HttpResponse, JsonResponse from django.http import Http404, HttpResponse, JsonResponse
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
@@ -17,7 +17,7 @@ from django.shortcuts import get_object_or_404
from django.template.loader import get_template from django.template.loader import get_template
from PyPDF2 import PdfFileMerger, PdfFileReader from PyPDF2 import PdfFileMerger, PdfFileReader
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont, ImageOps
from barcode import Code39 from barcode import Code39
from barcode.writer import ImageWriter from barcode.writer import ImageWriter
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
@@ -25,14 +25,12 @@ from z3c.rml import rml2pdf
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \ from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \
is_ajax, OEmbedView is_ajax, OEmbedView
from assets import forms, models from assets import forms, models
from assets.models import get_available_asset_id
class AssetList(LoginRequiredMixin, generic.ListView): class AssetList(LoginRequiredMixin, generic.ListView):
model = models.Asset model = models.Asset
template_name = 'asset_list.html' template_name = 'asset_list.html'
paginate_by = 40 paginate_by = 40
ordering = ['-pk']
hide_hidden_status = True hide_hidden_status = True
def get_initial(self): def get_initial(self):
@@ -50,13 +48,7 @@ class AssetList(LoginRequiredMixin, generic.ListView):
# TODO Feedback to user when search fails # TODO Feedback to user when search fails
query_string = form.cleaned_data['q'] or "" query_string = form.cleaned_data['q'] or ""
if len(query_string) == 0: queryset = models.Asset.objects.search(query=query_string)
queryset = self.model.objects.all()
elif len(query_string) >= 3:
queryset = self.model.objects.filter(
Q(asset_id__exact=query_string.upper()) | Q(description__icontains=query_string) | Q(serial_number__exact=query_string))
else:
queryset = self.model.objects.filter(Q(asset_id__exact=query_string.upper()))
if form.cleaned_data['is_cable']: if form.cleaned_data['is_cable']:
queryset = queryset.filter(is_cable=True) queryset = queryset.filter(is_cable=True)
@@ -87,6 +79,25 @@ class AssetList(LoginRequiredMixin, generic.ListView):
return context return context
class CableList(AssetList):
template_name = 'cable_list.html'
paginator = None
def get_queryset(self):
queryset = super().get_queryset().filter(is_cable=True)
if self.form.cleaned_data['cable_type']:
queryset = queryset.filter(cable_type__in=self.form.cleaned_data['cable_type'])
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = "Cable List"
context["total_length"] = self.get_queryset().aggregate(Sum('length'))['length__sum']
return context
class AssetIDUrlMixin: class AssetIDUrlMixin:
def get_object(self, queryset=None): def get_object(self, queryset=None):
pk = self.kwargs.get(self.pk_url_kwarg) pk = self.kwargs.get(self.pk_url_kwarg)
@@ -140,7 +151,7 @@ class AssetCreate(LoginRequiredMixin, generic.CreateView):
def get_initial(self, *args, **kwargs): def get_initial(self, *args, **kwargs):
initial = super().get_initial(*args, **kwargs) initial = super().get_initial(*args, **kwargs)
initial["asset_id"] = get_available_asset_id() initial["asset_id"] = models.get_available_asset_id()
return initial return initial
def get_success_url(self): def get_success_url(self):
@@ -155,6 +166,11 @@ class DuplicateMixin:
class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate): class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
def get_initial(self, *args, **kwargs):
initial = super().get_initial(*args, **kwargs)
initial["asset_id"] = models.get_available_asset_id(wanted_prefix=self.get_object().asset_id_prefix)
return initial
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["create"] = None context["create"] = None
@@ -176,6 +192,7 @@ class AssetOEmbed(OEmbedView):
class AssetAuditList(AssetList): class AssetAuditList(AssetList):
template_name = 'asset_audit_list.html' template_name = 'asset_audit_list.html'
hide_hidden_status = True
# TODO Refresh this when the modal is submitted # TODO Refresh this when the modal is submitted
def get_queryset(self): def get_queryset(self):
@@ -262,7 +279,7 @@ class SupplierUpdate(GenericUpdateView, ModalURLMixin):
form_class = forms.SupplierForm form_class = forms.SupplierForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SupplierUpdate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if is_ajax(self.request): if is_ajax(self.request):
context['override'] = "base_ajax.html" context['override'] = "base_ajax.html"
else: else:
@@ -306,7 +323,6 @@ class CableTypeCreate(generic.CreateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["create"] = True context["create"] = True
context["page_title"] = "Create Cable Type" context["page_title"] = "Create Cable Type"
return context return context
def get_success_url(self): def get_success_url(self):
@@ -322,7 +338,6 @@ class CableTypeUpdate(generic.UpdateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["edit"] = True context["edit"] = True
context["page_title"] = f"Edit Cable Type {self.object}" context["page_title"] = f"Edit Cable Type {self.object}"
return context return context
def get_success_url(self): def get_success_url(self):
@@ -333,7 +348,9 @@ def generate_label(pk):
black = (0, 0, 0) black = (0, 0, 0)
white = (255, 255, 255) white = (255, 255, 255)
size = (700, 200) size = (700, 200)
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20) font_size = 22
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", font_size)
heavy_font = ImageFont.truetype("static/fonts/OpenSans-Bold.tff", font_size + 13)
obj = get_object_or_404(models.Asset, asset_id=pk) obj = get_object_or_404(models.Asset, asset_id=pk)
asset_id = f"Asset: {obj.asset_id}" asset_id = f"Asset: {obj.asset_id}"
@@ -342,22 +359,25 @@ def generate_label(pk):
csa = f"CSA: {obj.csa}mm²" csa = f"CSA: {obj.csa}mm²"
image = Image.new("RGB", size, white) image = Image.new("RGB", size, white)
image = ImageOps.expand(image, border=(5, 5, 5, 5), fill=black)
logo = Image.open("static/imgs/square_logo.png") logo = Image.open("static/imgs/square_logo.png")
draw = ImageDraw.Draw(image) draw = ImageDraw.Draw(image)
draw.text((210, 140), asset_id, fill=black, font=font) draw.text((300, 0), asset_id, fill=black, font=heavy_font)
if obj.is_cable: if obj.is_cable:
draw.text((210, 170), length, fill=black, font=font) y = 140
draw.text((360, 170), csa, fill=black, font=font) draw.text((210, y), length, fill=black, font=font)
draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", fill=black, font=font) if obj.csa:
draw.text((365, y), csa, fill=black, font=font)
draw.text((210, size[1] - font_size - 8), "TEC PA & Lighting (0115) 84 68720", fill=black, font=font)
barcode = Code39(str(obj.asset_id), writer=ImageWriter()) barcode = Code39(str(obj.asset_id), writer=ImageWriter())
logo_size = (200, 200) logo_size = (200, 200)
image.paste(logo.resize(logo_size, Image.ANTIALIAS)) image.paste(logo.resize(logo_size, Image.ANTIALIAS), box=(5, 5))
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False}) barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
width, height = barcode_image.size width, height = barcode_image.size
image.paste(barcode_image.crop((0, 0, width, 135)), (int(((size[0] + logo_size[0]) - width) / 2), 0)) image.paste(barcode_image.crop((0, 0, width, 100)), (int(((size[0] + logo_size[0]) - width) / 2), 40))
return image return image
@@ -386,14 +406,16 @@ class GenerateLabels(generic.View):
base64_encoded_result_bytes = base64.b64encode(img_bytes) base64_encoded_result_bytes = base64.b64encode(img_bytes)
base64_encoded_result_str = base64_encoded_result_bytes.decode('ascii') base64_encoded_result_str = base64_encoded_result_bytes.decode('ascii')
images.append(base64_encoded_result_str) images.append((get_object_or_404(models.Asset, asset_id=asset_id), base64_encoded_result_str))
name = f"Asset Label Sheet generated at {timezone.now()}"
context = { context = {
'images0': images[::4], 'images0': images[::3],
'images1': images[1::4], 'images1': images[1::3],
'images2': images[2::4], 'images2': images[2::3],
'images3': images[3::4], # 'images3': images[3::4],
'filename': "Asset Label Sheet generated at {}".format(timezone.now()) 'filename': name
} }
merger = PdfFileMerger() merger = PdfFileMerger()
@@ -405,6 +427,6 @@ class GenerateLabels(generic.View):
merged = BytesIO() merged = BytesIO()
merger.write(merged) merger.write(merged)
response['Content-Disposition'] = 'filename="{}"'.format(context['filename']) response['Content-Disposition'] = f'filename="{name}"'
response.write(merged.getvalue()) response.write(merged.getvalue())
return response return response

View File

@@ -28,6 +28,7 @@ def admin_user(admin_user):
admin_user.last_name = "Test" admin_user.last_name = "Test"
admin_user.initials = "ETU" admin_user.initials = "ETU"
admin_user.is_approved = True admin_user.is_approved = True
admin_user.is_supervisor = True
admin_user.save() admin_user.save()
return admin_user return admin_user

1315
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
"autocompleter": "^6.1.2", "autocompleter": "^6.1.2",
"autoprefixer": "^10.4.0", "autoprefixer": "^10.4.0",
"bootstrap": "^4.6.1", "bootstrap": "^4.6.1",
"bootstrap-select": "^1.13.17", "bootstrap-select": "^1.13.18",
"clipboard": "^2.0.8", "clipboard": "^2.0.8",
"cssnano": "^5.0.13", "cssnano": "^5.0.13",
"easymde": "^2.16.1", "easymde": "^2.16.1",
@@ -27,14 +27,14 @@
"html5sortable": "^0.13.3", "html5sortable": "^0.13.3",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"konami": "^1.6.3", "konami": "^1.6.3",
"moment": "^2.27.0", "moment": "^2.29.4",
"node-sass": "^7.0.0", "node-sass": "^7.0.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"postcss": "^8.4.5", "postcss": "^8.4.5",
"uglify-js": "^3.14.5" "uglify-js": "^3.14.5"
}, },
"devDependencies": { "devDependencies": {
"browser-sync": "^2.27.7" "browser-sync": "^2.27.10"
}, },
"scripts": { "scripts": {
"gulp": "gulp", "gulp": "gulp",

View File

@@ -47,14 +47,16 @@ function initPicker(obj) {
//log: 3, //log: 3,
preprocessData: function (data) { preprocessData: function (data) {
var i, l = data.length, array = []; var i, l = data.length, array = [];
array.push({ if (!obj.data('noclear')) {
text: clearSelectionLabel, array.push({
value: '', text: clearSelectionLabel,
data:{ value: '',
update_url: '', data:{
subtext:'' update_url: '',
} subtext:''
}); }
});
}
if (l) { if (l) {
for(i = 0; i < l; i++){ for(i = 0; i < l; i++){
@@ -71,11 +73,13 @@ function initPicker(obj) {
return array; return array;
} }
}; };
console.log(obj.data);
obj.prepend($("<option></option>") if (!obj.data('noclear')) {
.attr("value",'') obj.prepend($("<option></option>")
.text(clearSelectionLabel) .attr("value",'')
.data('update_url','')); //Add "clear selection" option .text(clearSelectionLabel)
.data('update_url','')); //Add "clear selection" option
}
obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker

View File

@@ -33,6 +33,25 @@ $fa-font-path: '/static/fonts';
@import "node_modules/@fortawesome/fontawesome-free/scss/fontawesome"; @import "node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
@import "node_modules/@fortawesome/fontawesome-free/scss/solid"; @import "node_modules/@fortawesome/fontawesome-free/scss/solid";
html {
--brand: #3F58AA;
scrollbar-color: #3F58AA Canvas !important;
}
:root { accent-color: var(--brand); }
:focus-visible { outline-color: var(--brand); }
::selection { background-color: var(--brand); }
::marker { color: var(--brand); }
:is(
::-webkit-calendar-picker-indicator,
::-webkit-clear-button,
::-webkit-inner-spin-button,
::-webkit-outer-spin-button
) {
color: var(--brand);
}
@media screen and @media screen and
(prefers-reduced-motion: reduce), (prefers-reduced-motion: reduce),
(update: slow) { (update: slow) {
@@ -175,7 +194,7 @@ svg {
span.fas { span.fas {
padding-left: 0.1em !important; padding-left: 0.1em !important;
padding-right: 0.1em !important; padding-right: 0.3em !important;
} }
html.embedded { html.embedded {
@@ -252,3 +271,13 @@ html.embedded {
} }
} }
} }
.card, .card-header, .btn, input, select, .CodeMirror, .editor-toolbar, .card-img-top {
border-radius: 0 !important;
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
.bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light {
padding-right: 1rem !important;
}

View File

@@ -3,7 +3,7 @@
{% block content %} {% block content %}
<form action="" method="post">{% csrf_token %} <form action="" method="post">{% csrf_token %}
<p>The following objects will be merged. Please select the 'master' record which you would like to keep. Other records will have associated events moved to the 'master' copy, and then will be deleted.</p> <p>The following objects will be merged. Please select the 'master' record which you would like to keep. This may take some time.</p>
<table> <table>
{% for form in forms %} {% for form in forms %}

View File

@@ -1,13 +1,12 @@
{% load nice_errors from filters %} {% load nice_errors from filters %}
{% if form.errors %} {% if form.errors %}
<div class="alert alert-danger alert-dismissable"> <div class="alert alert-danger mb-0">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<dl> <dl>
{% with form|nice_errors as qq %} {% with form|nice_errors as qq %}
{% for error_name,desc in qq.items %} {% for error_name,desc in qq.items %}
<span class="row"> <span class="row">
<dt class="col-4">{{error_name}}</dt> <dt class="col-3">{{error_name}}</dt>
<dd class="col-8">{{desc}}</dd> <dd class="col-9">{{desc}}</dd>
</span> </span>
{% endfor %} {% endfor %}
{% endwith %} {% endwith %}

View File

@@ -1,6 +1,7 @@
{% extends override|default:"base_rigs.html" %} {% extends override|default:"base_rigs.html" %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% load button from filters %} {% load button from filters %}
{% load markdown_tags %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
@@ -22,7 +23,7 @@
<dd>{{ object.address|linebreaksbr }}</dd> <dd>{{ object.address|linebreaksbr }}</dd>
<dt>Notes</dt> <dt>Notes</dt>
<dd>{{ object.notes|linebreaksbr }}</dd> <dd>{{ object.notes|markdown }}</dd>
{% if object.three_phase_available is not None %} {% if object.three_phase_available is not None %}
<dt>Three Phase Available</dt> <dt>Three Phase Available</dt>

View File

@@ -1,6 +1,27 @@
{% extends override|default:"base_rigs.html" %} {% extends override|default:"base_rigs.html" %}
{% load button from filters %} {% load button from filters %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% load static %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/easymde.min.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
{% endblock %}
{% block js %}
{{ block.super }}
<script>
$(document).ready(function () {
setupMDE('.md-enabled');
});
</script>
{% endblock %}
{% block content %} {% block content %}
<div class="col"> <div class="col">
@@ -43,7 +64,7 @@
<label for="{{ form.notes.id_for_label }}" <label for="{{ form.notes.id_for_label }}"
class="col-sm-2 control-label">{{ form.notes.label }}</label> class="col-sm-2 control-label">{{ form.notes.label }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
{% render_field form.notes class+="form-control" placeholder=form.notes.label %} {% render_field form.notes class+="form-control md-enabled" placeholder=form.notes.label %}
</div> </div>
</div> </div>
{% if form.three_phase_available is not None %} {% if form.three_phase_available is not None %}

View File

@@ -4,6 +4,8 @@
<a href="{% url target pk %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%} {% if text == 'Print' %}target="_blank"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a> <a href="{% url target pk %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%} {% if text == 'Print' %}target="_blank"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
{% elif copy %} {% elif copy %}
<button class="btn btn-secondary btn-sm mr-1" data-clipboard-target="{{id}}" data-content="Copied to clipboard!"><span class="fas fa-copy"></span></button> <button class="btn btn-secondary btn-sm mr-1" data-clipboard-target="{{id}}" data-content="Copied to clipboard!"><span class="fas fa-copy"></span></button>
{% elif today %}
<button class="btn btn-info col-sm-2" onclick="var date = new Date(); $('#{{id}}').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'))" tabindex="-1" type="button">Today</button>
{% else %} {% else %}
<a href="{% url target %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a> <a href="{% url target %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
{% endif %} {% endif %}

View File

@@ -1,38 +1,11 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
<form id="searchForm" class="form-inline flex-nowrap mx-md-3 px-2 border border-light rounded w-75" role="form" method="GET" action="{% url 'event_archive' %}"> <form id="searchForm" class="form-inline flex-nowrap mx-md-3 px-2 border border-light rounded" role="form" method="GET" action="{% url 'search' %}">
<div class="input-group input-group-sm flex-nowrap"> <div class="input-group">
<div class="input-group-prepend"> <input id="id_search_input" type="search" name="q" class="form-control form-control-sm" placeholder="Search..." value="{{ request.GET.q }}" />
<input id="id_search_input" type="search" name="q" class="form-control form-control-sm" placeholder="Search..." value="{{ request.GET.q }}" /> <div class="input-group-append">
<button class="btn btn-info form-control form-control-sm btn-sm"><span class="fas fa-search"></span><span class="sr-only"> Search</span></button>
</div> </div>
<select id="search-options" class="custom-select form-control" style="border-top-right-radius: 0px; border-bottom-right-radius: 0px; width: 15ch;">
<option selected data-action="{% url 'event_archive' %}" href="#">Events</option>
<option data-action="{% url 'person_list' %}" href="#">People</option>
<option data-action="{% url 'organisation_list' %}" href="#">Organisations</option>
<option data-action="{% url 'venue_list' %}" href="#">Venues</option>
{% if perms.RIGS.view_invoice %}
<option data-action="{% url 'invoice_archive' %}" href="#">Invoices</option>
{% endif %}
<option data-action="{% url 'asset_list' %}" href="#">Assets</option>
<option data-action="{% url 'supplier_list' %}" href="#">Suppliers</option>
</select>
</div> </div>
<button class="btn btn-info form-control form-control-sm btn-sm w-25" style="border-top-left-radius: 0px;border-bottom-left-radius: 0px;"><span class="fas fa-search"></span><span class="sr-only"> Search</span></button>
<a href="{% url 'search_help' %}" class="nav-link modal-href ml-1"><span class="fas fa-question-circle"></span></a> <a href="{% url 'search_help' %}" class="nav-link modal-href ml-1"><span class="fas fa-question-circle"></span></a>
</form> </form>
{% endif %} {% endif %}
{% block js %}
<script>
$('#search-options').change(function(){
$('#searchForm').attr('action', $(this).children('option:selected').data('action'));
});
$(document).ready(function(){
$('#id_search_input').keypress(function (e) {
if (e.which == 13) {
$('#searchForm').attr('action', $('#search-options option').first().data('action')).submit();
return false;
}
});
});
</script>
{% endblock %}

View File

@@ -1,5 +1,10 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% include 'form_errors.html' %} {% include 'form_errors.html' %}
{% if form.errors %}
<div class="alert alert-info">
<p><strong>Please note:</strong> If it has been more than a year since you last logged in, your account will have been automatically deactivated. Contact <a href="mailto:it@nottinghamtec.co.uk">it@nottinghamtec.co.uk</a> for assistance.</p>
</div>
{% endif %}
<div class="col-sm-6 offset-sm-3 col-lg-4 offset-lg-4"> <div class="col-sm-6 offset-sm-3 col-lg-4 offset-lg-4">
<form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %} <form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %}
<div class="form-group"> <div class="form-group">

View File

@@ -0,0 +1,48 @@
{% extends "base_rigs.html" %}
{% load to_class_name from filters %}
{% load markdown_tags %}
{% block content %}
{% include 'partials/search.html' %}
{% for object in object_list %}
{% with object|to_class_name as klass %}
<div class="card m-2">
<h4 class="card-header"><a href='{{ object.get_absolute_url }}'>[{{ klass }}] {{ object }}</a>
<small>
{% if klass == "Event" %}
{% if object.venue %}
<strong>Venue:</strong> {{ object.venue }}
{% endif %}
{% if object.is_rig %}
<strong>Client:</strong> {{ object.person.name }}
{% if object.organisation %}
for {{ object.organisation.name }}
{% endif %}
{% if object.dry_hire %}(Dry Hire){% endif %}
{% else %}
<strong>Non-Rig</strong>
{% endif %}
<strong>Times:</strong>
{{ object.start_date|date:"D d/m/Y" }}
{% if object.has_start_time %}
{{ object.start_time|date:"H:i" }}
{% endif %}
{% if object.end_date or object.has_end_time %}
&ndash;
{% endif %}
{% if object.end_date and object.end_date != object.start_date %}
{{ object.end_date|date:"D d/m/Y" }}
{% endif %}
{% if object.has_end_time %}
{{ object.end_time|date:"H:i" }}
{% endif %}
{% endif %}
</small>
</h4>
</div>
{% endwith %}
{% empty %}
<h3 class="py-3 text-warning">No results found</h3>
{% endfor %}
{% endblock content %}

View File

@@ -6,6 +6,10 @@ from reversion.admin import VersionAdmin
admin.site.register(models.TrainingCategory, VersionAdmin) admin.site.register(models.TrainingCategory, VersionAdmin)
admin.site.register(models.TrainingItem, VersionAdmin) admin.site.register(models.TrainingItem, VersionAdmin)
admin.site.register(models.TrainingLevel, VersionAdmin) admin.site.register(models.TrainingLevel, VersionAdmin)
admin.site.register(models.TrainingItemQualification, VersionAdmin)
admin.site.register(models.TrainingLevelQualification, VersionAdmin) admin.site.register(models.TrainingLevelQualification, VersionAdmin)
admin.site.register(models.TrainingLevelRequirement, VersionAdmin) admin.site.register(models.TrainingLevelRequirement, VersionAdmin)
@admin.register(models.TrainingItemQualification)
class TrainingItemQualificationAdmin(VersionAdmin):
list_display = ['__str__', 'trainee']

View File

@@ -1,5 +1,5 @@
from PyRIGS.decorators import user_passes_test_with_403 from PyRIGS.decorators import user_passes_test_with_403
def has_perm_or_supervisor(perm, login_url=None, oembed_view=None): def is_supervisor(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, 'is_supervisor') and u.is_supervisor))

View File

@@ -1,40 +1,50 @@
import datetime
from django import forms from django import forms
from training import models from training import models
from RIGS.models import Profile
class QualificationForm(forms.ModelForm): class QualificationForm(forms.ModelForm):
related_models = {
'item': models.TrainingItem,
'supervisor': models.Trainee
}
class Meta: class Meta:
model = models.TrainingItemQualification model = models.TrainingItemQualification
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs):
pk = kwargs.pop('pk', None)
super().__init__(*args, **kwargs)
self.fields['trainee'].initial = Profile.objects.get(pk=pk)
self.fields['date'].widget.format = '%Y-%m-%d'
def clean_date(self): def clean_date(self):
date = self.cleaned_data['date'] date = self.cleaned_data.get('date')
if date > date.today(): if date > date.today():
raise forms.ValidationError('Qualification date may not be in the future') raise forms.ValidationError('Qualification date may not be in the future')
return date return date
def clean_supervisor(self): def clean_supervisor(self):
supervisor = self.cleaned_data['supervisor'] supervisor = self.cleaned_data.get('supervisor')
item = self.cleaned_data['item'] if supervisor.pk == self.cleaned_data.get('trainee').pk:
if supervisor.pk == self.cleaned_data['trainee'].pk:
raise forms.ValidationError('One may not supervise oneself...') raise forms.ValidationError('One may not supervise oneself...')
if item.category.training_level:
if not supervisor.level_qualifications.filter(level=item.category.training_level):
raise forms.ValidationError('Selected supervising person is missing requisite training level to train in this department')
elif not supervisor.is_supervisor:
raise forms.ValidationError('Selected supervisor must actually *be* a supervisor...')
return supervisor return supervisor
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['date'].widget.format = '%Y-%m-%d'
class AddQualificationForm(QualificationForm):
def __init__(self, *args, **kwargs):
pk = kwargs.pop('pk', None)
super().__init__(*args, **kwargs)
if pk:
self.fields['trainee'].initial = models.Trainee.objects.get(pk=pk)
class RequirementForm(forms.ModelForm): class RequirementForm(forms.ModelForm):
related_models = {
'item': models.TrainingItem
}
depth = forms.ChoiceField(choices=models.TrainingItemQualification.CHOICES) depth = forms.ChoiceField(choices=models.TrainingItemQualification.CHOICES)
class Meta: class Meta:
@@ -45,3 +55,26 @@ class RequirementForm(forms.ModelForm):
pk = kwargs.pop('pk', None) pk = kwargs.pop('pk', None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['level'].initial = models.TrainingLevel.objects.get(pk=pk) self.fields['level'].initial = models.TrainingLevel.objects.get(pk=pk)
class SessionLogForm(forms.Form):
trainees = forms.ModelMultipleChoiceField(models.Trainee.objects.all())
items_0 = forms.ModelMultipleChoiceField(models.TrainingItem.objects.all(), required=False)
items_1 = forms.ModelMultipleChoiceField(models.TrainingItem.objects.all(), required=False)
items_2 = forms.ModelMultipleChoiceField(models.TrainingItem.objects.all(), required=False)
supervisor = forms.ModelChoiceField(models.Trainee.objects.all())
date = forms.DateField(initial=datetime.date.today)
notes = forms.CharField(required=False, widget=forms.Textarea)
related_models = {
'supervisor': models.Trainee
}
def clean_date(self):
return QualificationForm.clean_date(self)
def clean_supervisor(self):
supervisor = self.cleaned_data['supervisor']
if supervisor in self.cleaned_data.get('trainees', []):
raise forms.ValidationError('One may not supervise oneself...')
return supervisor

View File

@@ -150,7 +150,7 @@ class Command(BaseCommand):
number = previous_item.reference_number + 1 number = previous_item.reference_number + 1
else: else:
number = 0 number = 0
item = models.TrainingItem.objects.create(category=category, reference_number=number, name=name) item = models.TrainingItem.objects.create(category=category, reference_number=number, description=name)
self.items.append(item) self.items.append(item)
def setup_levels(self): def setup_levels(self):

View File

@@ -55,7 +55,7 @@ class Command(BaseCommand):
supervisor = Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor", supervisor = Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor",
initials="SV", initials="SV",
email="supervisor@example.com", is_active=True, email="supervisor@example.com", is_active=True,
is_staff=True, is_approved=True) is_staff=True, is_approved=True, is_supervisor=True)
supervisor.set_password('supervisor') supervisor.set_password('supervisor')
supervisor.groups.add(Group.objects.get(name="Keyholders")) supervisor.groups.add(Group.objects.get(name="Keyholders"))
supervisor.save() supervisor.save()

View File

@@ -104,19 +104,19 @@ class Command(BaseCommand):
obj, created = models.TrainingItem.objects.update_or_create( obj, created = models.TrainingItem.objects.update_or_create(
pk=int(child.find('ID').text), pk=int(child.find('ID').text),
reference_number=number, reference_number=number,
name=name, description=name,
category=category, category=category,
active=active active=active
) )
except IntegrityError: except IntegrityError:
print("Training Item {}.{} {} has a duplicate reference number".format(category.reference_number, number, name)) print(f"Training Item {category.reference_number}.{number} {name} has a duplicate reference number")
if created: if created:
tally[1] += 1 tally[1] += 1
else: else:
tally[0] += 1 tally[0] += 1
print('Training Items - Updated: {}, Created: {}'.format(tally[0], tally[1])) print(f'Training Items - Updated: {tally[0]}, Created: {tally[1]}')
def import_TrainingItemQualification(self): def import_TrainingItemQualification(self):
tally = [0, 0, 0] tally = [0, 0, 0]
@@ -129,9 +129,9 @@ class Command(BaseCommand):
("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT), ] ("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT), ]
for (depth, depth_index) in depths: for (depth, depth_index) in depths:
if child.find('{}_Date'.format(depth)) is not None: if child.find(f'{depth}_Date') is not None:
if child.find('{}_Assessor_ID'.format(depth)) is None: if child.find(f'{depth}_Assessor_ID') is None:
print("Training Record #{} had no supervisor. Assigning System User.".format(child.find('ID').text)) print(f"Training Record #{child.find('ID').text} had no supervisor. Assigning System User.")
supervisor = Profile.objects.get(first_name="God") supervisor = Profile.objects.get(first_name="God")
continue continue
supervisor = Profile.objects.get(pk=self.id_map[child.find('{}_Assessor_ID'.format(depth)).text]) supervisor = Profile.objects.get(pk=self.id_map[child.find('{}_Assessor_ID'.format(depth)).text])

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.11 on 2022-01-30 11:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('training', '0003_trainingcategory_training_level'),
]
operations = [
migrations.RenameField(
model_name='trainingitem',
old_name='name',
new_name='description',
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.2.12 on 2022-02-23 15:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('training', '0004_rename_name_trainingitem_description'),
]
operations = [
migrations.AlterModelOptions(
name='trainingcategory',
options={'ordering': ['reference_number'], 'verbose_name_plural': 'Training Categories'},
),
migrations.AddField(
model_name='trainingitem',
name='prerequisites',
field=models.ManyToManyField(blank=True, to='training.TrainingItem'),
),
]

View File

@@ -1,15 +1,32 @@
from RIGS.models import Profile import datetime
from RIGS.models import Profile, filter_by_pk
from reversion import revisions as reversion from reversion import revisions as reversion
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Q, F, Value, CharField
from django.db.models.functions import Concat
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from versioning.versioning import RevisionMixin from versioning.versioning import RevisionMixin
from queryable_properties.properties import queryable_property
from queryable_properties.managers import QueryablePropertiesManager
from django.utils.translation import gettext_lazy as _
class TraineeManager(models.Manager): class TraineeManager(models.Manager):
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter(is_active=True, is_approved=True) return super().get_queryset().filter(is_active=True, is_approved=True)
def search(self, query=None):
qs = self.get_queryset()
if query is not None:
or_lookup = (Q(first_name__icontains=query) |
Q(last_name__icontains=query) | Q(initials__icontains=query)
)
or_lookup = filter_by_pk(or_lookup, query)
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
@reversion.register(for_concrete_model=False, fields=['is_supervisor']) @reversion.register(for_concrete_model=False, fields=['is_supervisor'])
class Trainee(Profile, RevisionMixin): class Trainee(Profile, RevisionMixin):
@@ -61,25 +78,64 @@ class TrainingCategory(models.Model):
class Meta: class Meta:
verbose_name_plural = 'Training Categories' verbose_name_plural = 'Training Categories'
ordering = ['reference_number']
class TrainingItemManager(QueryablePropertiesManager):
def search(self, query=None):
qs = self.get_queryset()
if query is not None:
or_lookup = (Q(description__icontains=query) | Q(display_id=query))
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
@reversion.register @reversion.register
class TrainingItem(models.Model): class TrainingItem(models.Model):
reference_number = models.IntegerField() reference_number = models.IntegerField()
category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.CASCADE) category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.CASCADE)
name = models.CharField(max_length=50) description = models.CharField(max_length=50)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)
prerequisites = models.ManyToManyField('self', symmetrical=False, blank=True)
objects = TrainingItemManager()
@property @property
def name(self):
return str(self)
@queryable_property
def display_id(self): def display_id(self):
return f"{self.category.reference_number}.{self.reference_number}" return f"{self.category.reference_number}.{self.reference_number}"
@display_id.filter
@classmethod
def display_id(cls, lookup, value):
if '.' in str(value):
try:
category_number, number = value.split('.', 2)
if category_number and number:
return Q(category__reference_number=int(category_number), reference_number=int(number))
except ValueError:
pass
return models.Q()
def __str__(self): def __str__(self):
name = f"{self.display_id} {self.name}" name = f"{self.display_id} {self.description}"
if not self.active: if not self.active:
name += " (inactive)" name += " (inactive)"
return name return name
def get_absolute_url(self):
return reverse('item_list')
def has_prereqs(self):
return self.prerequisites.all().exists()
def user_has_requirements(self, user):
# Always true if there are no prerequisites, otherwise get a set of prerequsite IDs and check if they are a subset of the set of qualification IDs
return not self.has_prereqs() or set(self.prerequisites.values_list('pk', flat=True)).issubset(set(user.qualifications_obtained.values_list('item', flat=True)))
@staticmethod @staticmethod
def user_has_qualification(item, user, depth): def user_has_qualification(item, user, depth):
return user.qualifications_obtained.only('item', 'depth').filter(item=item, depth__gte=depth).exists() return user.qualifications_obtained.only('item', 'depth').filter(item=item, depth__gte=depth).exists()
@@ -89,6 +145,21 @@ class TrainingItem(models.Model):
ordering = ['category__reference_number', 'reference_number'] ordering = ['category__reference_number', 'reference_number']
class TrainingItemQualificationManager(QueryablePropertiesManager):
def search(self, query=None):
qs = self.get_queryset().select_related('item', 'supervisor', 'item__category')
if query is not None:
or_lookup = (Q(item__description__icontains=query) | Q(supervisor__first_name__icontains=query) | Q(supervisor__last_name__icontains=query) | Q(item__category__name__icontains=query) | Q(item__display_id=query))
try:
or_lookup = Q(item__category__reference_number=int(query)) | or_lookup
except: # noqa
pass
qs = qs.filter(or_lookup).distinct()
return qs
@reversion.register @reversion.register
class TrainingItemQualification(models.Model, RevisionMixin): class TrainingItemQualification(models.Model, RevisionMixin):
STARTED = 0 STARTED = 0
@@ -108,12 +179,29 @@ class TrainingItemQualification(models.Model, RevisionMixin):
notes = models.TextField(blank=True) notes = models.TextField(blank=True)
# TODO Maximum depth - some things stop at Complete and you can't be passed out in them # TODO Maximum depth - some things stop at Complete and you can't be passed out in them
objects = TrainingItemQualificationManager()
def clean(self):
errdict = {}
# Validate supervisor can train in this item
if hasattr(self, 'supervisor'): # This will be false if form validation fails
if self.item.category.training_level:
if not self.supervisor.level_qualifications.filter(level=self.item.category.training_level):
errdict['supervisor'] = ('Selected supervising person is missing requisite training level to train in this department')
elif not self.supervisor.is_supervisor:
errdict['supervisor'] = ('Selected supervisor must actually *be* a supervisor...')
# Item requirements only apply to being passed out
if self.depth == TrainingItemQualification.PASSED_OUT and not self.item.user_has_requirements(self.trainee):
errdict['item'] = ('Missing prerequisites')
if errdict != {}: # If there was an error when validation
raise ValidationError(errdict)
def __str__(self): def __str__(self):
return f"{self.get_depth_display()} in {self.item} on {self.date.strftime('%b %d %Y')}" return f"{self.get_depth_display()} in {self.item} on {self.date.strftime('%b %d %Y')}"
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return f"{self.get_depth_display()} in {self.item}" return f"{self.trainee} {self.get_depth_display().lower()} in {self.item}"
@classmethod @classmethod
def get_colour_from_depth(cls, depth): def get_colour_from_depth(cls, depth):
@@ -125,7 +213,7 @@ class TrainingItemQualification(models.Model, RevisionMixin):
return "info" return "info"
def get_absolute_url(self): def get_absolute_url(self):
return reverse('trainee_item_detail', kwargs={'pk': self.trainee.pk}) return reverse('edit_qualification', kwargs={'pk': self.pk})
class Meta: class Meta:
unique_together = ["trainee", "item", "depth"] unique_together = ["trainee", "item", "depth"]
@@ -248,6 +336,8 @@ class TrainingLevelRequirement(models.Model, RevisionMixin):
item = models.ForeignKey('TrainingItem', on_delete=models.CASCADE) item = models.ForeignKey('TrainingItem', on_delete=models.CASCADE)
depth = models.IntegerField(choices=TrainingItemQualification.CHOICES) depth = models.IntegerField(choices=TrainingItemQualification.CHOICES)
reversion_hide = True
def __str__(self): def __str__(self):
depth = TrainingItemQualification.CHOICES[self.depth][1] depth = TrainingItemQualification.CHOICES[self.depth][1]
return f"{depth} in {self.item}" return f"{depth} in {self.item}"
@@ -277,6 +367,13 @@ class TrainingLevelQualification(models.Model, RevisionMixin):
return f"{self.trainee} is qualified in the {self.level}" return f"{self.trainee} is qualified in the {self.level}"
return f"{self.trainee} is qualified as a {self.level}" return f"{self.trainee} is qualified as a {self.level}"
@property
def activity_feed_string(self):
return str(self)
def get_absolute_url(self):
return reverse('trainee_detail', kwargs={'pk': self.trainee.pk})
class Meta: class Meta:
unique_together = ["trainee", "level"] unique_together = ["trainee", "level"]
ordering = ['-confirmed_on'] ordering = ['-confirmed_on']

View File

@@ -32,11 +32,7 @@
{% endif %} {% endif %}
<form id="requirement-form" action="{{ form.action|default:request.path }}" method="post">{% csrf_token %} <form id="requirement-form" action="{{ form.action|default:request.path }}" method="post">{% csrf_token %}
{% render_field form.level|attr:'hidden' value=form.level.initial %} {% render_field form.level|attr:'hidden' value=form.level.initial %}
<div class="form-group form-row"> {% include 'partials/item_field.html' %}
<label for="item_id" class="col-sm-2 col-form-label">Item</label>
<select name="item" id="item_id" class="form-control selectpicker custom-select col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}" required>
</select>
</div>
<div class="form-group form-row"> <div class="form-group form-row">
<label for="depth" class="col-sm-2 col-form-label">Depth</label> <label for="depth" class="col-sm-2 col-form-label">Depth</label>
{% render_field form.depth|add_class:'form-control col-sm'|attr:'required' %} {% render_field form.depth|add_class:'form-control col-sm'|attr:'required' %}

View File

@@ -30,6 +30,9 @@
<a class="dropdown-item" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> Item List</a> <a class="dropdown-item" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> Item List</a>
</div> </div>
</li> </li>
{% if request.user.is_supervisor %}
<li class="nav-item"><a class="nav-link" href="{% url 'session_log' %}"><span class="fas fa-users"></span> Log Session</a></li>
{% endif %}
<li class="nav-item"><a class="nav-link" href="{% url 'training_activity_table' %}"><span class="fas fa-random"></span> Recent Changes</a></li> <li class="nav-item"><a class="nav-link" href="{% url 'training_activity_table' %}"><span class="fas fa-random"></span> Recent Changes</a></li>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -28,25 +28,13 @@
{% include 'form_errors.html' %} {% include 'form_errors.html' %}
{% csrf_token %} {% csrf_token %}
{% render_field form.trainee|attr:'hidden' value=form.trainee.initial %} {% render_field form.trainee|attr:'hidden' value=form.trainee.initial %}
<div class="form-group form-row"> {% include 'partials/item_field.html' %}
<label for="item_id" class="col-sm-2 col-form-label">Item</label>
<select name="item" id="item_id" class="form-control selectpicker custom-select col-sm-4" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}?fields=reference_number,name&filters=active" required>
{% if object.item %}
<option value="{{object.item.pk}}" selected>{{object.item}}</option>
{% endif %}
</select>
</div>
<div class="form-group form-row"> <div class="form-group form-row">
<label for="depth" class="col-sm-2 col-form-label">Depth</label> <label for="depth" class="col-sm-2 col-form-label">Depth</label>
{% render_field form.depth|add_class:'form-control custom-select col-sm-4' %} {% render_field form.depth|add_class:'form-control custom-select col-sm-8' %}
</div> </div>
<div class="form-group form-row"> <div class="form-group form-row">
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label> {% include 'partials/supervisor_field.html' %}
<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 %}
</select>
</div> </div>
<div class="form-group form-row"> <div class="form-group form-row">
<label for="date" class="col-sm-2 col-form-label">Training Date</label> <label for="date" class="col-sm-2 col-form-label">Training Date</label>
@@ -55,7 +43,7 @@
{% render_field form.date|add_class:'form-control'|attr:'type="date"' value=training_date %} {% render_field form.date|add_class:'form-control'|attr:'type="date"' value=training_date %}
{% endwith %} {% endwith %}
</div> </div>
<button class="btn btn-info col-sm-2" onclick="var date = new Date(); $('#id_date').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'))" tabindex="-1" type="button">Today</button> {% button 'today' id='id_date' %}
</div> </div>
<div class="form-group form-row"> <div class="form-group form-row">
<label for="id_notes" class="col-sm-2 col-form-label">Notes</label> <label for="id_notes" class="col-sm-2 col-form-label">Notes</label>

View File

@@ -13,11 +13,19 @@
<div class="card-body"> <div class="card-body">
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
{% for item in category.items.all %} {% for item in category.items.all %}
{% if item.active %} <li class="list-group-item {% if not item.active%}text-warning{%endif%}">{{ item }}
<li class="list-group-item">{{ item }}</li> {% if item.prerequisites.exists %}
{% elif request.user.is_superuser %} <div class="ml-3 font-italic">
<li class="list-group-item text-warning">{{ item }}</li> <p class="text-info mb-0">Passed Out Prerequisites:</p>
{% endif %} <ul>
{% for p in item.prerequisites.all %}
<li>{{p}}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<a href="{% url 'item_qualification' item.pk %}" class="btn btn-info"><span class="fas fa-user"></span> Qualified Users</a>
</li>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,49 @@
{% extends 'base_training.html' %}
{% load paginator from filters %}
{% load colour_from_depth from tags %}
{% load static %}
{% block js %}
<script src="{% static "js/tooltip.js" %}"></script>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{% endblock %}
{% block content %}
<div class="row pt-2">
<div class="col">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Name</th>
<th>Level</th>
<th>Date</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr id="row_item">
<th scope="row" class="align-middle" id="cell_name"><a href="{% url 'trainee_detail' object.trainee.pk %}">{{ object.trainee }} {% if request.user.pk == object.trainee.pk %}<span class="fas fa-user text-success"></span>{%endif%}</a></th>
<td class="table-{% colour_from_depth object.depth %}">{{ object.get_depth_display }}</td>
<td>{{ object.date }}</td>
<td>{{ object.notes }}</td>
</tr>
{% empty %}
<tr class="table-warning">
<td colspan="6" class="text-center">Nothing found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% paginator %}
{% endblock %}

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
<div class="form-group form-row">
<label for="item_id" class="col col-form-label">Item</label>
<select name="item" id="item_id" class="selectpicker col-sm-10 px-0" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}?fields=display_id,description&filters=active" required>
{% if item %}
<option value="{{form.item.value}}" selected>{{item}}</option>
{% endif %}
</select>
</div>

View File

@@ -0,0 +1,6 @@
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
<select name="supervisor" id="supervisor_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required data-noclear="true">
{% if supervisor %}
<option value="{{form.supervisor.value}}" selected>{{ supervisor }}</option>
{% endif %}
</select>

View File

@@ -1,7 +1,8 @@
{% extends 'base_rigs.html' %} {% extends 'base_training.html' %}
{% load static %} {% load static %}
{% load button from filters %} {% load button from filters %}
{% load colour_from_depth from tags %}
{% block css %} {% block css %}
{{ block.super }} {{ block.super }}
@@ -23,27 +24,33 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<form class="form"> <form class="form" method="POST" id="session_form" action="{% url 'session_log' %}">
{% include 'form_errors.html' %}
{% csrf_token %}
<h3>People</h3> <h3>People</h3>
<div class="form-group"> <div class="form-group row" id="supervisor_group">
<label for="selectpicker">Select Supervisor</label> {% include 'partials/supervisor_field.html' %}
<select name="supervisor" id="supervisor_id" class="form-control selectpicker custom-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
</select>
</div> </div>
<div class="form-group"> <div class="form-group row" id="trainees_group">
<label for="selectpicker">Select Attendees</label> <label for="trainees_id" class="col-sm-2">Select Attendees</label>
<select multiple name="attendees" id="attendees_id" class="form-control selectpicker custom-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials"> <select multiple name="trainees" id="trainees_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" data-noclear="true">
</select> </select>
</div> </div>
<h3>Training Items</h3> <h3>Training Items</h3>
<div class="row"> {% for depth in depths %}
{% for depth in depths %} <div class="form-group row" id="{{depth.0}}">
<div class="col"> <label for="selectpicker" class="col-sm-2 text-{% colour_from_depth depth.0 %} py-1">{{ depth.1 }} Items</label>
<h4>{{ depth.1 }}</h4> <select multiple name="items_{{depth.0}}" id="items_{{depth.0}}_id" class="selectpicker col-sm-10 px-0" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}?fields=display_id,description&filters=active" data-noclear="true">
<select multiple name="{{ depth.0 }}" id="{{ depth.0 }}_id" class="form-control selectpicker custom-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}"> </select>
</select> </div>
</div> {% endfor %}
{% endfor %} <h3>Session Information</h3>
<div class="form-group row">
{% include 'partials/form_field.html' with field=form.date col='col-sm-6' %}
{% button 'today' id='id_date' %}
</div>
<div class="form-group">
{% include 'partials/form_field.html' with field=form.notes %}
</div> </div>
<div class="col-sm-12 text-right my-3"> <div class="col-sm-12 text-right my-3">
{% button 'submit' %} {% button 'submit' %}

View File

@@ -5,6 +5,41 @@
{% load linkornone from filters %} {% load linkornone from filters %}
{% load button from filters %} {% load button from filters %}
{% load colour_from_depth from tags %} {% load colour_from_depth from tags %}
{% load static %}
{% block css %}
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
{% endblock %}
{% block preload_js %}
<script src="{% static 'js/selects.js' %}"></script>
{% endblock %}
{% block js %}
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
$('document').ready(function(){
$('#edit').click(function (e) {
e.preventDefault();
var url = $(this).attr("href");
$.ajax({
url: url,
success: function(){
$link = $(this);
// Anti modal inception
if ($link.parents('#modal').length === 0) {
modaltarget = $link.data('target');
modalobject = "";
$('#modal').load(url, function (e) {
$('#modal').modal();
});
}
}
});
});
});
</script>
{% endblock %}
{% block content %} {% block content %}
<p class="text-muted text-right">Search by supervisor name, item name or item ID</p>{% include 'partials/list_search.html' %} <p class="text-muted text-right">Search by supervisor name, item name or item ID</p>{% include 'partials/list_search.html' %}
@@ -19,7 +54,7 @@
<th>Date</th> <th>Date</th>
<th>Supervisor</th> <th>Supervisor</th>
<th>Notes</th> <th>Notes</th>
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %} {% if request.user.is_supervisor %}
<th></th> <th></th>
{% endif %} {% endif %}
</tr> </tr>
@@ -32,8 +67,8 @@
<td>{{ object.date }}</td> <td>{{ object.date }}</td>
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td> <td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
<td>{{ object.notes }}</td> <td>{{ object.notes }}</td>
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %} {% if request.user.is_supervisor %}
<td>{% button 'edit' 'edit_qualification' trainee.pk %}</td> <td>{% button 'edit' 'edit_qualification' object.pk id="edit" %}</td>
{% endif %} {% endif %}
</tr> </tr>
{% empty %} {% empty %}

View File

@@ -40,7 +40,7 @@
<th>Van Driver?</th> <th>Van Driver?</th>
<th>Technician?</th> <th>Technician?</th>
<th>Supervisor?</th> <th>Supervisor?</th>
<th>Qualification Count</th> <th>Competency Assessed Count</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>

View File

@@ -24,7 +24,16 @@ def supervisor(db):
@pytest.fixture @pytest.fixture
def training_item(db): def training_item(db):
training_category = models.TrainingCategory.objects.create(reference_number=1, name="The Basics") 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") training_item = models.TrainingItem.objects.create(category=training_category, reference_number=1, description="How Not To Die")
yield training_item
training_category.delete()
training_item.delete()
@pytest.fixture
def training_item_2(db):
training_category = models.TrainingCategory.objects.create(reference_number=2, name="Sound")
training_item = models.TrainingItem.objects.create(category=training_category, reference_number=1, description="Fundamentals of Audio")
yield training_item yield training_item
training_category.delete() training_category.delete()
training_item.delete() training_item.delete()

View File

@@ -40,3 +40,42 @@ class AddQualification(FormPage):
@property @property
def success(self): def success(self):
return 'add' not in self.driver.current_url return 'add' not in self.driver.current_url
class SessionLog(FormPage):
URL_TEMPLATE = 'training/session_log'
_supervisor_selector = (By.CSS_SELECTOR, 'div#supervisor_group>div.bootstrap-select')
_trainees_selector = (By.CSS_SELECTOR, 'div#trainees_group>div.bootstrap-select')
_training_started_selector = (By.XPATH, '//div[1]/div/div/form/div[4]/div')
_training_complete_selector = (By.XPATH, '//div[1]/div/div/form/div[4]/div')
_competency_assessed_selector = (By.XPATH, '//div[1]/div/div/form/div[5]/div')
form_items = {
'date': (regions.DatePicker, (By.ID, 'id_date')),
'notes': (regions.TextBox, (By.ID, 'id_notes')),
}
@property
def supervisor_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._supervisor_selector))
@property
def trainees_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._trainees_selector))
@property
def training_started_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._training_started_selector))
@property
def training_complete_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._training_complete_selector))
@property
def competency_assessed_selector(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._competency_assessed_selector))
@property
def success(self):
return 'log' not in self.driver.current_url

View File

@@ -12,6 +12,15 @@ from training import models
from training.tests import pages from training.tests import pages
def select_super(page, supervisor):
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()
def test_add_qualification(logged_in_browser, live_server, trainee, supervisor, training_item): 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() page = pages.AddQualification(logged_in_browser.driver, live_server.url, pk=trainee.pk).open()
# assert page.name in str(trainee) # assert page.name in str(trainee)
@@ -24,18 +33,13 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
page.item_selector.toggle() page.item_selector.toggle()
assert page.item_selector.is_open assert page.item_selector.is_open
page.item_selector.search(training_item.name) page.item_selector.search(training_item.description)
time.sleep(2) # Slow down for javascript time.sleep(2) # Slow down for javascript
page.item_selector.set_option(training_item.name, True) # page.item_selector.set_option(training_item.description, True)
assert page.item_selector.options[0].selected assert page.item_selector.options[0].selected
page.item_selector.toggle() page.item_selector.toggle()
page.supervisor_selector.toggle() select_super(page, supervisor)
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() page.submit()
assert page.success assert page.success
@@ -44,3 +48,32 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
assert qualification.date == date assert qualification.date == date
assert qualification.notes == "A note" assert qualification.notes == "A note"
assert qualification.depth == models.TrainingItemQualification.STARTED assert qualification.depth == models.TrainingItemQualification.STARTED
def test_session_log(logged_in_browser, live_server, trainee, supervisor, training_item, training_item_2):
page = pages.SessionLog(logged_in_browser.driver, live_server.url).open()
page.date = date = datetime.date(2001, 1, 10)
page.notes = note = "A general note"
time.sleep(2) # Slow down for javascript
select_super(page, supervisor)
page.trainees_selector.toggle()
assert page.trainees_selector.is_open
page.trainees_selector.search(trainee.first_name)
time.sleep(2) # Slow down for javascript
page.trainees_selector.set_option(trainee.name, True)
# assert page.trainees_selector.options[0].selected
page.trainees_selector.toggle()
page.training_started_selector.toggle()
assert page.training_started_selector.is_open
page.training_started_selector.search(training_item.description[:-2])
time.sleep(2) # Slow down for javascript
# assert page.training_started_selector.options[0].selected
page.training_started_selector.toggle()
page.submit()
assert page.success

View File

@@ -16,7 +16,8 @@ def test_add_qualification(admin_client, trainee, admin_user, training_item):
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': trainee.pk, 'item': training_item.pk}) response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': trainee.pk, 'item': training_item.pk})
assertFormError(response, 'form', 'date', 'Qualification date may not be in the future') assertFormError(response, 'form', 'date', 'Qualification date may not be in the future')
assertFormError(response, 'form', 'supervisor', 'One may not supervise oneself...') assertFormError(response, 'form', 'supervisor', 'One may not supervise oneself...')
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': admin_user.pk, 'item': training_item.pk}) response = admin_client.post(url, {'date': date, 'trainee': admin_user.pk, 'supervisor': trainee.pk, 'item': training_item.pk})
print(response.content)
assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...') assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...')
@@ -28,7 +29,7 @@ def test_add_qualification_reversion(admin_client, trainee, training_item, super
assert response.status_code == 302 assert response.status_code == 302
qual = models.TrainingItemQualification.objects.last() qual = models.TrainingItemQualification.objects.last()
assert qual is not None assert qual is not None
assert training_item.pk == qual.pk assert training_item.pk == qual.item.pk
# Ensure only one revision has been created # Ensure only one revision has been created
assert Revision.objects.count() == 1 assert Revision.objects.count() == 1
response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1}) response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1})

View File

@@ -1,29 +1,34 @@
from django.urls import path from django.urls import path
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from training.decorators import has_perm_or_supervisor from training.decorators import is_supervisor
from PyRIGS.decorators import permission_required_with_403
from training import views, models from training import views, models
from versioning.views import VersionHistory from versioning.views import VersionHistory
urlpatterns = [ urlpatterns = [
path('items/', login_required(views.ItemList.as_view()), name='item_list'), path('items/', login_required(views.ItemList.as_view()), name='item_list'),
path('item/<int:pk>/qualified_users/', login_required(views.ItemQualifications.as_view()), name='item_qualification'),
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'), path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
path('trainee/<int:pk>/', path('trainee/<int:pk>/',
has_perm_or_supervisor('RIGS.view_profile')(views.TraineeDetail.as_view()), permission_required_with_403('RIGS.view_profile')(views.TraineeDetail.as_view()),
name='trainee_detail'), name='trainee_detail'),
path('trainee/<int:pk>/history', has_perm_or_supervisor('RIGS.view_profile')(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think) path('trainee/<int:pk>/history', permission_required_with_403('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/', is_supervisor()(views.AddQualification.as_view()),
name='add_qualification'), name='add_qualification'),
path('trainee/<int:pk>/edit_qualification/', has_perm_or_supervisor('training.change_trainingitemqualification')(views.EditQualification.as_view()), path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),
name='edit_qualification'), name='edit_qualification'),
path('levels/', login_required(views.LevelList.as_view()), name='level_list'), path('levels/', login_required(views.LevelList.as_view()), name='level_list'),
path('level/<int:pk>/', login_required(views.LevelDetail.as_view()), name='level_detail'), path('level/<int:pk>/', login_required(views.LevelDetail.as_view()), name='level_detail'),
path('level/<int:pk>/user/<int:u>/', login_required(views.LevelDetail.as_view()), name='level_detail'), path('level/<int:pk>/user/<int:u>/', login_required(views.LevelDetail.as_view()), name='level_detail'),
path('level/<int:pk>/add_requirement/', login_required(views.AddLevelRequirement.as_view()), name='add_requirement'), path('level/<int:pk>/add_requirement/', is_supervisor()(views.AddLevelRequirement.as_view()), name='add_requirement'),
path('level/remove_requirement/<int:pk>/', login_required(views.RemoveRequirement.as_view()), name='remove_requirement'), path('level/remove_requirement/<int:pk>/', is_supervisor()(views.RemoveRequirement.as_view()), name='remove_requirement'),
path('trainee/<int:pk>/level/<int:level_pk>/confirm', login_required(views.ConfirmLevel.as_view()), name='confirm_level'), path('trainee/<int:pk>/level/<int:level_pk>/confirm', is_supervisor()(views.ConfirmLevel.as_view()), name='confirm_level'),
path('trainee/<int:pk>/item_record', login_required(views.TraineeItemDetail.as_view()), name='trainee_item_detail'), path('trainee/<int:pk>/item_record', login_required(views.TraineeItemDetail.as_view()), name='trainee_item_detail'),
path('session_log', is_supervisor()(views.SessionLog.as_view()), name='session_log'),
] ]

Some files were not shown because too many files have changed in this diff Show More