mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-08 16:09:40 +00:00
Compare commits
76 Commits
3b9848d457
...
combine-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b16cf6333 | ||
|
|
7798f5c368 | ||
|
|
5c2e8b391c | ||
|
|
548bc1df81 | ||
|
|
c1d2bce8fb | ||
|
c71beab278
|
|||
|
259932a548
|
|||
|
7526485837
|
|||
| 39ed5aefb4 | |||
|
e7e760de2e
|
|||
| 9091197639 | |||
|
4f4baa62c1
|
|||
|
b9f8621e1a
|
|||
|
4b1dc37a7f
|
|||
|
|
9273ca35cf | ||
|
|
4a4b7fa30d | ||
| a44a532c7d | |||
| 3a2e5c943b | |||
| 426a9088cc | |||
|
|
1369a2f978 | ||
|
38eafbced3
|
|||
|
900002bf71
|
|||
|
2869c9fcc3
|
|||
|
00eb4e0e27
|
|||
|
23e17b0e34
|
|||
|
bf268a4566
|
|||
|
dedb8d81fe
|
|||
|
7d785f4f1b
|
|||
|
5eb113156b
|
|||
|
ab03ad081a
|
|||
| cd5889f60e | |||
| f18bf3b077 | |||
|
3d36d986a4
|
|||
|
41f5a23ef0
|
|||
|
09f48f740d
|
|||
|
805d77af20
|
|||
|
fabab87e23
|
|||
|
a95779e04e
|
|||
|
24e6ba540d
|
|||
|
14d3522b81
|
|||
|
e4cfaba57d
|
|||
|
d9664422c5
|
|||
|
27bb3f1d8e
|
|||
|
151ac8b3bd
|
|||
|
c2dcd86d5d
|
|||
|
6c14b30c13
|
|||
|
5215af349a
|
|||
|
a5e888fef5
|
|||
|
|
2ae4e4142c | ||
|
8799f822bb
|
|||
|
2dd3d306b4
|
|||
|
042004e1ae
|
|||
|
733ea69cc5
|
|||
|
bbea47e8ec
|
|||
|
c4aafbd7e5
|
|||
|
ccdc13df93
|
|||
|
aa19ceaf18
|
|||
|
05d280172d
|
|||
|
2f51b7b1d3
|
|||
|
8d1edb54ea
|
|||
| 54c90a7be4 | |||
|
3e1e0079d8
|
|||
|
b6952aeb52
|
|||
|
d33a4231fb
|
|||
|
8dea6aeab0
|
|||
|
34c03e379d
|
|||
|
988fb78b45
|
|||
|
eda314c092
|
|||
|
8ef520619a
|
|||
|
95931f86b4
|
|||
|
cc2cb5c4d1
|
|||
|
3ae507b469
|
|||
|
33754eed60
|
|||
|
15ab626593
|
|||
|
7bc47b446c
|
|||
|
83b287a418
|
14
.github/workflows/deploy.yml
vendored
Normal file
14
.github/workflows/deploy.yml
vendored
Normal 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"
|
||||
4
Pipfile
4
Pipfile
@@ -44,7 +44,7 @@ psutil = "~=5.8.0"
|
||||
psycopg2 = "~=2.8.6"
|
||||
Pygments = "~=2.7.4"
|
||||
pyparsing = "~=2.4.7"
|
||||
PyPDF2 = "~=1.26.0"
|
||||
PyPDF2 = "~=1.27.5"
|
||||
PyPOM = "~=2.2.0"
|
||||
python-dateutil = "~=2.8.1"
|
||||
pytoml = "~=0.1.21"
|
||||
@@ -80,6 +80,8 @@ importlib-metadata = "*"
|
||||
django-hcaptcha = "*"
|
||||
"z3c.rml" = "*"
|
||||
pikepdf = "*"
|
||||
django-queryable-properties = "*"
|
||||
django-mass-edit = "*"
|
||||
|
||||
[dev-packages]
|
||||
selenium = "~=3.141.0"
|
||||
|
||||
649
Pipfile.lock
generated
649
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "841781f4c4d3c12a34c0ff8ef3fd58171baf657478d5c339d4f6fc79d5830978"
|
||||
"sha256": "8ba92145c54db8b765e3d517f000ce0c42281586260c032bf89f181bb886d61b"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -37,6 +37,7 @@
|
||||
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
|
||||
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.10"
|
||||
},
|
||||
"attrs": {
|
||||
@@ -44,6 +45,7 @@
|
||||
"sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4",
|
||||
"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"
|
||||
},
|
||||
"backports.tempfile": {
|
||||
@@ -236,28 +238,30 @@
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3",
|
||||
"sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31",
|
||||
"sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac",
|
||||
"sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf",
|
||||
"sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316",
|
||||
"sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca",
|
||||
"sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638",
|
||||
"sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94",
|
||||
"sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12",
|
||||
"sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173",
|
||||
"sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b",
|
||||
"sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a",
|
||||
"sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f",
|
||||
"sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2",
|
||||
"sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9",
|
||||
"sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46",
|
||||
"sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903",
|
||||
"sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3",
|
||||
"sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1",
|
||||
"sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"
|
||||
"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": "==36.0.1"
|
||||
"version": "==37.0.2"
|
||||
},
|
||||
"cssselect": {
|
||||
"hashes": [
|
||||
@@ -300,11 +304,11 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:0a0a37f0b93aef30c4bf3a839c187e1175bcdeb7e177341da0cb7b8194416891",
|
||||
"sha256:69c94abe5d6b1b088bf475e09b7b74403f943e34da107e798465d2045da27e75"
|
||||
"sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6",
|
||||
"sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.11"
|
||||
"version": "==3.2.13"
|
||||
},
|
||||
"django-debug-toolbar": {
|
||||
"hashes": [
|
||||
@@ -345,6 +349,22 @@
|
||||
"index": "pypi",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:715f681f6af029ff3a8d73c7b1460abd8cbc5d5a5001efcb127032e84d9cb963",
|
||||
@@ -404,6 +424,7 @@
|
||||
"sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06",
|
||||
"sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.13.0"
|
||||
},
|
||||
"html5lib": {
|
||||
@@ -411,6 +432,7 @@
|
||||
"sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d",
|
||||
"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"
|
||||
},
|
||||
"icalendar": {
|
||||
@@ -431,12 +453,11 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6",
|
||||
"sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"
|
||||
"sha256:6affcdb3aec542dd98df8211e730bba6c5f2bec8288d47bacacde898f548c9ad",
|
||||
"sha256:9e5e553bbba1843cb4a00823014b907616be46ee503d2b9ba001d214a8da218f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version < '3.10'",
|
||||
"version": "==4.10.1"
|
||||
"version": "==4.11.0"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
@@ -557,6 +578,7 @@
|
||||
"sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958",
|
||||
"sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"packaging": {
|
||||
@@ -564,6 +586,7 @@
|
||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==21.3"
|
||||
},
|
||||
"pep517": {
|
||||
@@ -576,80 +599,82 @@
|
||||
},
|
||||
"pikepdf": {
|
||||
"hashes": [
|
||||
"sha256:11a9c17f6262113d37454638d61c6102eff298309ebfcf4b6c96a3fe3dd57785",
|
||||
"sha256:15cf594b41ba10415181c22cd9e1aab288929bd1b382a534a05f82293b0eac3a",
|
||||
"sha256:19fbfce2b7b7cefd6227f4cd067611d0026dd5e8ef4c42b7f49e4e0016b1cf1a",
|
||||
"sha256:2250f45865a177688e7a225f76db4fad7fb9af46e43fad77081ca41c74307874",
|
||||
"sha256:27a034849fa052e97b262be97efed65f8b1bf681214a754846faeccacd51a61d",
|
||||
"sha256:394d93eafa7688efb4f1c6365ec540fa8768888c041396354209386f72849eb2",
|
||||
"sha256:3dae8f11be19f55d3cf4b3eaf2b257aaf39f8f8bfd7eaab134c60c0f3438ec5c",
|
||||
"sha256:3f0c8421ae131846a33d970a388b77a6d7a02c7496ee92660247803247bad52e",
|
||||
"sha256:40d7d330cfe064b92dca1d8d8f25730ea5cadec9be185f811b96704e02065edd",
|
||||
"sha256:414d7b4bbd7cf3a9553e2254b1631c5ace59a716afa8d461bd76863255738504",
|
||||
"sha256:441157ce3165f77478bad5724fe2aa0428f58728e31152d8d8c0626015c51dd0",
|
||||
"sha256:4daf357f2436758213f164c71fa5aa2c835b4d1fb0b71247b0392198dd480de8",
|
||||
"sha256:4dce15f0f0aec4ed6840383c0897020eae1aa4382dbff5d18b5efbe2a99d09b4",
|
||||
"sha256:5a4cb65ef1f0fcc8c1b3daf2ef000bcb2318cb19961e6a4bcb7404bf37c78f10",
|
||||
"sha256:5bbcf6bc5d1ecc63f6c6c54c631d59340e29c89a300487517d0dad3630afa24c",
|
||||
"sha256:5edd87f1fb31f05b8251480fbc1e05e956589d1be36189234fd40d480dcc32f1",
|
||||
"sha256:6646f05057e88773844c7fefb6b5329e5215a235c692cbf70d5ed66d8c69b7e6",
|
||||
"sha256:792b9f670975fdf6dd47350129d1bf5f27ef1ecafaab6d9aa6be15ac58ceb8ae",
|
||||
"sha256:7dd41620f1dbbd719ca9f5ee4a8219fce35691a01d88274752078c2717bb354b",
|
||||
"sha256:8836e6060534c7245c8d736e93600667b928c767a012a73bb567f56bbe3d985c",
|
||||
"sha256:9bc477d142785ae4663bf200b929bfde4536e428c653553a5e62d32ea29148b1",
|
||||
"sha256:b2da6732a711ff217ee4d84f328c23db0476cfdd5c321b75bad727f481cc670f",
|
||||
"sha256:bcd68a15d06987b519148a09ff1e6840ee71249130bde59ffdf374825dd5826d",
|
||||
"sha256:beef92deb39a04c08a7401eebbe99dbec44b136e0a4f31fe3670159755feea38",
|
||||
"sha256:c714685a0868f277fdf36afeb84a2aa696dab0182eaef4bb91cf3e6b776ba468",
|
||||
"sha256:cd575cf0131683a7b661357bfd777b27c3c6c0d0fb7ef27e627f521122f75536",
|
||||
"sha256:fb3d7fb390192cfb1e287503dbc03229c1c77fe9820cf084546bb63fa997fd87"
|
||||
"sha256:21c7abcdaa33be32513009d84d8c9cf8a4485281c973e8e21dadac461dd1a2f2",
|
||||
"sha256:2a373b8f76db612578e8fa606f8d543f9f8badeaf48449a607982ff545851f7a",
|
||||
"sha256:2d71d93d31557462b5253292f3977cc90c5a6744dd572c5ea122a9487340b390",
|
||||
"sha256:300ddce9294d8b5d362125398066c555567832542114c81ac52e9f98abe30af1",
|
||||
"sha256:3c0bd9d678d1ee769dba3fef90ff8bebe292417bd11ef9663f07f3df9d3c4a73",
|
||||
"sha256:45b3d440448938b7b34e03ca84525b72149f36e5a1381128f9f86d9983b8df20",
|
||||
"sha256:476d567999f6ed2c83c7e62f59fe819e9b1a1671bfbc6694d24b64b88ac3c1d0",
|
||||
"sha256:678ec03d4a37987052f62099662664ecdeafb0bdcd1fbc562bb2d0d285faa134",
|
||||
"sha256:740245bc799adfab15a6819e3b485c7da5680ce69a2fb8becf2e1069f87588fd",
|
||||
"sha256:7800d2a51ce53b0fbb09bef225a6477671b124915b645027ba181e5559efbd03",
|
||||
"sha256:89647dd14994961ad6844b48e3f10158094eef21c9318d817e09166f99af5838",
|
||||
"sha256:961a84364e7ce13b9ab8ef305bdf237b28451005abcdbb084a7808ce6bd726ac",
|
||||
"sha256:973da09bdb82c0df8fc3056f63d92ab67556395354c77469c0ab1e319bb0c792",
|
||||
"sha256:a0c7c54f12098a023c0f70fcdeb2cf25ad782620ecd16c71d63aaee981175f0b",
|
||||
"sha256:a0dfd11f936f541f50c50f0e5dd7f8b425913efd9043337981a104f18aae5ded",
|
||||
"sha256:b40566fb0b9d858e3a6621b8bff0296902fe082d70530cfbab82c4762bffdf4e",
|
||||
"sha256:bd9ef14591559ab1bf2a448acd1467dd8a90c339425f80a65fe8d554ce69fbe9",
|
||||
"sha256:c4b1928c22879d84bdee9ebd163dca8e58f131c78eb5700c232f3dbad65d92b2",
|
||||
"sha256:c862ffd4aa2e17bc2ed50b357b6ecbe442e5a8bff84b8a5070f8e509440a52a4",
|
||||
"sha256:c8bc88ee5af1ad2c68fb8491a79d29d4476dc89ae3ecba471bb00ea8c43cfa35",
|
||||
"sha256:ca3202916f036222071db5ac77ee2767ab4a36f60ed740ff5200f1a8fda717c7",
|
||||
"sha256:cb4c7b8a83948ffa2b02acd6a4587c3a53501ccd18149ec1493bc240e07a3ac0",
|
||||
"sha256:ea223ad320b1f96f28def3f3c7521d8e1616f4f2f0b946fa31c960bf0bf65b97",
|
||||
"sha256:fd66f37547d6faa616978ed30f7e64fcc032eb3a531df8224bc2fb39d282af68",
|
||||
"sha256:fe39256cc251d8563ec8268542b127e7e1482a615a18812d02f93d7970c4b033"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.3.1"
|
||||
"version": "==4.5.0"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
"sha256:03b27b197deb4ee400ed57d8d4e572d2d8d80f825b6634daf6e2c18c3c6ccfa6",
|
||||
"sha256:0b281fcadbb688607ea6ece7649c5d59d4bbd574e90db6cd030e9e85bde9fecc",
|
||||
"sha256:0ebd8b9137630a7bbbff8c4b31e774ff05bbb90f7911d93ea2c9371e41039b52",
|
||||
"sha256:113723312215b25c22df1fdf0e2da7a3b9c357a7d24a93ebbe80bfda4f37a8d4",
|
||||
"sha256:2d16b6196fb7a54aff6b5e3ecd00f7c0bab1b56eee39214b2b223a9d938c50af",
|
||||
"sha256:2fd8053e1f8ff1844419842fd474fc359676b2e2a2b66b11cc59f4fa0a301315",
|
||||
"sha256:31b265496e603985fad54d52d11970383e317d11e18e856971bdbb86af7242a4",
|
||||
"sha256:3586e12d874ce2f1bc875a3ffba98732ebb12e18fb6d97be482bd62b56803281",
|
||||
"sha256:47f5cf60bcb9fbc46011f75c9b45a8b5ad077ca352a78185bd3e7f1d294b98bb",
|
||||
"sha256:490e52e99224858f154975db61c060686df8a6b3f0212a678e5d2e2ce24675c9",
|
||||
"sha256:500d397ddf4bbf2ca42e198399ac13e7841956c72645513e8ddf243b31ad2128",
|
||||
"sha256:52abae4c96b5da630a8b4247de5428f593465291e5b239f3f843a911a3cf0105",
|
||||
"sha256:6579f9ba84a3d4f1807c4aab4be06f373017fc65fff43498885ac50a9b47a553",
|
||||
"sha256:68e06f8b2248f6dc8b899c3e7ecf02c9f413aab622f4d6190df53a78b93d97a5",
|
||||
"sha256:6c5439bfb35a89cac50e81c751317faea647b9a3ec11c039900cd6915831064d",
|
||||
"sha256:72c3110228944019e5f27232296c5923398496b28be42535e3b2dc7297b6e8b6",
|
||||
"sha256:72f649d93d4cc4d8cf79c91ebc25137c358718ad75f99e99e043325ea7d56100",
|
||||
"sha256:7aaf07085c756f6cb1c692ee0d5a86c531703b6e8c9cae581b31b562c16b98ce",
|
||||
"sha256:80fe92813d208ce8aa7d76da878bdc84b90809f79ccbad2a288e9bcbeac1d9bd",
|
||||
"sha256:95545137fc56ce8c10de646074d242001a112a92de169986abd8c88c27566a05",
|
||||
"sha256:97b6d21771da41497b81652d44191489296555b761684f82b7b544c49989110f",
|
||||
"sha256:98cb63ca63cb61f594511c06218ab4394bf80388b3d66cd61d0b1f63ee0ea69f",
|
||||
"sha256:9f3b4522148586d35e78313db4db0df4b759ddd7649ef70002b6c3767d0fdeb7",
|
||||
"sha256:a09a9d4ec2b7887f7a088bbaacfd5c07160e746e3d47ec5e8050ae3b2a229e9f",
|
||||
"sha256:b5050d681bcf5c9f2570b93bee5d3ec8ae4cf23158812f91ed57f7126df91762",
|
||||
"sha256:bb47a548cea95b86494a26c89d153fd31122ed65255db5dcbc421a2d28eb3379",
|
||||
"sha256:bc462d24500ba707e9cbdef436c16e5c8cbf29908278af053008d9f689f56dee",
|
||||
"sha256:c2067b3bb0781f14059b112c9da5a91c80a600a97915b4f48b37f197895dd925",
|
||||
"sha256:d154ed971a4cc04b93a6d5b47f37948d1f621f25de3e8fa0c26b2d44f24e3e8f",
|
||||
"sha256:d5dcea1387331c905405b09cdbfb34611050cc52c865d71f2362f354faee1e9f",
|
||||
"sha256:ee6e2963e92762923956fe5d3479b1fdc3b76c83f290aad131a2f98c3df0593e",
|
||||
"sha256:fd0e5062f11cb3e730450a7d9f323f4051b532781026395c4323b8ad055523c4"
|
||||
"sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97",
|
||||
"sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049",
|
||||
"sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c",
|
||||
"sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae",
|
||||
"sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28",
|
||||
"sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030",
|
||||
"sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56",
|
||||
"sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976",
|
||||
"sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e",
|
||||
"sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e",
|
||||
"sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f",
|
||||
"sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b",
|
||||
"sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a",
|
||||
"sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e",
|
||||
"sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa",
|
||||
"sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7",
|
||||
"sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00",
|
||||
"sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838",
|
||||
"sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360",
|
||||
"sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b",
|
||||
"sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a",
|
||||
"sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd",
|
||||
"sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4",
|
||||
"sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70",
|
||||
"sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204",
|
||||
"sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc",
|
||||
"sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b",
|
||||
"sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669",
|
||||
"sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7",
|
||||
"sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e",
|
||||
"sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c",
|
||||
"sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092",
|
||||
"sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c",
|
||||
"sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5",
|
||||
"sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==9.0.0"
|
||||
"version": "==9.0.1"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
|
||||
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"premailer": {
|
||||
@@ -739,10 +764,10 @@
|
||||
},
|
||||
"pyopenssl": {
|
||||
"hashes": [
|
||||
"sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3",
|
||||
"sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6"
|
||||
"sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf",
|
||||
"sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0"
|
||||
],
|
||||
"version": "==21.0.0"
|
||||
"version": "==22.0.0"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
@@ -754,10 +779,11 @@
|
||||
},
|
||||
"pypdf2": {
|
||||
"hashes": [
|
||||
"sha256:e28f902f2f0a1603ea95ebe21dff311ef09be3d0f0ef29a3e44a932729564385"
|
||||
"sha256:7c18c1d48e56547b1c33f772dc15d6adbd1f4020b62e64bb4a0bc0ee2ab94511",
|
||||
"sha256:9c81fc06be50fbf2e6199e8c2eac05f3eaafae4b3905ecca41200f5b02ac43d7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.26.0"
|
||||
"version": "==1.27.5"
|
||||
},
|
||||
"pypom": {
|
||||
"hashes": [
|
||||
@@ -767,6 +793,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.2.3"
|
||||
},
|
||||
"pysocks": {
|
||||
"hashes": [
|
||||
"sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
|
||||
"sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
|
||||
"sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
|
||||
],
|
||||
"version": "==1.7.1"
|
||||
},
|
||||
"python-barcode": {
|
||||
"hashes": [
|
||||
"sha256:daa32fb999a843812fbb1c75ff909638811af7c465f0a991e9e41d26d2a44a24",
|
||||
@@ -801,53 +835,45 @@
|
||||
},
|
||||
"reportlab": {
|
||||
"hashes": [
|
||||
"sha256:0430cfe397415759839ef89abee6db82e8a8f9bb5831a3c93e7763915c755345",
|
||||
"sha256:13072e33e8cbac6fd6e776fecabdefafb0261886b2ab7cb3b874a9384f1b0ffe",
|
||||
"sha256:1767106d03320e76a708d2c40488fe1785580a0d7abac7715e01a3cc910c1179",
|
||||
"sha256:17f35a856bbf46989d557d4016822bcdd3ada88d3afb567de03a4b29676aa52e",
|
||||
"sha256:19414f4357287a7573a60bcb76a092c9ea82bf09f01d04b3afb5c1bd3c660df2",
|
||||
"sha256:1d8d9674eb6ba1b6c3d6a8e3d5d4e4231b3576db653d1b1fdac2538afee54c7a",
|
||||
"sha256:23236dc70598b688e979444c4840c5cec88a2a12fe81ba6f8cc807120a2cad33",
|
||||
"sha256:28c339d25eab804a8bd004dfaa5a80c7568178561741f4ce6e69dae05d38041f",
|
||||
"sha256:2c93a551b60c7fd3b17942772847f7c4ee2f08ae74c87ef8f325fe8083d2aa6e",
|
||||
"sha256:2e80045f36dd4b9b63b19fc073149f70857fe8590027ab3658db80ac6235ecd0",
|
||||
"sha256:38aa912301d93e2267861d820cb3f6eebed8deb58d0df429421578b9ba033eee",
|
||||
"sha256:47587ce01cf9ac25f6d187116a9f9cef710dc58ccea001024d950c4f5a504643",
|
||||
"sha256:587b3d8ce0a065a00975516013aebb062e6161fba3cf399b22f270e4d9a3db1e",
|
||||
"sha256:5a650284cc09caa32b5845c055bf035cb76949b87d57e9eed56d98f863613417",
|
||||
"sha256:5e113c630b6109efe0285230706c8423bff1b82c2e2824e441401a467a1215b7",
|
||||
"sha256:68e339411cc9329ff50982a7c1d55eabd53ac9be24d4442088af58328bae54d3",
|
||||
"sha256:6ae1fb03faf4b6710e2c081d5208416a5d557e0cc00ff24fc124dd42a7158114",
|
||||
"sha256:6f363e09aacaa7aaff232197fddb667d899822aa57d10091aea4fbb1f56b7fa7",
|
||||
"sha256:70841d7eb4aa2f8ad4afacce07711481a0dcd9d01679da5627173443131a33a2",
|
||||
"sha256:71d91002878c4d2a17a6bd7208c59373e6148977fe674bb79eec3eb9e63aa20f",
|
||||
"sha256:7a09e5bf9c8e02c373e5e558cc5c2cfbc5d3c68560a406c6d16254363cfa989e",
|
||||
"sha256:85095ef9f3697859064cb1b22f19659bf4ba25e7dadb9c6be65f322cd68ba88f",
|
||||
"sha256:8dafdcdde7243f0864d6d11dd9bfffbd1e6bce6c3e668fe992f56ae48377c822",
|
||||
"sha256:9a822486a98fe002bbe248fdf3f126739c1ad29032b54b71a3f67b6364a77677",
|
||||
"sha256:aa57dc0818e066fdced9457b9e6c6fb269d63e2d96902001c7dbe010bce6ebcc",
|
||||
"sha256:b0836c6cdee4b88e2366e0ff152c1327578149e09850b7cab6016444c5b3eb26",
|
||||
"sha256:b2988ffc33032096e808e7a4a36f5b453fcc9587873c85c1b44bc6846bbbd09c",
|
||||
"sha256:bd38d58895b359ef429df3c97dc00c3fef0ab57f45556de416ba9b7d7fc71ae2",
|
||||
"sha256:be87dca9253efd3cd0f351b785530c02e67664e284e3c4a97cdd0c7dd806d39a",
|
||||
"sha256:c21bdb11d7fccea28bf08eac13d9d031836e335c5e0620eae1d4336f193e9a03",
|
||||
"sha256:c43f847f2598b5c2fc9b63871d7da641c0b90e384d8da8018d4d7173a0b82cd4",
|
||||
"sha256:c780cc5208c67b25bdddd08480f874614cd0ec0bed39e1a848448543f2093945",
|
||||
"sha256:c9bcf696bc8935ff90ecb50c7644e2af01f63a444d4b4bd39d41d2abdd7bb224",
|
||||
"sha256:cb48b71088f5c9eff5715dde0bd4d5372d4713ffa92247acf0f04fd17ab2078d",
|
||||
"sha256:d48f638893b3eb4c9b2afeec2de4f95a4b57fb8c398e3d7f9a7fb4b4d9546820",
|
||||
"sha256:d8fe27ad312671c9347cf5997f7c1017833fac17233f33296281ba9fa0de189a",
|
||||
"sha256:d98b759661070f5588b30152d0caaf16ac387f60372f8fa2568c9ad4014cd7f3",
|
||||
"sha256:e2022ad36409e7616ed6311f7ab113f236cac66ba0d22be4f53bf7e77654b143",
|
||||
"sha256:e45159f4d19304f5e79be13283fe53bdd006c4fd4d93ff3cb6ac082ca017c418",
|
||||
"sha256:eb3ef5394b4b2c904ab467dbbe1efcfbe046e1395c2d3064420ccef89806570e",
|
||||
"sha256:f326b04a3fb3c7c58b799bd23b60790b181893f052fe5a8011c9cd9984e24a43",
|
||||
"sha256:f401ed014ea861dea2ae621f7810fb15b3bc021e6487dee97b32f175bbf1b7eb",
|
||||
"sha256:f4d4eb3a949ccb0782e4d6560fcd5ee6f34636d1ee24f1d2a2b1f530af89481a",
|
||||
"sha256:fdc3dc1242be557f6a8bb9e21751296cc721f60b8e2b684690049e656d798520"
|
||||
"sha256:22c28e593e2c37110f79df9bb31ba7782dc8c0002f33d8070c6d18e1c7380bfc",
|
||||
"sha256:238f1088b1ce94d25790774546fc52e3efd909eafe0c56f71d286996dd2d2db0",
|
||||
"sha256:2668687baf0a6c64f90193eca74dfa69bf172bf38e436c7be91e0b13867132ec",
|
||||
"sha256:2adb9c53c86b30290b407a24b88cf07b09c3b325866b5125b4dca4aa7996021e",
|
||||
"sha256:2ca0c987433bf63d765a9dcc9cb54695e617725ee81058af615f8d42fc29c0d8",
|
||||
"sha256:2cf111835bd4b9afbdf8568c4031e2727cdc64a914bbd68e60aa190672f70d34",
|
||||
"sha256:30d75931893f6c5beb14a93b0a3701cf14a6353c0b48acefa6b4c2391464b861",
|
||||
"sha256:44c62615504f669a92a62431e847a11c281072ec3a4820a8880dab7338cad53c",
|
||||
"sha256:473680fb899aed897963ddbf4536b377e40c7ea6fba83337e7f544e3040df956",
|
||||
"sha256:502ae45775ddf6ed10f23253f8a7768b52b9517ac590babcb92aab0336a2a13a",
|
||||
"sha256:5a681047247a6d896ed7ec18b95054c9c139c0269417beb066985244b8d18f75",
|
||||
"sha256:5d62c8341a426984d488fadab2e2b35c4e3e4f5c6ceb2e6b57d7fc41cb7ba992",
|
||||
"sha256:6910eb0152a72be5ebe8984472f9b2eeb1a5dc3db20a591cbcf179b14c2757a8",
|
||||
"sha256:6e9f42099141bb35013297b8de8b7329946d94e881cbd72c3d76f44d5a9df705",
|
||||
"sha256:7ac03370a672c9df9e691da4870f5db79d6227f37a6faf7d17a822890d42de60",
|
||||
"sha256:8a49fec7ea0c410dc84c88ac8c965605a3e6d50a9b81afb9539175168c7deaf7",
|
||||
"sha256:97b5ab874e8d74f3dbe3b48a531df7df269acb35c3e5eed9d41b3579bef9ad77",
|
||||
"sha256:9db71af717229dad72fe5f4dfb587eb952a07f7c1bcd83df402b676c78a334f5",
|
||||
"sha256:a089addc73b770d159615fc4c90cd06226b0c071d30c63e8addf57b9533049ee",
|
||||
"sha256:a09acda69357664190a02f239abb01505d519a2563ba89d57d6fb55ca14ade72",
|
||||
"sha256:ae252b718fb6de4da766d2b4b3402592923e327641dfa0a1b3cfecaa8a95229f",
|
||||
"sha256:b109d8594a5140f8c0e93c0d091e16c6274267027077cddbc590d4bff7acb35c",
|
||||
"sha256:b1d4940ff5f573f54855507c2d2ddfeb9a034ad3f040fa5168cf235717531b78",
|
||||
"sha256:b44a59e75a2c20912e21960df45c0644ded4538300becbb1df5b4cceea2afa11",
|
||||
"sha256:b84c0c3ad09eb9183fb2e54e44da92d84436d9f3a3263d1456e463c723c54906",
|
||||
"sha256:d05603fcf2acee5d01eb814d36b212aafbd82cafb9ae861dff41daaf893f95f1",
|
||||
"sha256:d42a442f4593ab5e196debc32aff0c36fcbf4031f068e1c9435d4137f47d7990",
|
||||
"sha256:dd1cdb62dc123f5859ca514eb639f70660bdc818c95fb0ee2370a175a0e20ce4",
|
||||
"sha256:e6d3affa0e484fb55e1061bbdf778797c68a648127f91102b1f0a6173ecb590e",
|
||||
"sha256:e7ca3699612efc278c666193aa340937066d8045cde247c4b409c8f416e0811e",
|
||||
"sha256:e80ed55cbbaf905635a2673d439495e1b1925b8379ea56aa2fc859a00e41af9f",
|
||||
"sha256:edab6b0fc5984051b9b74d33579b7e3d228b70a5801904aa645828a95efb8486",
|
||||
"sha256:ef659caf2f2824ab0bdf9e98a3886272232bcb1c756be4eb4f5c3c60a9519092",
|
||||
"sha256:f00e0218854e168bd8d5379d07f0e138285c34b5fe3878c8d5d4f691e280d95e",
|
||||
"sha256:f2bc48fc45f13d9ccc123462ab3bfd18a78e4bd58d027f9d4a226110c78adc3c",
|
||||
"sha256:f2be927d8717c5947e7968f089492c088a4103bfe6287ee01a001e0b9a84545b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.6.5"
|
||||
"version": "==3.6.6"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
@@ -866,17 +892,26 @@
|
||||
},
|
||||
"selenium": {
|
||||
"hashes": [
|
||||
"sha256:27e7b64df961d609f3d57237caa0df123abbbe22d038f2ec9e332fb90ec1a939"
|
||||
"sha256:866b6dd6c459210662bff922ee7c33162d21920fbf6811e8e5a52be3866a687f"
|
||||
],
|
||||
"version": "==4.1.0"
|
||||
"markers": "python_version ~= '3.7'",
|
||||
"version": "==4.1.5"
|
||||
},
|
||||
"sentry-sdk": {
|
||||
"hashes": [
|
||||
"sha256:141da032f0fa4c56f9af6b361fda57360af1789576285bd1944561f9c274f9c0",
|
||||
"sha256:9aeff2a47f4038460296b920bf4d269284e8454e1c67547ee002ccafd9c2442b"
|
||||
"sha256:3817274fba2498c8ebf6b896ee98ac916c5598706340573268c07bf2bb30d831",
|
||||
"sha256:98fd155fa5d5fec1dbabed32a1a4ae2705f1edaa5dae4e7f7b62a384ba30e759"
|
||||
],
|
||||
"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": {
|
||||
"hashes": [
|
||||
@@ -958,6 +993,7 @@
|
||||
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
|
||||
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"sortedcontainers": {
|
||||
@@ -973,7 +1009,6 @@
|
||||
"sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.0'",
|
||||
"version": "==2.3.1"
|
||||
},
|
||||
"sqlparse": {
|
||||
@@ -1011,6 +1046,7 @@
|
||||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"tornado": {
|
||||
@@ -1062,16 +1098,18 @@
|
||||
},
|
||||
"trio": {
|
||||
"hashes": [
|
||||
"sha256:895e318e5ec5e8cea9f60b473b6edb95b215e82d99556a03eb2d20c5e027efe1",
|
||||
"sha256:c27c231e66336183c484fbfe080fa6cc954149366c15dc21db8b7290081ec7b8"
|
||||
"sha256:670a52d3115d0e879e1ac838a4eb999af32f858163e3a704fe4839de2a676070",
|
||||
"sha256:fb2d48e4eab0dfb786a472cd514aaadc71e3445b203bc300bad93daa75d77c1a"
|
||||
],
|
||||
"version": "==0.19.0"
|
||||
"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": {
|
||||
@@ -1099,10 +1137,11 @@
|
||||
},
|
||||
"wsproto": {
|
||||
"hashes": [
|
||||
"sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38",
|
||||
"sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f"
|
||||
"sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b",
|
||||
"sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"yolk": {
|
||||
"hashes": [
|
||||
@@ -1318,11 +1357,20 @@
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"async-generator": {
|
||||
"hashes": [
|
||||
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
|
||||
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.10"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4",
|
||||
"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"
|
||||
},
|
||||
"certifi": {
|
||||
@@ -1333,65 +1381,115 @@
|
||||
"index": "pypi",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd",
|
||||
"sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"
|
||||
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
|
||||
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.10"
|
||||
"version": "==2.0.12"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0",
|
||||
"sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd",
|
||||
"sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884",
|
||||
"sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48",
|
||||
"sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76",
|
||||
"sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0",
|
||||
"sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64",
|
||||
"sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685",
|
||||
"sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47",
|
||||
"sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d",
|
||||
"sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840",
|
||||
"sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f",
|
||||
"sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971",
|
||||
"sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c",
|
||||
"sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a",
|
||||
"sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de",
|
||||
"sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17",
|
||||
"sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4",
|
||||
"sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521",
|
||||
"sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57",
|
||||
"sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b",
|
||||
"sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282",
|
||||
"sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644",
|
||||
"sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475",
|
||||
"sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d",
|
||||
"sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da",
|
||||
"sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953",
|
||||
"sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2",
|
||||
"sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e",
|
||||
"sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c",
|
||||
"sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc",
|
||||
"sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64",
|
||||
"sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74",
|
||||
"sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617",
|
||||
"sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3",
|
||||
"sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d",
|
||||
"sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa",
|
||||
"sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739",
|
||||
"sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8",
|
||||
"sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8",
|
||||
"sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781",
|
||||
"sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58",
|
||||
"sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9",
|
||||
"sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c",
|
||||
"sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd",
|
||||
"sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e",
|
||||
"sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"
|
||||
"sha256:06f54765cdbce99901871d50fe9f41d58213f18e98b170a30ca34f47de7dd5e8",
|
||||
"sha256:114944e6061b68a801c5da5427b9173a0dd9d32cd5fcc18a13de90352843737d",
|
||||
"sha256:1414e8b124611bf4df8d77215bd32cba6e3425da8ce9c1f1046149615e3a9a31",
|
||||
"sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879",
|
||||
"sha256:2ab88a01cd180b5640ccc9c47232e31924d5f9967ab7edd7e5c91c68eee47a69",
|
||||
"sha256:338c417613f15596af9eb7a39353b60abec9d8ce1080aedba5ecee6a5d85f8d3",
|
||||
"sha256:3401b0d2ed9f726fadbfa35102e00d1b3547b73772a1de5508ef3bdbcb36afe7",
|
||||
"sha256:462105283de203df8de58a68c1bb4ba2a8a164097c2379f664fa81d6baf94b81",
|
||||
"sha256:4cd696aa712e6cd16898d63cf66139dc70d998f8121ab558f0e1936396dbc579",
|
||||
"sha256:4d06380e777dd6b35ee936f333d55b53dc4a8271036ff884c909cf6e94be8b6c",
|
||||
"sha256:61f4fbf3633cb0713437291b8848634ea97f89c7e849c2be17a665611e433f53",
|
||||
"sha256:6d4a6f30f611e657495cc81a07ff7aa8cd949144e7667c5d3e680d73ba7a70e4",
|
||||
"sha256:6f5fee77ec3384b934797f1873758f796dfb4f167e1296dc00f8b2e023ce6ee9",
|
||||
"sha256:75b5dbffc334e0beb4f6c503fb95e6d422770fd2d1b40a64898ea26d6c02742d",
|
||||
"sha256:7835f76a081787f0ca62a53504361b3869840a1620049b56d803a8cb3a9eeea3",
|
||||
"sha256:79bf405432428e989cad7b8bc60581963238f7645ae8a404f5dce90236cc0293",
|
||||
"sha256:8329635c0781927a2c6ae068461e19674c564e05b86736ab8eb29c420ee7dc20",
|
||||
"sha256:8586b177b4407f988731eb7f41967415b2197f35e2a6ee1a9b9b561f6323c8e9",
|
||||
"sha256:892e7fe32191960da559a14536768a62e83e87bbb867e1b9c643e7e0fbce2579",
|
||||
"sha256:91502bf27cbd5c83c95cfea291ef387469f2387508645602e1ca0fd8a4ba7548",
|
||||
"sha256:93b16b08f94c92cab88073ffd185070cdcb29f1b98df8b28e6649145b7f2c90d",
|
||||
"sha256:9c9441d57b0963cf8340268ad62fc83de61f1613034b79c2b1053046af0c5284",
|
||||
"sha256:ad8f9068f5972a46d50fe5f32c09d6ee11da69c560fcb1b4c3baea246ca4109b",
|
||||
"sha256:afb03f981fadb5aed1ac6e3dd34f0488e1a0875623d557b6fad09b97a942b38a",
|
||||
"sha256:b5ba058610e8289a07db2a57bce45a1793ec0d3d11db28c047aae2aa1a832572",
|
||||
"sha256:baa8be8aba3dd1e976e68677be68a960a633a6d44c325757aefaa4d66175050f",
|
||||
"sha256:c06455121a089252b5943ea682187a4e0a5cf0a3fb980eb8e7ce394b144430a9",
|
||||
"sha256:c1a9942e282cc9d3ed522cd3e3cab081149b27ea3bda72d6f61f84eaf88c1a63",
|
||||
"sha256:c488db059848702aff30aa1d90ef87928d4e72e4f00717343800546fdbff0a94",
|
||||
"sha256:cb5311d6ccbd22578c80028c5e292a7ab9adb91bd62c1982087fad75abe2e63d",
|
||||
"sha256:cbe91bc84be4e5ef0b1480d15c7b18e29c73bdfa33e07d3725da7d18e1b0aff2",
|
||||
"sha256:cc692c9ee18f0dd3214843779ba6b275ee4bb9b9a5745ba64265bce911aefd1a",
|
||||
"sha256:cc972d829ad5ef4d4c5fcabd2bbe2add84ce8236f64ba1c0c72185da3a273130",
|
||||
"sha256:ceb6534fcdfb5c503affb6b1130db7b5bfc8a0f77fa34880146f7a5c117987d0",
|
||||
"sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe",
|
||||
"sha256:db094a6a4ae6329ed322a8973f83630b12715654c197dd392410400a5bfa1a73",
|
||||
"sha256:df32ee0f4935a101e4b9a5f07b617d884a531ed5666671ff6ac66d2e8e8246d8",
|
||||
"sha256:e5af1feee71099ae2e3b086ec04f57f9950e1be9ecf6c420696fea7977b84738",
|
||||
"sha256:e814a4a5a1d95223b08cdb0f4f57029e8eab22ffdbae2f97107aeef28554517e",
|
||||
"sha256:f8cabc5fd0091976ab7b020f5708335033e422de25e20ddf9416bdce2b7e07d8",
|
||||
"sha256:fbc86ae8cc129c801e7baaafe3addf3c8d49c9c1597c44bdf2d78139707c3c62"
|
||||
],
|
||||
"version": "==6.2"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==6.3.3"
|
||||
},
|
||||
"coveralls": {
|
||||
"hashes": [
|
||||
@@ -1401,6 +1499,33 @@
|
||||
"index": "pypi",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:4206c85ffba0301f83aecc38e5b01b1b9a4b45a545d9456a827e3fabea18d952",
|
||||
@@ -1420,8 +1545,17 @@
|
||||
"sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5",
|
||||
"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"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06",
|
||||
"sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.13.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
@@ -1437,11 +1571,20 @@
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"outcome": {
|
||||
"hashes": [
|
||||
"sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958",
|
||||
"sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==21.3"
|
||||
},
|
||||
"pluggy": {
|
||||
@@ -1449,6 +1592,7 @@
|
||||
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
|
||||
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"psutil": {
|
||||
@@ -1490,6 +1634,7 @@
|
||||
"sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
|
||||
"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"
|
||||
},
|
||||
"pycodestyle": {
|
||||
@@ -1500,6 +1645,20 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
|
||||
"sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
|
||||
],
|
||||
"version": "==2.21"
|
||||
},
|
||||
"pyopenssl": {
|
||||
"hashes": [
|
||||
"sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf",
|
||||
"sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0"
|
||||
],
|
||||
"version": "==22.0.0"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
@@ -1516,13 +1675,21 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.2.3"
|
||||
},
|
||||
"pysocks": {
|
||||
"hashes": [
|
||||
"sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
|
||||
"sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
|
||||
"sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
|
||||
],
|
||||
"version": "==1.7.1"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
|
||||
"sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
|
||||
"sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db",
|
||||
"sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.2.5"
|
||||
"version": "==7.0.1"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
@@ -1545,6 +1712,7 @@
|
||||
"sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e",
|
||||
"sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"pytest-reverse": {
|
||||
@@ -1563,6 +1731,9 @@
|
||||
"version": "==3.3.1"
|
||||
},
|
||||
"pytest-xdist": {
|
||||
"extras": [
|
||||
"psutil"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf",
|
||||
"sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"
|
||||
@@ -1580,9 +1751,18 @@
|
||||
},
|
||||
"selenium": {
|
||||
"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": {
|
||||
"hashes": [
|
||||
@@ -1592,6 +1772,21 @@
|
||||
"index": "pypi",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:0fef9b632a0e220fe9cad7df1e2d5119932b6d2baf7843b80334248d6fda62d9",
|
||||
@@ -1599,19 +1794,29 @@
|
||||
],
|
||||
"version": "==0.17.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||
],
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"tomli": {
|
||||
"hashes": [
|
||||
"sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224",
|
||||
"sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"
|
||||
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
"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": {
|
||||
"hashes": [
|
||||
@@ -1621,6 +1826,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.26.8"
|
||||
},
|
||||
"wsproto": {
|
||||
"hashes": [
|
||||
"sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b",
|
||||
"sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"zope.component": {
|
||||
"hashes": [
|
||||
"sha256:607628e4c84f7887a69a958542b5c304663e726b73aba0882e3a3f059bff14f3",
|
||||
|
||||
@@ -9,9 +9,8 @@ from RIGS import models
|
||||
|
||||
def get_oembed(login_url, request, oembed_view, kwargs):
|
||||
context = {}
|
||||
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'],
|
||||
reverse(oembed_view, kwargs=kwargs))
|
||||
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
||||
context['oembed_url'] = f"{request.scheme}://{request.META['HTTP_HOST']}{reverse(oembed_view, kwargs=kwargs)}"
|
||||
context['login_url'] = f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}"
|
||||
resp = render(request, 'login_redirect.html', context=context)
|
||||
return resp
|
||||
|
||||
@@ -25,7 +24,7 @@ def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
|
||||
if oembed_view is not None:
|
||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||
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.__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:
|
||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||
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:
|
||||
resp = render(request, '403.html')
|
||||
resp.status_code = 403
|
||||
|
||||
@@ -68,6 +68,7 @@ INSTALLED_APPS = (
|
||||
'reversion',
|
||||
'widget_tweaks',
|
||||
'hcaptcha',
|
||||
'massadmin',
|
||||
)
|
||||
|
||||
MIDDLEWARE = (
|
||||
|
||||
@@ -10,6 +10,7 @@ from pytest_django.asserts import assertTemplateUsed, assertInHTML
|
||||
from PyRIGS import urls
|
||||
from RIGS.models import Event, Profile
|
||||
from assets.models import Asset
|
||||
from training.tests.test_unit import get_response
|
||||
from django.db import connection
|
||||
from django.template.defaultfilters import striptags
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
@@ -135,3 +136,11 @@ def test_keyholder_access(client):
|
||||
assertContains(response, 'View Revision History')
|
||||
client.logout()
|
||||
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)
|
||||
|
||||
@@ -23,10 +23,12 @@ urlpatterns = [
|
||||
name="api_secure"),
|
||||
|
||||
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('', include('users.urls')),
|
||||
|
||||
path('admin/', include('massadmin.urls')),
|
||||
path('admin/', admin.site.urls),
|
||||
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
|
||||
]
|
||||
|
||||
130
PyRIGS/views.py
130
PyRIGS/views.py
@@ -1,7 +1,18 @@
|
||||
import datetime
|
||||
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 import messages
|
||||
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.views import generic
|
||||
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 assets import models as asset_models
|
||||
@@ -22,6 +35,13 @@ def is_ajax(request):
|
||||
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
|
||||
template_name = 'index.html'
|
||||
|
||||
@@ -120,7 +140,7 @@ class SecureAPIRequest(generic.View):
|
||||
'text': o.name,
|
||||
}
|
||||
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:
|
||||
pass
|
||||
results.append(data)
|
||||
@@ -182,20 +202,7 @@ class GenericListView(generic.ListView):
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
q = 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)
|
||||
object_list = self.model.objects.search(query=self.request.GET.get('q', ""))
|
||||
|
||||
orderBy = self.request.GET.get('orderBy', "name")
|
||||
if orderBy != "":
|
||||
@@ -236,6 +243,53 @@ class GenericCreateView(generic.CreateView):
|
||||
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):
|
||||
template_name = 'search_help.html'
|
||||
|
||||
@@ -265,3 +319,47 @@ class OEmbedView(generic.View):
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
193
RIGS/admin.py
193
RIGS/admin.py
@@ -8,6 +8,7 @@ from django.db.models import Count
|
||||
from django.forms import ModelForm
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db import IntegrityError
|
||||
from reversion import revisions as reversion
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
@@ -21,17 +22,139 @@ admin.site.register(models.EventItem, VersionAdmin)
|
||||
admin.site.register(models.Invoice, VersionAdmin)
|
||||
|
||||
|
||||
def approve_user(modeladmin, request, queryset):
|
||||
queryset.update(is_approved=True)
|
||||
@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
|
||||
|
||||
|
||||
approve_user.short_description = "Approve selected users"
|
||||
class AssociateAdmin(VersionAdmin):
|
||||
search_fields = ['id', 'name']
|
||||
list_display_links = ['id', 'name']
|
||||
actions = ['merge']
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).annotate(event_count=Count('event'))
|
||||
|
||||
def number_of_events(self, obj):
|
||||
return obj.latest_events.count()
|
||||
|
||||
number_of_events.admin_order_field = 'event_count'
|
||||
|
||||
def merge(self, request, queryset):
|
||||
if request.POST.get('post'): # Has the user confirmed which is the master record?
|
||||
try:
|
||||
master_object_pk = request.POST.get('master')
|
||||
master_object = queryset.get(pk=master_object_pk)
|
||||
except ObjectDoesNotExist:
|
||||
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
|
||||
return
|
||||
|
||||
primary_object, deleted_objects, deleted_objects_count = merge_model_instances(master_object, queryset.exclude(pk=master_object_pk).all())
|
||||
reversion.set_comment('Merging Objects')
|
||||
self.message_user(request, f"Objects successfully merged. {deleted_objects_count} old objects deleted.")
|
||||
else: # Present the confirmation screen
|
||||
class TempForm(ModelForm):
|
||||
class Meta:
|
||||
model = queryset.model
|
||||
fields = self.merge_fields
|
||||
|
||||
forms = []
|
||||
for obj in queryset:
|
||||
forms.append(TempForm(instance=obj))
|
||||
|
||||
context = {
|
||||
'title': _("Are you sure?"),
|
||||
'queryset': queryset,
|
||||
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
||||
'forms': forms
|
||||
}
|
||||
return TemplateResponse(request, 'admin_associate_merge.html', context)
|
||||
|
||||
|
||||
@admin.register(models.Profile)
|
||||
class ProfileAdmin(UserAdmin):
|
||||
# Don't know how to add 'is_approved' whilst preserving the default list...
|
||||
list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
|
||||
class ProfileAdmin(UserAdmin, AssociateAdmin):
|
||||
list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events')
|
||||
list_display_links = ['username']
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password')}),
|
||||
(_('Personal info'), {
|
||||
@@ -49,62 +172,12 @@ class ProfileAdmin(UserAdmin):
|
||||
)
|
||||
form = user_forms.ProfileChangeForm
|
||||
add_form = user_forms.ProfileCreationForm
|
||||
actions = [approve_user]
|
||||
actions = ['approve_user', 'merge']
|
||||
|
||||
merge_fields = ['username', 'first_name', 'last_name', 'initials', 'email', 'phone', 'is_supervisor']
|
||||
|
||||
class AssociateAdmin(VersionAdmin):
|
||||
list_display = ('id', 'name', 'number_of_events')
|
||||
search_fields = ['id', 'name']
|
||||
list_display_links = ['id', 'name']
|
||||
actions = ['merge']
|
||||
|
||||
merge_fields = ['name']
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super(AssociateAdmin, self).get_queryset(request).annotate(event_count=Count('event'))
|
||||
|
||||
def number_of_events(self, obj):
|
||||
return obj.latest_events.count()
|
||||
|
||||
number_of_events.admin_order_field = 'event_count'
|
||||
|
||||
def merge(self, request, queryset):
|
||||
if request.POST.get('post'): # Has the user confirmed which is the master record?
|
||||
try:
|
||||
masterObjectPk = request.POST.get('master')
|
||||
masterObject = queryset.get(pk=masterObjectPk)
|
||||
except ObjectDoesNotExist:
|
||||
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
|
||||
return
|
||||
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
for obj in queryset.exclude(pk=masterObjectPk):
|
||||
events = obj.event_set.all()
|
||||
for event in events:
|
||||
masterObject.event_set.add(event)
|
||||
obj.delete()
|
||||
reversion.set_comment('Merging Objects')
|
||||
|
||||
self.message_user(request, "Objects successfully merged.")
|
||||
return
|
||||
else: # Present the confirmation screen
|
||||
|
||||
class TempForm(ModelForm):
|
||||
class Meta:
|
||||
model = queryset.model
|
||||
fields = self.merge_fields
|
||||
|
||||
forms = []
|
||||
for obj in queryset:
|
||||
forms.append(TempForm(instance=obj))
|
||||
|
||||
context = {
|
||||
'title': _("Are you sure?"),
|
||||
'queryset': queryset,
|
||||
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
||||
'forms': forms
|
||||
}
|
||||
return TemplateResponse(request, 'admin_associate_merge.html', context)
|
||||
def approve_user(modeladmin, request, queryset):
|
||||
queryset.update(is_approved=True)
|
||||
|
||||
|
||||
@admin.register(models.Person)
|
||||
|
||||
@@ -153,6 +153,10 @@ class EventAuthorisationRequestForm(forms.Form):
|
||||
|
||||
|
||||
class EventRiskAssessmentForm(forms.ModelForm):
|
||||
related_models = {
|
||||
'power_mic': models.Profile,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for name, field in self.fields.items():
|
||||
@@ -172,9 +176,9 @@ class EventRiskAssessmentForm(forms.ModelForm):
|
||||
unexpected_values = []
|
||||
for field, value in models.RiskAssessment.expected_values.items():
|
||||
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'):
|
||||
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()
|
||||
|
||||
class Meta:
|
||||
@@ -235,9 +239,9 @@ class EventChecklistForm(forms.ModelForm):
|
||||
pk = int(key.split('_')[1])
|
||||
|
||||
for field in other_fields:
|
||||
value = self.data['{}_{}'.format(field, pk)]
|
||||
value = self.data[f'{field}_{pk}']
|
||||
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:
|
||||
item = models.EventChecklistCrew.objects.get(pk=pk)
|
||||
|
||||
18
RIGS/migrations/0045_alter_profile_is_approved.py
Normal file
18
RIGS/migrations/0045_alter_profile_is_approved.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
112
RIGS/models.py
112
RIGS/models.py
@@ -8,6 +8,7 @@ from urllib.parse import urlparse
|
||||
|
||||
import pytz
|
||||
from django import forms
|
||||
from django.db.models import Q, F
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -20,11 +21,22 @@ from reversion.models import Version
|
||||
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):
|
||||
initials = models.CharField(max_length=5, null=True, blank=False)
|
||||
phone = models.CharField(max_length=13, blank=True, 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...
|
||||
last_emailed = models.DateTimeField(blank=True, null=True)
|
||||
dark_theme = models.BooleanField(default=False)
|
||||
@@ -51,7 +63,7 @@ class Profile(AbstractUser):
|
||||
def name(self):
|
||||
name = self.get_full_name()
|
||||
if self.initials:
|
||||
name += ' "{}"'.format(self.initials)
|
||||
name += f' "{self.initials}"'
|
||||
return name
|
||||
|
||||
@property
|
||||
@@ -70,15 +82,28 @@ class Profile(AbstractUser):
|
||||
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):
|
||||
name = models.CharField(max_length=50)
|
||||
phone = models.CharField(max_length=15, blank=True, default='')
|
||||
email = models.EmailField(blank=True, default='')
|
||||
|
||||
address = models.TextField(blank=True, default='')
|
||||
|
||||
notes = models.TextField(blank=True, default='')
|
||||
|
||||
objects = ContactableManager()
|
||||
|
||||
def __str__(self):
|
||||
string = self.name
|
||||
if self.notes is not None:
|
||||
@@ -110,12 +135,12 @@ class Organisation(models.Model, RevisionMixin):
|
||||
name = models.CharField(max_length=50)
|
||||
phone = models.CharField(max_length=15, blank=True, default='')
|
||||
email = models.EmailField(blank=True, default='')
|
||||
|
||||
address = models.TextField(blank=True, default='')
|
||||
|
||||
notes = models.TextField(blank=True, default='')
|
||||
union_account = models.BooleanField(default=False)
|
||||
|
||||
objects = ContactableManager()
|
||||
|
||||
def __str__(self):
|
||||
string = self.name
|
||||
if self.notes is not None:
|
||||
@@ -184,9 +209,10 @@ class Venue(models.Model, RevisionMixin):
|
||||
email = models.EmailField(blank=True, default='')
|
||||
three_phase_available = models.BooleanField(default=False)
|
||||
notes = models.TextField(blank=True, default='')
|
||||
|
||||
address = models.TextField(blank=True, default='')
|
||||
|
||||
objects = ContactableManager()
|
||||
|
||||
def __str__(self):
|
||||
string = self.name
|
||||
if self.notes and len(self.notes) > 0:
|
||||
@@ -260,6 +286,23 @@ class EventManager(models.Manager):
|
||||
|
||||
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'])
|
||||
class Event(models.Model, RevisionMixin):
|
||||
@@ -314,10 +357,8 @@ class Event(models.Model, RevisionMixin):
|
||||
def display_id(self):
|
||||
if self.pk:
|
||||
if self.is_rig:
|
||||
return str("N%05d" % self.pk)
|
||||
|
||||
return f"N{self.pk:05d}"
|
||||
return self.pk
|
||||
|
||||
return "????"
|
||||
|
||||
# Calculated values
|
||||
@@ -530,6 +571,34 @@ class InvoiceManager(models.Manager):
|
||||
query = self.raw(sql)
|
||||
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'])
|
||||
class Invoice(models.Model, RevisionMixin):
|
||||
@@ -569,14 +638,14 @@ class Invoice(models.Model, RevisionMixin):
|
||||
|
||||
@property
|
||||
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):
|
||||
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
|
||||
return f"{self.display_id}: {self.event} (£{self.balance:.2f})"
|
||||
|
||||
@property
|
||||
def display_id(self):
|
||||
return "{:05d}".format(self.pk)
|
||||
return f"#{self.pk:05d}"
|
||||
|
||||
class Meta:
|
||||
ordering = ['-invoice_date']
|
||||
@@ -681,7 +750,7 @@ class RiskAssessment(models.Model, RevisionMixin):
|
||||
'contractors': False,
|
||||
'other_companies': False,
|
||||
'crew_fatigue': False,
|
||||
'big_power': False,
|
||||
# 'big_power': False Doesn't require checking with a super either way
|
||||
'generators': False,
|
||||
'other_companies_power': False,
|
||||
'nonstandard_equipment_power': False,
|
||||
@@ -723,15 +792,22 @@ class RiskAssessment(models.Model, RevisionMixin):
|
||||
else:
|
||||
return self.SMALL[0]
|
||||
|
||||
def get_event_size_display(self):
|
||||
return self.SIZES[self.event_size][1] + " Event"
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return str(self.event)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return str(self)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ra_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return "%i - %s" % (self.pk, self.event)
|
||||
return f"{self.pk} | {self.event}"
|
||||
|
||||
|
||||
@reversion.register(follow=['vehicles', 'crew'])
|
||||
@@ -813,7 +889,7 @@ class EventChecklist(models.Model, RevisionMixin):
|
||||
return reverse('ec_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return "%i - %s" % (self.pk, self.event)
|
||||
return f"{self.pk} - {self.event}"
|
||||
|
||||
|
||||
@reversion.register
|
||||
@@ -825,7 +901,7 @@ class EventChecklistVehicle(models.Model, RevisionMixin):
|
||||
reversion_hide = True
|
||||
|
||||
def __str__(self):
|
||||
return "{} driven by {}".format(self.vehicle, str(self.driver))
|
||||
return f"{self.vehicle} driven by {self.driver}"
|
||||
|
||||
|
||||
@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.')
|
||||
|
||||
def __str__(self):
|
||||
return "{} ({})".format(str(self.crewmember), self.role)
|
||||
return f"{self.crewmember} ({self.role})"
|
||||
|
||||
@@ -54,7 +54,7 @@ def send_eventauthorisation_success_email(instance):
|
||||
elif instance.event.organisation is not None and instance.email == instance.event.organisation.email:
|
||||
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(
|
||||
subject,
|
||||
@@ -70,7 +70,7 @@ def send_eventauthorisation_success_email(instance):
|
||||
|
||||
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(),
|
||||
'application/pdf'
|
||||
)
|
||||
@@ -116,7 +116,7 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
|
||||
}
|
||||
|
||||
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),
|
||||
to=[admin.email],
|
||||
reply_to=[user.email],
|
||||
|
||||
139
RIGS/templates/base_print.xml
Normal file
139
RIGS/templates/base_print.xml
Normal 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 & 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>
|
||||
@@ -27,15 +27,12 @@
|
||||
|
||||
calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
themeSystem: 'bootstrap',
|
||||
//defaultView: 'dayGridMonth', This is now default
|
||||
aspectRatio: 1.5,
|
||||
eventTimeFormat: {
|
||||
'hour': '2-digit',
|
||||
'minute': '2-digit',
|
||||
'hour12': false
|
||||
},
|
||||
//nowIndicator: true,
|
||||
//firstDay: 1,
|
||||
headerToolbar: false,
|
||||
editable: false,
|
||||
dayMaxEventRows: true, // allow "more" link when too many events
|
||||
@@ -58,8 +55,10 @@
|
||||
};
|
||||
$(doc).each(function() {
|
||||
end = $(this).attr('latest')
|
||||
allDay = false
|
||||
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 = {
|
||||
@@ -67,7 +66,8 @@
|
||||
'end': end,
|
||||
'className': 'modal-href',
|
||||
'title': $(this).attr('title'),
|
||||
'url': $(this).attr('url')
|
||||
'url': $(this).attr('url'),
|
||||
'allDay': allDay
|
||||
}
|
||||
|
||||
if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
<div class="col-sm-8">
|
||||
<div class="row">
|
||||
<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 %}
|
||||
<option value="{{form.person.value}}" selected="selected" data-update_url="{% url 'person_update' form.person.value %}">{{ person }}</option>
|
||||
{% endif %}
|
||||
@@ -149,7 +149,7 @@
|
||||
<div class="col-sm-8">
|
||||
<div class="row">
|
||||
<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 %}
|
||||
<option value="{{form.organisation.value}}" selected="selected" data-update_url="{% url 'organisation_update' form.organisation.value %}">{{ organisation }}</option>
|
||||
{% endif %}
|
||||
@@ -207,7 +207,7 @@
|
||||
<div class="col-sm-8">
|
||||
<div class="row">
|
||||
<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 %}
|
||||
<option value="{{form.venue.value}}" selected="selected" data-update_url="{% url 'venue_update' form.venue.value %}">{{ venue }}</option>
|
||||
{% endif %}
|
||||
@@ -277,10 +277,8 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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">
|
||||
{% render_field form.dry_hire %}{{ form.dry_hire.label }}
|
||||
</label>
|
||||
{{ form.dry_hire.label }} {% render_field form.dry_hire %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -302,7 +300,7 @@
|
||||
class="col-sm-4 col-form-label">{{ form.mic.label }}</label>
|
||||
|
||||
<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 %}
|
||||
<option value="{{form.mic.value}}" selected="selected" >{{ mic.name }}</option>
|
||||
{% endif %}
|
||||
@@ -316,7 +314,7 @@
|
||||
class="col-sm-4 col-form-label">{{ form.checked_in_by.label }}</label>
|
||||
|
||||
<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 %}
|
||||
<option value="{{form.checked_in_by.value}}" selected="selected" >{{ checked_in_by.name }}</option>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,136 +1,5 @@
|
||||
<?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>
|
||||
{% extends 'base_print.xml' %}
|
||||
|
||||
<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 > {# 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 & 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">
|
||||
[Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
|
||||
</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">
|
||||
[Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
|
||||
</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>
|
||||
{% block content %}
|
||||
{% include "event_print_page.xml" %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{% load markdown_tags %}
|
||||
{% load filters %}
|
||||
|
||||
<setNextFrame name="main"/>
|
||||
<nextFrame/>
|
||||
<blockTable style="headLayout" colWidths="330,165">
|
||||
<tr>
|
||||
<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">
|
||||
<b>{{object.start_date|date:"D jS N Y"}}</b>
|
||||
@@ -180,7 +178,7 @@
|
||||
{% for item in object.items.all %}
|
||||
<tr>
|
||||
<td>
|
||||
<para>{{ item.name }}</para>
|
||||
<para><b>{{ item.name }}</b></para>
|
||||
{% if item.description %}
|
||||
{{ item.description|markdown:"rml" }}
|
||||
{% endif %}
|
||||
|
||||
@@ -5,21 +5,6 @@
|
||||
|
||||
{% 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 %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
@@ -33,11 +18,11 @@
|
||||
<dl class="dl-horizontal">
|
||||
{% if object.person.email %}
|
||||
<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 %}
|
||||
{% if object.organisation.email %}
|
||||
<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 %}
|
||||
</dl>
|
||||
{% else %}
|
||||
@@ -57,11 +42,20 @@
|
||||
</form>
|
||||
</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>
|
||||
$('#auth-request-form').on('submit', function () {
|
||||
$('#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>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
135
RIGS/templates/hs/ra_print.xml
Normal file
135
RIGS/templates/hs/ra_print.xml
Normal 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 %}
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
{% load help_text from filters %}
|
||||
{% load yesnoi from filters %}
|
||||
{% load linkornone from filters %}
|
||||
{% load filters %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row py-3">
|
||||
@@ -47,7 +45,7 @@
|
||||
</dd>
|
||||
<dt class="col-sm-6">{{ object|help_text:'power_mic'|safe }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ object.power_mic.name|default:'None' }}
|
||||
{{ object.power_mic.name|default:object.event.mic }}
|
||||
</dd>
|
||||
<dt class="col-sm-6">{{ object|help_text:'outside' }}</dt>
|
||||
<dd class="col-sm-6">
|
||||
@@ -144,7 +142,7 @@
|
||||
</dd>
|
||||
<dt class="col-12">{{ object|help_text:'persons_responsible_structures' }}</dt>
|
||||
<dd class="col-12">
|
||||
{{ object.persons_responsible_structures.name|default:'N/A'|linebreaks }}
|
||||
{{ object.persons_responsible_structures|default:'N/A'|linebreaks }}
|
||||
</dd>
|
||||
<dt class="col-12">{{ object|help_text:'rigging_plan'|safe }}</dt>
|
||||
<dd class="col-12">
|
||||
@@ -157,6 +155,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
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>
|
||||
@@ -98,9 +98,9 @@
|
||||
<label for="{{ form.power_mic.id_for_label }}"
|
||||
class="col col-form-label">{{ form.power_mic.help_text|safe }}</label>
|
||||
<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">
|
||||
{% if object.power_mic %}
|
||||
<option value="{{object.power_mic.pk}}" selected="selected">{{ object.power_mic.name }}</option>
|
||||
<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 power_mic %}
|
||||
<option value="{{form.power_mic.value}}" selected="selected">{{ power_mic }}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
@@ -40,7 +40,7 @@
|
||||
<dt class="col-sm-6">Phone Number</dt>
|
||||
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</dd>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{% if event.internal %}
|
||||
<a class="btn item-add modal-href event-authorise-request
|
||||
{% 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 %}
|
||||
btn-warning
|
||||
{% elif event.auth_request_to %}
|
||||
@@ -18,7 +18,7 @@
|
||||
btn-secondary
|
||||
{% 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="d-none d-sm-inline">
|
||||
{% if event.authorised %}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{% if object.venue %}
|
||||
<dt class="col-sm-6">Venue Notes</dt>
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
{% endif %}
|
||||
{% if not event.dry_hire %}
|
||||
{% 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 %}
|
||||
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% load namewithnotes from filters %}
|
||||
{% load markdown_tags %}
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0" id="event_table">
|
||||
<thead>
|
||||
@@ -29,7 +30,15 @@
|
||||
<!---Number-->
|
||||
<th scope="row" id="event_number">{{ event.display_id }}</th>
|
||||
<!--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" }}
|
||||
{% if event.has_start_time %}
|
||||
{{ event.start_time|date:"H:i" }}
|
||||
@@ -43,14 +52,6 @@
|
||||
{% endif %}</strong>
|
||||
</span>
|
||||
{% 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>
|
||||
<!---Details-->
|
||||
<td id="event_details" class="w-100">
|
||||
@@ -74,7 +75,7 @@
|
||||
</h5>
|
||||
{% endif %}
|
||||
{% if not event.cancelled and event.description %}
|
||||
<p>{{ event.description|linebreaksbr }}</p>
|
||||
<p>{{ event.description|markdown }}</p>
|
||||
{% endif %}
|
||||
{% include 'partials/event_status.html' %}
|
||||
</td>
|
||||
|
||||
@@ -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}
|
||||
elif type == 'submit':
|
||||
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}
|
||||
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ class TestEventCreate(BaseRigboardTest):
|
||||
self.assertEqual("Test Item 1", testitem['name'])
|
||||
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()
|
||||
|
||||
# See new item appear in table
|
||||
@@ -224,9 +224,9 @@ class TestEventCreate(BaseRigboardTest):
|
||||
self.assertEqual('47.90', row.subtotal)
|
||||
|
||||
# Check totals TODO convert to page properties
|
||||
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.assertEqual("9.58", self.driver.find_element_by_id('vat').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.assertEqual("9.58", self.driver.find_element(By.ID, 'vat').text)
|
||||
self.assertEqual("57.48", total.text)
|
||||
|
||||
self.page.submit()
|
||||
|
||||
@@ -83,6 +83,7 @@ urlpatterns = [
|
||||
name='ra_list'),
|
||||
path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(views.EventRiskAssessmentReview.as_view()),
|
||||
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()),
|
||||
name='event_ec'),
|
||||
|
||||
@@ -28,7 +28,8 @@ class InvoiceIndex(generic.ListView):
|
||||
total = 0
|
||||
for i in context['object_list']:
|
||||
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"
|
||||
return context
|
||||
|
||||
@@ -43,7 +44,7 @@ class InvoiceDetail(generic.DetailView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
|
||||
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date}) "
|
||||
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date})"
|
||||
if self.object.void:
|
||||
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
|
||||
elif self.object.is_closed:
|
||||
@@ -59,11 +60,14 @@ class InvoicePrint(generic.View):
|
||||
object = invoice.event
|
||||
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 = {
|
||||
'object': object,
|
||||
'invoice': invoice,
|
||||
'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)
|
||||
@@ -73,7 +77,7 @@ class InvoicePrint(generic.View):
|
||||
pdfData = buffer.read()
|
||||
|
||||
response = HttpResponse(content_type='application/pdf')
|
||||
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
|
||||
response['Content-Disposition'] = f'filename="{filename}"'
|
||||
response.write(pdfData)
|
||||
return response
|
||||
|
||||
@@ -124,32 +128,7 @@ class InvoiceArchive(generic.ListView):
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
q = self.request.GET.get('q', "")
|
||||
|
||||
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
|
||||
return self.model.objects.search(self.request.GET.get('q')).order_by('-invoice_date')
|
||||
|
||||
|
||||
class InvoiceWaiting(generic.ListView):
|
||||
@@ -163,7 +142,7 @@ class InvoiceWaiting(generic.ListView):
|
||||
objects = self.get_queryset()
|
||||
for obj in objects:
|
||||
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
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@@ -6,11 +6,13 @@ from django.views import generic
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from RIGS import models, forms
|
||||
from RIGS.views.rigboard import get_related
|
||||
from PyRIGS.views import PrintView
|
||||
|
||||
|
||||
class EventRiskAssessmentCreate(generic.CreateView):
|
||||
model = models.RiskAssessment
|
||||
template_name = 'risk_assessment_form.html'
|
||||
template_name = 'hs/risk_assessment_form.html'
|
||||
form_class = forms.EventRiskAssessmentForm
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
@@ -37,7 +39,8 @@ class EventRiskAssessmentCreate(generic.CreateView):
|
||||
epk = self.kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
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
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -46,7 +49,7 @@ class EventRiskAssessmentCreate(generic.CreateView):
|
||||
|
||||
class EventRiskAssessmentEdit(generic.UpdateView):
|
||||
model = models.RiskAssessment
|
||||
template_name = 'risk_assessment_form.html'
|
||||
template_name = 'hs/risk_assessment_form.html'
|
||||
form_class = forms.EventRiskAssessmentForm
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -62,24 +65,25 @@ class EventRiskAssessmentEdit(generic.UpdateView):
|
||||
ra = models.RiskAssessment.objects.get(pk=rpk)
|
||||
context['event'] = ra.event
|
||||
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
|
||||
|
||||
|
||||
class EventRiskAssessmentDetail(generic.DetailView):
|
||||
model = models.RiskAssessment
|
||||
template_name = 'risk_assessment_detail.html'
|
||||
template_name = 'hs/risk_assessment_detail.html'
|
||||
|
||||
def get_context_data(self, **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
|
||||
|
||||
|
||||
class EventRiskAssessmentList(generic.ListView):
|
||||
paginate_by = 20
|
||||
model = models.RiskAssessment
|
||||
template_name = 'hs_object_list.html'
|
||||
template_name = 'hs/hs_object_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
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):
|
||||
model = models.EventChecklist
|
||||
template_name = 'event_checklist_detail.html'
|
||||
template_name = 'hs/event_checklist_detail.html'
|
||||
|
||||
def get_context_data(self, **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
|
||||
|
||||
|
||||
class EventChecklistEdit(generic.UpdateView):
|
||||
model = models.EventChecklist
|
||||
template_name = 'event_checklist_form.html'
|
||||
template_name = 'hs/event_checklist_form.html'
|
||||
form_class = forms.EventChecklistForm
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -134,19 +138,14 @@ class EventChecklistEdit(generic.UpdateView):
|
||||
ec = models.EventChecklist.objects.get(pk=pk)
|
||||
context['event'] = ec.event
|
||||
context['edit'] = True
|
||||
context['page_title'] = 'Edit Event Checklist for Event {}'.format(ec.event.display_id)
|
||||
form = context['form']
|
||||
# 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)
|
||||
context['page_title'] = f'Edit Event Checklist for Event {ec.event.display_id}'
|
||||
get_related(context['form'], context)
|
||||
return context
|
||||
|
||||
|
||||
class EventChecklistCreate(generic.CreateView):
|
||||
model = models.EventChecklist
|
||||
template_name = 'event_checklist_form.html'
|
||||
template_name = 'hs/event_checklist_form.html'
|
||||
form_class = forms.EventChecklistForm
|
||||
|
||||
# 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()
|
||||
|
||||
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 super(EventChecklistCreate, self).get(self)
|
||||
@@ -175,7 +174,7 @@ class EventChecklistCreate(generic.CreateView):
|
||||
epk = self.kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
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
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -185,7 +184,7 @@ class EventChecklistCreate(generic.CreateView):
|
||||
class EventChecklistList(generic.ListView):
|
||||
paginate_by = 20
|
||||
model = models.EventChecklist
|
||||
template_name = 'hs_object_list.html'
|
||||
template_name = 'hs/hs_object_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
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):
|
||||
paginate_by = 20
|
||||
model = models.Event
|
||||
template_name = 'hs_list.html'
|
||||
template_name = 'hs/hs_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
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['page_title'] = 'H&S Overview'
|
||||
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
|
||||
|
||||
@@ -93,7 +93,7 @@ class CalendarICS(ICalFeed):
|
||||
title += item.name
|
||||
|
||||
# Add the status
|
||||
title += ' (' + str(item.get_status_display()) + ')'
|
||||
title += f' ({item.get_status_display()})'
|
||||
|
||||
return title
|
||||
|
||||
@@ -101,9 +101,8 @@ class CalendarICS(ICalFeed):
|
||||
return item.earliest_time
|
||||
|
||||
def item_end_datetime(self, item):
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
def item_location(self, item):
|
||||
@@ -115,13 +114,13 @@ class CalendarICS(ICalFeed):
|
||||
|
||||
tz = pytz.timezone(self.timezone)
|
||||
|
||||
desc = 'Rig ID = ' + str(item.pk) + '\n'
|
||||
desc += 'Event = ' + item.name + '\n'
|
||||
desc = f'Rig ID = {item.display_id}\n'
|
||||
desc += f'Event = {item.name}\n'
|
||||
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
|
||||
if item.is_rig and item.person:
|
||||
desc += 'Client = ' + item.person.name + (
|
||||
(' 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 += '\n'
|
||||
@@ -140,23 +139,18 @@ class CalendarICS(ICalFeed):
|
||||
|
||||
desc += '\n'
|
||||
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
|
||||
# desc += 'Notes:\n'+item.notes+'\n\n'
|
||||
|
||||
base_url = "https://rigs.nottinghamtec.co.uk"
|
||||
desc += 'URL = ' + base_url + str(item.get_absolute_url())
|
||||
desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}'
|
||||
|
||||
return desc
|
||||
|
||||
def item_link(self, item):
|
||||
# Make a link to the event in the web interface
|
||||
# base_url = "https://pyrigs.nottinghamtec.co.uk"
|
||||
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
|
||||
return item.last_edited_at
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import copy
|
||||
import datetime
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from io import BytesIO
|
||||
|
||||
import premailer
|
||||
import simplejson
|
||||
from PyPDF2 import PdfFileMerger, PdfFileReader
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.staticfiles import finders
|
||||
@@ -24,10 +19,9 @@ from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import generic
|
||||
from z3c.rml import rml2pdf
|
||||
|
||||
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
|
||||
|
||||
__author__ = 'ghost'
|
||||
@@ -98,11 +92,8 @@ class EventCreate(generic.CreateView):
|
||||
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.")
|
||||
|
||||
# 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)
|
||||
get_related(form, context)
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -121,11 +112,7 @@ class EventUpdate(generic.UpdateView):
|
||||
|
||||
form = context['form']
|
||||
|
||||
# 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)
|
||||
get_related(form, context)
|
||||
|
||||
return context
|
||||
|
||||
@@ -178,35 +165,16 @@ class EventDuplicate(EventUpdate):
|
||||
return context
|
||||
|
||||
|
||||
class EventPrint(generic.View):
|
||||
def get(self, request, pk):
|
||||
object = get_object_or_404(models.Event, pk=pk)
|
||||
template = get_template('event_print.xml')
|
||||
class EventPrint(PrintView):
|
||||
model = models.Event
|
||||
template_name = 'event_print.xml'
|
||||
append_terms = True
|
||||
|
||||
merger = PdfFileMerger()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['quote'] = True
|
||||
context['filename'] = f"Event_{context['object'].display_id}_{context['object_name']}_{context['object'].start_date}.pdf"
|
||||
return context
|
||||
|
||||
|
||||
class EventArchive(generic.ListView):
|
||||
@@ -216,7 +184,6 @@ class EventArchive(generic.ListView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['start'] = self.request.GET.get('start', None)
|
||||
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
|
||||
context['statuses'] = models.Event.EVENT_STATUS_CHOICES
|
||||
@@ -240,32 +207,17 @@ class EventArchive(generic.ListView):
|
||||
filter &= Q(start_date__gte=start)
|
||||
|
||||
q = self.request.GET.get('q', "")
|
||||
objects = self.model.objects.all()
|
||||
|
||||
if q != "":
|
||||
qfilter = Q(name__icontains=q) | Q(description__icontains=q) | Q(notes__icontains=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
|
||||
if q:
|
||||
objects = self.model.objects.search(q)
|
||||
|
||||
status = self.request.GET.getlist('status', "")
|
||||
|
||||
if len(status) > 0:
|
||||
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
|
||||
qs.select_related('person', 'organisation', 'venue', 'mic')
|
||||
@@ -320,7 +272,7 @@ class EventAuthorise(generic.UpdateView):
|
||||
messages.add_message(self.request, messages.WARNING,
|
||||
"This event has already been authorised, but the amount has changed. " +
|
||||
"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):
|
||||
form = super().get_form(**kwargs)
|
||||
@@ -389,7 +341,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
||||
context['to_name'] = event.organisation.name
|
||||
|
||||
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),
|
||||
to=[email],
|
||||
reply_to=[self.request.user.email],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from RIGS.admin import AssociateAdmin
|
||||
from assets import models as assets
|
||||
|
||||
|
||||
@@ -17,9 +17,13 @@ class AssetStatusAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(assets.Supplier)
|
||||
class SupplierAdmin(VersionAdmin):
|
||||
class SupplierAdmin(AssociateAdmin):
|
||||
list_display = ['id', 'name']
|
||||
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)
|
||||
|
||||
@@ -33,6 +33,7 @@ class AssetSearchForm(forms.Form):
|
||||
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
|
||||
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
|
||||
is_cable = forms.BooleanField(required=False)
|
||||
cable_type = forms.ModelMultipleChoiceField(models.CableType.objects.all(), required=False)
|
||||
date_acquired = forms.DateField(required=False)
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import random
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from django.db.utils import IntegrityError
|
||||
from django.utils import timezone
|
||||
from reversion import revisions as reversion
|
||||
|
||||
@@ -125,5 +126,9 @@ class Command(BaseCommand):
|
||||
if i % 3 == 0:
|
||||
asset.purchased_from = random.choice(self.suppliers)
|
||||
|
||||
asset.clean()
|
||||
asset.save()
|
||||
with transaction.atomic():
|
||||
try:
|
||||
asset.clean()
|
||||
asset.save()
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
19
assets/migrations/0023_alter_asset_purchased_from.py
Normal file
19
assets/migrations/0023_alter_asset_purchased_from.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
assets/migrations/0024_alter_asset_salvage_value.py
Normal file
18
assets/migrations/0024_alter_asset_salvage_value.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
24
assets/migrations/0026_auto_20220526_1623.py
Normal file
24
assets/migrations/0026_auto_20220526_1623.py
Normal 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]),
|
||||
),
|
||||
]
|
||||
@@ -2,11 +2,12 @@ import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, connection
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from reversion import revisions as reversion
|
||||
from reversion.models import Version
|
||||
|
||||
from RIGS.models import Profile
|
||||
from RIGS.models import Profile, ContactableManager
|
||||
from versioning.versioning import RevisionMixin
|
||||
|
||||
|
||||
@@ -46,6 +47,8 @@ class Supplier(models.Model, RevisionMixin):
|
||||
|
||||
notes = models.TextField(blank=True, default="")
|
||||
|
||||
objects = ContactableManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -88,23 +91,23 @@ class CableType(models.Model):
|
||||
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=""):
|
||||
sql = """
|
||||
SELECT a.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
|
||||
a.asset_id_prefix = b.asset_id_prefix)
|
||||
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s;
|
||||
"""
|
||||
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()
|
||||
last_asset = Asset.objects.filter(asset_id_prefix=wanted_prefix).last()
|
||||
return 9000 if last_asset is None else wanted_prefix + str(last_asset.asset_id_number + 1)
|
||||
|
||||
|
||||
def validate_positive(value):
|
||||
if value < 0:
|
||||
raise ValidationError("A price cannot be negative")
|
||||
|
||||
|
||||
@reversion.register
|
||||
@@ -116,11 +119,11 @@ class Asset(models.Model, RevisionMixin):
|
||||
category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE)
|
||||
status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE)
|
||||
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_sold = models.DateField(blank=True, null=True)
|
||||
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
|
||||
salvage_value = 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])
|
||||
replacement_cost = models.DecimalField(null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
# Audit
|
||||
@@ -142,6 +145,8 @@ class Asset(models.Model, RevisionMixin):
|
||||
|
||||
reversion_perm = 'assets.asset_finance'
|
||||
|
||||
objects = AssetManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['asset_id_prefix', 'asset_id_number']
|
||||
permissions = [
|
||||
@@ -165,12 +170,6 @@ class Asset(models.Model, RevisionMixin):
|
||||
errdict["asset_id"] = [
|
||||
"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 not self.length or self.length <= 0:
|
||||
errdict["length"] = ["The length of a cable must be more than 0"]
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
date = new Date();
|
||||
}
|
||||
$('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'));
|
||||
return false;
|
||||
}
|
||||
function setFieldValue(ID, CSA) {
|
||||
$('#' + String(ID)).val(CSA);
|
||||
return false;
|
||||
}
|
||||
function checkIfCableHidden() {
|
||||
document.getElementById("cable-table").hidden = !document.getElementById("id_is_cable").checked;
|
||||
@@ -39,16 +41,16 @@
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.date_acquired col="col-6" %}
|
||||
<div class="col-sm-4">
|
||||
<button class="btn btn-info" onclick="setAcquired(true);" tabindex="-1">Today</button>
|
||||
<button class="btn btn-warning" onclick="setAcquired(false);" tabindex="-1">Unknown</button>
|
||||
<div class="col-sm-2">
|
||||
<button class="btn btn-info" onclick="return setAcquired(true);" tabindex="-1">Today</button>
|
||||
<button class="btn btn-warning" onclick="return setAcquired(false);" tabindex="-1">Unknown</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.date_sold col="col-6" %}
|
||||
</div>
|
||||
<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>
|
||||
<hr>
|
||||
<div class="form-group form-row">
|
||||
@@ -64,16 +66,16 @@
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %}
|
||||
<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-success" onclick="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-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="return setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ 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 class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text title='CSA' col="col-6" %}
|
||||
<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="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 }}', '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 }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,13 +75,13 @@
|
||||
<div class="col">
|
||||
<div id="category-group" class="form-group px-1" style="margin-bottom: 0;">
|
||||
<label for="category" class="sr-only">Category</label>
|
||||
{% render_field form.category|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
|
||||
{% 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 class="col">
|
||||
<div id="status-group" class="form-group px-1" style="margin-bottom: 0;">
|
||||
<label for="status" class="sr-only">Status</label>
|
||||
{% render_field form.status|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
|
||||
{% 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 mt-2">
|
||||
@@ -121,6 +121,7 @@
|
||||
</button></span>{%endfor%}</p>
|
||||
</div>
|
||||
</div>
|
||||
<h3>{{ object_list.count }} assets</h3>
|
||||
<div class="row">
|
||||
<div class="col px-0">
|
||||
{% include 'partials/asset_list_table.html' %}
|
||||
|
||||
162
assets/templates/cable_list.html
Normal file
162
assets/templates/cable_list.html
Normal 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 %}
|
||||
@@ -12,22 +12,18 @@
|
||||
</template>
|
||||
<stylesheet>
|
||||
<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>
|
||||
</stylesheet>
|
||||
<story>
|
||||
<blockTable style="table">
|
||||
{% for i in images0 %}
|
||||
<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"
|
||||
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
|
||||
<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"
|
||||
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
|
||||
<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"
|
||||
width="120" height="35"/></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>
|
||||
<td>{% with images0|index:forloop.counter0 as image %}{% if image %}<illustration width="180" height="55" borderStrokeWidth="1"
|
||||
borderStrokeColor="black"><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 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"
|
||||
{% if image.0.csa >= 4 %}width="180" height="55"{% else %}width="130" height="38"{%endif%}/></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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</blockTable>
|
||||
|
||||
14
assets/templates/partials/asset_list_buttons.html
Normal file
14
assets/templates/partials/asset_list_buttons.html
Normal 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 %}
|
||||
@@ -18,19 +18,7 @@
|
||||
<td class="assetCategory align-middle">{{ item.category }}</td>
|
||||
<td class="assetStatus align-middle">{{ item.status }}</td>
|
||||
<td class="d-none d-sm-table-cell">
|
||||
{% 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 %}
|
||||
{% include 'partials/asset_list_buttons.html' %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load title_spaced from filters %}
|
||||
{% 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 %}
|
||||
<div class="input-group {{col}}">
|
||||
{% if prepend %}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{% if create or edit or duplicate %}
|
||||
<div class="form-group" id="parent-group">
|
||||
<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 %}
|
||||
<option value="{{object.parent.pk}}" selected>{{object.parent.description}}</option>
|
||||
{% endif %}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
|
||||
<div class="row">
|
||||
<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 %}
|
||||
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
|
||||
{% endif %}
|
||||
@@ -39,10 +39,10 @@
|
||||
</div>
|
||||
|
||||
<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-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>
|
||||
|
||||
@@ -70,8 +70,8 @@
|
||||
<dd>{% if object.purchased_from %}<a href="{{object.purchased_from.get_absolute_url}}">{{ object.purchased_from }}</a>{%else%}-{%endif%}</dd>
|
||||
<dt>Purchase Price</dt>
|
||||
<dd>£{{ object.purchase_price|default_if_none:'-' }}</dd>
|
||||
<dt>Salvage Value</dt>
|
||||
<dd>£{{ object.salvage_value|default_if_none:'-' }}</dd>
|
||||
<dt>Replacement Cost</dt>
|
||||
<dd>£{{ object.replacement_cost|default_if_none:'-' }}</dd>
|
||||
<dt>Date Acquired</dt>
|
||||
<dd>{{ object.date_acquired|default_if_none:'-' }}</dd>
|
||||
{% if object.date_sold %}
|
||||
|
||||
@@ -18,18 +18,23 @@ def status(db):
|
||||
|
||||
|
||||
@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)
|
||||
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
|
||||
yield cable_type
|
||||
connector.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()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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
|
||||
asset.delete()
|
||||
|
||||
@@ -79,7 +79,7 @@ class AssetForm(FormPage):
|
||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
|
||||
'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_sold': (regions.DatePicker, (By.ID, 'id_date_sold')),
|
||||
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
||||
@@ -221,7 +221,7 @@ class AssetAuditList(AssetList):
|
||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
||||
'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')),
|
||||
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
||||
'status': (regions.SingleSelectPicker, (By.ID, 'id_status')),
|
||||
|
||||
@@ -94,6 +94,55 @@ class TestAssetList(AutoLoginTest):
|
||||
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
|
||||
class TestAssetForm(AutoLoginTest):
|
||||
def setUp(self):
|
||||
@@ -130,7 +179,7 @@ class TestAssetForm(AutoLoginTest):
|
||||
self.page.comments = comments = "This is actually a sledgehammer, not a cable..."
|
||||
|
||||
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.purchased_from_selector.toggle()
|
||||
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
|
||||
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
|
||||
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!")
|
||||
|
||||
|
||||
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
|
||||
class TestAssetAudit(AutoLoginTest):
|
||||
def setUp(self):
|
||||
@@ -293,14 +320,14 @@ class TestAssetAudit(AutoLoginTest):
|
||||
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1,
|
||||
voltage_rating=40, num_pins=13)
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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.wait = WebDriverWait(self.driver, 20)
|
||||
|
||||
@@ -316,25 +343,6 @@ class TestAssetAudit(AutoLoginTest):
|
||||
self.driver.implicitly_wait(4)
|
||||
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):
|
||||
self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets))
|
||||
asset_row = self.page.assets[0]
|
||||
|
||||
@@ -84,7 +84,7 @@ def test_oembed(client, test_asset):
|
||||
|
||||
|
||||
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.')
|
||||
assert_asset_form_errors(response)
|
||||
|
||||
@@ -99,7 +99,7 @@ def test_cable_create(admin_client):
|
||||
|
||||
def test_asset_edit(admin_client, test_asset):
|
||||
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)
|
||||
|
||||
|
||||
@@ -127,4 +127,4 @@ def assert_asset_form_errors(response):
|
||||
assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
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', 'salvage_value', 'A price cannot be negative')
|
||||
assertFormError(response, 'form', 'replacement_cost', 'A price cannot be negative')
|
||||
|
||||
@@ -20,8 +20,9 @@ urlpatterns = [
|
||||
path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset')
|
||||
(views.AssetDuplicate.as_view()), name='asset_duplicate'),
|
||||
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/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'),
|
||||
|
||||
@@ -6,7 +6,7 @@ from io import BytesIO
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
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.urls import reverse, reverse_lazy
|
||||
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 PyPDF2 import PdfFileMerger, PdfFileReader
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
||||
from barcode import Code39
|
||||
from barcode.writer import ImageWriter
|
||||
from z3c.rml import rml2pdf
|
||||
@@ -25,14 +25,12 @@ from z3c.rml import rml2pdf
|
||||
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \
|
||||
is_ajax, OEmbedView
|
||||
from assets import forms, models
|
||||
from assets.models import get_available_asset_id
|
||||
|
||||
|
||||
class AssetList(LoginRequiredMixin, generic.ListView):
|
||||
model = models.Asset
|
||||
template_name = 'asset_list.html'
|
||||
paginate_by = 40
|
||||
ordering = ['-pk']
|
||||
hide_hidden_status = True
|
||||
|
||||
def get_initial(self):
|
||||
@@ -50,13 +48,7 @@ class AssetList(LoginRequiredMixin, generic.ListView):
|
||||
|
||||
# TODO Feedback to user when search fails
|
||||
query_string = form.cleaned_data['q'] or ""
|
||||
if len(query_string) == 0:
|
||||
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()))
|
||||
queryset = models.Asset.objects.search(query=query_string)
|
||||
|
||||
if form.cleaned_data['is_cable']:
|
||||
queryset = queryset.filter(is_cable=True)
|
||||
@@ -87,6 +79,25 @@ class AssetList(LoginRequiredMixin, generic.ListView):
|
||||
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:
|
||||
def get_object(self, queryset=None):
|
||||
pk = self.kwargs.get(self.pk_url_kwarg)
|
||||
@@ -140,7 +151,7 @@ class AssetCreate(LoginRequiredMixin, generic.CreateView):
|
||||
|
||||
def get_initial(self, *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
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -155,6 +166,11 @@ class DuplicateMixin:
|
||||
|
||||
|
||||
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):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["create"] = None
|
||||
@@ -176,6 +192,7 @@ class AssetOEmbed(OEmbedView):
|
||||
|
||||
class AssetAuditList(AssetList):
|
||||
template_name = 'asset_audit_list.html'
|
||||
hide_hidden_status = True
|
||||
|
||||
# TODO Refresh this when the modal is submitted
|
||||
def get_queryset(self):
|
||||
@@ -262,7 +279,7 @@ class SupplierUpdate(GenericUpdateView, ModalURLMixin):
|
||||
form_class = forms.SupplierForm
|
||||
|
||||
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):
|
||||
context['override'] = "base_ajax.html"
|
||||
else:
|
||||
@@ -306,7 +323,6 @@ class CableTypeCreate(generic.CreateView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["create"] = True
|
||||
context["page_title"] = "Create Cable Type"
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -322,7 +338,6 @@ class CableTypeUpdate(generic.UpdateView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["edit"] = True
|
||||
context["page_title"] = f"Edit Cable Type {self.object}"
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -333,7 +348,9 @@ def generate_label(pk):
|
||||
black = (0, 0, 0)
|
||||
white = (255, 255, 255)
|
||||
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)
|
||||
|
||||
asset_id = f"Asset: {obj.asset_id}"
|
||||
@@ -342,22 +359,25 @@ def generate_label(pk):
|
||||
csa = f"CSA: {obj.csa}mm²"
|
||||
|
||||
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")
|
||||
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:
|
||||
draw.text((210, 170), length, fill=black, font=font)
|
||||
draw.text((360, 170), csa, fill=black, font=font)
|
||||
draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", fill=black, font=font)
|
||||
y = 140
|
||||
draw.text((210, y), length, 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())
|
||||
|
||||
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})
|
||||
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
|
||||
|
||||
@@ -386,14 +406,16 @@ class GenerateLabels(generic.View):
|
||||
|
||||
base64_encoded_result_bytes = base64.b64encode(img_bytes)
|
||||
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 = {
|
||||
'images0': images[::4],
|
||||
'images1': images[1::4],
|
||||
'images2': images[2::4],
|
||||
'images3': images[3::4],
|
||||
'filename': "Asset Label Sheet generated at {}".format(timezone.now())
|
||||
'images0': images[::3],
|
||||
'images1': images[1::3],
|
||||
'images2': images[2::3],
|
||||
# 'images3': images[3::4],
|
||||
'filename': name
|
||||
}
|
||||
merger = PdfFileMerger()
|
||||
|
||||
@@ -405,6 +427,6 @@ class GenerateLabels(generic.View):
|
||||
merged = BytesIO()
|
||||
merger.write(merged)
|
||||
|
||||
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
|
||||
response['Content-Disposition'] = f'filename="{name}"'
|
||||
response.write(merged.getvalue())
|
||||
return response
|
||||
|
||||
@@ -27,6 +27,8 @@ def admin_user(admin_user):
|
||||
admin_user.first_name = "Event"
|
||||
admin_user.last_name = "Test"
|
||||
admin_user.initials = "ETU"
|
||||
admin_user.is_approved = True
|
||||
admin_user.is_supervisor = True
|
||||
admin_user.save()
|
||||
return admin_user
|
||||
|
||||
|
||||
1315
package-lock.json
generated
1315
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
||||
"autocompleter": "^6.1.2",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"bootstrap": "^4.6.1",
|
||||
"bootstrap-select": "^1.13.17",
|
||||
"bootstrap-select": "^1.13.18",
|
||||
"clipboard": "^2.0.8",
|
||||
"cssnano": "^5.0.13",
|
||||
"easymde": "^2.16.1",
|
||||
@@ -27,14 +27,14 @@
|
||||
"html5sortable": "^0.13.3",
|
||||
"jquery": "^3.6.0",
|
||||
"konami": "^1.6.3",
|
||||
"moment": "^2.27.0",
|
||||
"moment": "^2.29.4",
|
||||
"node-sass": "^7.0.0",
|
||||
"popper.js": "^1.16.1",
|
||||
"postcss": "^8.4.5",
|
||||
"uglify-js": "^3.14.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browser-sync": "^2.27.7"
|
||||
"browser-sync": "^2.27.10"
|
||||
},
|
||||
"scripts": {
|
||||
"gulp": "gulp",
|
||||
|
||||
@@ -47,14 +47,16 @@ function initPicker(obj) {
|
||||
//log: 3,
|
||||
preprocessData: function (data) {
|
||||
var i, l = data.length, array = [];
|
||||
array.push({
|
||||
text: clearSelectionLabel,
|
||||
value: '',
|
||||
data:{
|
||||
update_url: '',
|
||||
subtext:''
|
||||
}
|
||||
});
|
||||
if (!obj.data('noclear')) {
|
||||
array.push({
|
||||
text: clearSelectionLabel,
|
||||
value: '',
|
||||
data:{
|
||||
update_url: '',
|
||||
subtext:''
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (l) {
|
||||
for(i = 0; i < l; i++){
|
||||
@@ -71,11 +73,13 @@ function initPicker(obj) {
|
||||
return array;
|
||||
}
|
||||
};
|
||||
|
||||
obj.prepend($("<option></option>")
|
||||
.attr("value",'')
|
||||
.text(clearSelectionLabel)
|
||||
.data('update_url','')); //Add "clear selection" option
|
||||
console.log(obj.data);
|
||||
if (!obj.data('noclear')) {
|
||||
obj.prepend($("<option></option>")
|
||||
.attr("value",'')
|
||||
.text(clearSelectionLabel)
|
||||
.data('update_url','')); //Add "clear selection" option
|
||||
}
|
||||
|
||||
|
||||
obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker
|
||||
|
||||
@@ -77,13 +77,13 @@
|
||||
border-collapse: separate !important;
|
||||
border-spacing: 0;
|
||||
}
|
||||
.table tr th {
|
||||
#event_table tr th {
|
||||
border-right: 0 !important;
|
||||
}
|
||||
.table tr td {
|
||||
#event_table tr td {
|
||||
border-left: 0 !important;
|
||||
}
|
||||
.table tr td:not(:last-child) {
|
||||
#event_table tr td:not(:last-child) {
|
||||
border-right: 0 !important;
|
||||
}
|
||||
@each $color, $value in $theme-colors {
|
||||
|
||||
@@ -33,6 +33,25 @@ $fa-font-path: '/static/fonts';
|
||||
@import "node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
|
||||
@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
|
||||
(prefers-reduced-motion: reduce),
|
||||
(update: slow) {
|
||||
@@ -175,7 +194,7 @@ svg {
|
||||
|
||||
span.fas {
|
||||
padding-left: 0.1em !important;
|
||||
padding-right: 0.1em !important;
|
||||
padding-right: 0.3em !important;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
{% block content %}
|
||||
<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>
|
||||
{% for form in forms %}
|
||||
@@ -15,8 +15,8 @@
|
||||
<th>{{ field.label }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td><input type="radio" name="master" value="{{form.instance.pk|unlocalize}}"></td>
|
||||
<td>{{form.instance.pk}}</td>
|
||||
@@ -37,4 +37,4 @@
|
||||
<input type="submit" value="Merge them" />
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{% load nice_errors from filters %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-dismissable">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<div class="alert alert-danger mb-0">
|
||||
<dl>
|
||||
{% with form|nice_errors as qq %}
|
||||
{% for error_name,desc in qq.items %}
|
||||
<span class="row">
|
||||
<dt class="col-4">{{error_name}}</dt>
|
||||
<dd class="col-8">{{desc}}</dd>
|
||||
<dt class="col-3">{{error_name}}</dt>
|
||||
<dd class="col-9">{{desc}}</dd>
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends override|default:"base_rigs.html" %}
|
||||
{% load widget_tweaks %}
|
||||
{% load button from filters %}
|
||||
{% load markdown_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
@@ -22,7 +23,7 @@
|
||||
<dd>{{ object.address|linebreaksbr }}</dd>
|
||||
|
||||
<dt>Notes</dt>
|
||||
<dd>{{ object.notes|linebreaksbr }}</dd>
|
||||
<dd>{{ object.notes|markdown }}</dd>
|
||||
|
||||
{% if object.three_phase_available is not None %}
|
||||
<dt>Three Phase Available</dt>
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
{% extends override|default:"base_rigs.html" %}
|
||||
{% load button from filters %}
|
||||
{% 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 %}
|
||||
<div class="col">
|
||||
@@ -43,7 +64,7 @@
|
||||
<label for="{{ form.notes.id_for_label }}"
|
||||
class="col-sm-2 control-label">{{ form.notes.label }}</label>
|
||||
<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>
|
||||
{% if form.three_phase_available is not None %}
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'rigboard' %}"><span class="fas fa-list align-middle"></span><span class="align-middle"> Rigboard</span></a>
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'web_calendar' %}"><span class="fas fa-calendar align-middle"></span><span class="align-middle"> Calendar</span></a>
|
||||
{% if perms.RIGS.add_event %}
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'event_create' %}"><span class="fas fa-plus align-middle"></span><span class="align-middle"> New Event</span></a>
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'event_create' %}"><span class="fas fa-plus align-middle text-success"></span><span class="align-middle"> New Event</span></a>
|
||||
{% endif %}
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'event_archive' %}"><span class="fas fa-book align-middle"></span><span class="align-middle"> Event Archive</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,11 +30,11 @@
|
||||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'asset_index' %}"><span class="fas fa-tag align-middle"></span><span class="align-middle"> Asset List</span></a>
|
||||
{% if perms.assets.add_asset %}
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'asset_create' %}"><span class="fas fa-plus align-middle"></span><span class="align-middle"> New Asset</span></a>
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'asset_create' %}"><span class="fas fa-plus align-middle text-success"></span><span class="align-middle"> New Asset</span></a>
|
||||
{% endif %}
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'supplier_list' %}"><span class="fas fa-parachute-box align-middle"></span><span class="align-middle"> Supplier List</span></a>
|
||||
{% if perms.assets.add_supplier %}
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'supplier_create' %}"><span class="fas fa-plus align-middle"></span><span class="align-middle"> New Supplier</span></a>
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'supplier_create' %}"><span class="fas fa-plus align-middle text-success"></span><span class="align-middle"> New Supplier</span></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,10 +44,10 @@
|
||||
<img class="card-img-top d-none d-sm-block" src="{% static 'imgs/training.jpg' %}" alt="People watching a presentation" style="height: 150px; object-fit: cover;">
|
||||
<h4 class="card-header">Training Database</h4>
|
||||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action text-info" href="{% url 'trainee_detail' request.user.pk %}"><span class="fas fa-file-signature align-middle"></span><span class="align-middle"> My Training Record</span></a>
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'trainee_list' %}"><span class="fas fa-users"></span> Trainee List</a>
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'level_list' %}"><span class="fas fa-layer-group"></span> Level List</a></a>
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> Item List</a></a>
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'trainee_detail' request.user.pk %}"><span class="fas fa-file-signature align-middle text-info"></span><span class="align-middle"> My Training Record</span></a>
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'trainee_list' %}"><span class="fas fa-users"></span><span class="align-middle"> Trainee List</span></a>
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'level_list' %}"><span class="fas fa-layer-group"></span> <span class="align-middle">Level List</span></a>
|
||||
<a class="list-group-item list-group-item-action" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> <span class="align-middle">Item List</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,6 +58,7 @@
|
||||
<a class="list-group-item list-group-item-action" href="https://forum.nottinghamtec.co.uk" target="_blank" rel="noopener noreferrer"><span class="fas fa-comment-alt text-primary align-middle"></span><span class="align-middle"> TEC Forum</span></a>
|
||||
<a class="list-group-item list-group-item-action" href="//nottinghamtec.sharepoint.com" target="_blank" rel="noopener noreferrer"><span class="fas fa-folder text-info align-middle"></span><span class="align-middle"> TEC Sharepoint</span></a>
|
||||
<a class="list-group-item list-group-item-action" href="//wiki.nottinghamtec.co.uk" target="_blank" rel="noopener noreferrer"><span class="fas fa-pen-square align-middle"></span><span class="align-middle"> TEC Wiki</span></a>
|
||||
<a class="list-group-item list-group-item-action" href="https://secure.jotformeu.com/UoNSU/accident_report_form?studentGroup=Media+or+Service+Group&mediaserviceGroup=TEC" target="_blank" rel="noopener noreferrer"><span class="fas fa-heartbeat align-middle text-danger"></span><span class="align-middle"> H&S Report Form</span></a>
|
||||
{% if perms.RIGS.change_event %}
|
||||
<a class="list-group-item list-group-item-action" href="//members.nottinghamtec.co.uk/price" target="_blank" rel="noopener noreferrer"><span class="fas fa-pound-sign text-warning align-middle"></span><span class="align-middle"> Price List</span></a>
|
||||
{% endif %}
|
||||
|
||||
@@ -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>
|
||||
{% 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>
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
|
||||
@@ -1,38 +1,11 @@
|
||||
{% 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' %}">
|
||||
<div class="input-group input-group-sm flex-nowrap">
|
||||
<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 }}" />
|
||||
<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 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>
|
||||
<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>
|
||||
<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>
|
||||
</form>
|
||||
{% 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 %}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{% load widget_tweaks %}
|
||||
{% 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">
|
||||
<form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
|
||||
48
templates/search_results.html
Normal file
48
templates/search_results.html
Normal 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 %}
|
||||
–
|
||||
{% 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 %}
|
||||
@@ -2,10 +2,14 @@ from django.contrib import admin
|
||||
from training import models
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
# admin.site.register(models.Trainee, VersionAdmin)
|
||||
|
||||
admin.site.register(models.TrainingCategory, VersionAdmin)
|
||||
admin.site.register(models.TrainingItem, VersionAdmin)
|
||||
admin.site.register(models.TrainingLevel, VersionAdmin)
|
||||
admin.site.register(models.TrainingItemQualification, VersionAdmin)
|
||||
admin.site.register(models.TrainingLevelQualification, VersionAdmin)
|
||||
admin.site.register(models.TrainingLevelRequirement, VersionAdmin)
|
||||
|
||||
|
||||
@admin.register(models.TrainingItemQualification)
|
||||
class TrainingItemQualificationAdmin(VersionAdmin):
|
||||
list_display = ['__str__', 'trainee']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from PyRIGS.decorators import user_passes_test_with_403
|
||||
|
||||
|
||||
def has_perm_or_supervisor(perm, login_url=None, oembed_view=None):
|
||||
return user_passes_test_with_403(lambda u: (hasattr(u, 'is_supervisor') and u.is_supervisor) or u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)
|
||||
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))
|
||||
|
||||
@@ -1,36 +1,50 @@
|
||||
import datetime
|
||||
|
||||
from django import forms
|
||||
|
||||
from training import models
|
||||
from RIGS.models import Profile
|
||||
|
||||
|
||||
class QualificationForm(forms.ModelForm):
|
||||
related_models = {
|
||||
'item': models.TrainingItem,
|
||||
'supervisor': models.Trainee
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = models.TrainingItemQualification
|
||||
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):
|
||||
date = self.cleaned_data['date']
|
||||
date = self.cleaned_data.get('date')
|
||||
if date > date.today():
|
||||
raise forms.ValidationError('Qualification date may not be in the future')
|
||||
return date
|
||||
|
||||
def clean_supervisor(self):
|
||||
supervisor = self.cleaned_data['supervisor']
|
||||
if supervisor.pk == self.cleaned_data['trainee'].pk:
|
||||
supervisor = self.cleaned_data.get('supervisor')
|
||||
if supervisor.pk == self.cleaned_data.get('trainee').pk:
|
||||
raise forms.ValidationError('One may not supervise oneself...')
|
||||
if not supervisor.is_supervisor:
|
||||
raise forms.ValidationError('Selected supervisor must actually *be* a 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):
|
||||
related_models = {
|
||||
'item': models.TrainingItem
|
||||
}
|
||||
|
||||
depth = forms.ChoiceField(choices=models.TrainingItemQualification.CHOICES)
|
||||
|
||||
class Meta:
|
||||
@@ -41,3 +55,26 @@ class RequirementForm(forms.ModelForm):
|
||||
pk = kwargs.pop('pk', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
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
|
||||
|
||||
@@ -150,7 +150,7 @@ class Command(BaseCommand):
|
||||
number = previous_item.reference_number + 1
|
||||
else:
|
||||
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)
|
||||
|
||||
def setup_levels(self):
|
||||
|
||||
@@ -55,7 +55,7 @@ class Command(BaseCommand):
|
||||
supervisor = Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor",
|
||||
initials="SV",
|
||||
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.groups.add(Group.objects.get(name="Keyholders"))
|
||||
supervisor.save()
|
||||
|
||||
@@ -104,19 +104,19 @@ class Command(BaseCommand):
|
||||
obj, created = models.TrainingItem.objects.update_or_create(
|
||||
pk=int(child.find('ID').text),
|
||||
reference_number=number,
|
||||
name=name,
|
||||
description=name,
|
||||
category=category,
|
||||
active=active
|
||||
)
|
||||
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:
|
||||
tally[1] += 1
|
||||
else:
|
||||
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):
|
||||
tally = [0, 0, 0]
|
||||
@@ -129,9 +129,9 @@ class Command(BaseCommand):
|
||||
("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT), ]
|
||||
|
||||
for (depth, depth_index) in depths:
|
||||
if child.find('{}_Date'.format(depth)) is not None:
|
||||
if child.find('{}_Assessor_ID'.format(depth)) is None:
|
||||
print("Training Record #{} had no supervisor. Assigning System User.".format(child.find('ID').text))
|
||||
if child.find(f'{depth}_Date') is not None:
|
||||
if child.find(f'{depth}_Assessor_ID') is None:
|
||||
print(f"Training Record #{child.find('ID').text} had no supervisor. Assigning System User.")
|
||||
supervisor = Profile.objects.get(first_name="God")
|
||||
continue
|
||||
supervisor = Profile.objects.get(pk=self.id_map[child.find('{}_Assessor_ID'.format(depth)).text])
|
||||
|
||||
19
training/migrations/0003_trainingcategory_training_level.py
Normal file
19
training/migrations/0003_trainingcategory_training_level.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.11 on 2022-01-25 12:58
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('training', '0002_alter_traininglevel_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trainingcategory',
|
||||
name='training_level',
|
||||
field=models.ForeignKey(help_text='If this is set, any user with the selected level may pass out users within this category, regardless of other status', null=True, on_delete=django.db.models.deletion.CASCADE, to='training.traininglevel'),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
22
training/migrations/0005_auto_20220223_1535.py
Normal file
22
training/migrations/0005_auto_20220223_1535.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -1,36 +1,64 @@
|
||||
from RIGS.models import Profile
|
||||
import datetime
|
||||
from RIGS.models import Profile, filter_by_pk
|
||||
from reversion import revisions as reversion
|
||||
from django.core.exceptions import ValidationError
|
||||
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.utils.safestring import mark_safe
|
||||
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 _
|
||||
|
||||
|
||||
@reversion.register(for_concrete_model=False, fields=[])
|
||||
class TraineeManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
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'])
|
||||
class Trainee(Profile, RevisionMixin):
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
objects = TraineeManager()
|
||||
|
||||
# FIXME use queryset
|
||||
def started_levels(self):
|
||||
return [level for level in TrainingLevel.objects.all() if level.percentage_complete(self) > 0 and level.pk not in self.level_qualifications.values_list('level', flat=True)]
|
||||
|
||||
@property
|
||||
def confirmed_levels(self):
|
||||
return self.level_qualifications.exclude(confirmed_on=None).select_related('level')
|
||||
|
||||
@property
|
||||
def is_technician(self):
|
||||
return self.level_qualifications.exclude(confirmed_on=None).select_related('level') \
|
||||
return self.confirmed_levels \
|
||||
.filter(level__level=TrainingLevel.TECHNICIAN) \
|
||||
.exclude(level__department=TrainingLevel.HAULAGE) \
|
||||
.exclude(level__department__isnull=True).exists()
|
||||
|
||||
@property
|
||||
def is_driver(self):
|
||||
return self.level_qualifications.all().exclude(confirmed_on=None).select_related('level').filter(level__department=TrainingLevel.HAULAGE).exists()
|
||||
return self.confirmed_levels.filter(level__department=TrainingLevel.HAULAGE).exists()
|
||||
|
||||
def get_records_of_depth(self, depth):
|
||||
return self.qualifications_obtained.filter(depth=depth).select_related('item', 'trainee', 'supervisor')
|
||||
|
||||
def is_user_qualified_in(self, item, required_depth):
|
||||
return self.qualifications_obtained.values('item', 'depth').filter(item=item).filter(depth__gte=required_depth).first() is not None # this is a somewhat ghetto version of get_or_none
|
||||
return self.qualifications_obtained.values('item', 'depth').filter(item=item).filter(depth__gte=required_depth).exists()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('trainee_detail', kwargs={'pk': self.pk})
|
||||
@@ -39,39 +67,75 @@ class Trainee(Profile, RevisionMixin):
|
||||
def display_id(self):
|
||||
return str(self)
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return self.first_name + " " + self.last_name
|
||||
|
||||
|
||||
class TrainingCategory(models.Model):
|
||||
reference_number = models.IntegerField(unique=True)
|
||||
name = models.CharField(max_length=50)
|
||||
training_level = models.ForeignKey('TrainingLevel', on_delete=models.CASCADE, null=True, help_text="If this is set, any user with the selected level may pass out users within this category, regardless of other status")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.reference_number}. {self.name}"
|
||||
|
||||
class Meta:
|
||||
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
|
||||
class TrainingItem(models.Model):
|
||||
reference_number = models.IntegerField()
|
||||
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)
|
||||
prerequisites = models.ManyToManyField('self', symmetrical=False, blank=True)
|
||||
|
||||
objects = TrainingItemManager()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return str(self)
|
||||
|
||||
@queryable_property
|
||||
def display_id(self):
|
||||
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):
|
||||
name = f"{self.display_id} {self.name}"
|
||||
name = f"{self.display_id} {self.description}"
|
||||
if not self.active:
|
||||
name += " (inactive)"
|
||||
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
|
||||
def user_has_qualification(item, user, depth):
|
||||
return user.qualifications_obtained.only('item', 'depth').filter(item=item, depth__gte=depth).exists()
|
||||
@@ -81,6 +145,21 @@ class TrainingItem(models.Model):
|
||||
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
|
||||
class TrainingItemQualification(models.Model, RevisionMixin):
|
||||
STARTED = 0
|
||||
@@ -100,12 +179,29 @@ class TrainingItemQualification(models.Model, RevisionMixin):
|
||||
notes = models.TextField(blank=True)
|
||||
# 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):
|
||||
return f"{self.get_depth_display()} in {self.item} on {self.date.strftime('%b %d %Y')}"
|
||||
|
||||
@property
|
||||
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
|
||||
def get_colour_from_depth(cls, depth):
|
||||
@@ -117,7 +213,7 @@ class TrainingItemQualification(models.Model, RevisionMixin):
|
||||
return "info"
|
||||
|
||||
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:
|
||||
unique_together = ["trainee", "item", "depth"]
|
||||
@@ -214,9 +310,9 @@ class TrainingLevel(models.Model, RevisionMixin):
|
||||
if self.level == self.TA:
|
||||
return self.get_level_display()
|
||||
else:
|
||||
return "{} Common Competencies".format(self.get_level_display())
|
||||
return f"{self.get_level_display()} Common Competencies"
|
||||
else:
|
||||
return "{} {}".format(self.get_department_display(), self.get_level_display())
|
||||
return f"{self.get_department_display()} {self.get_level_display()}"
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
@@ -231,7 +327,7 @@ class TrainingLevel(models.Model, RevisionMixin):
|
||||
icon = f"<span class='fas fa-{self.icon}'></span>"
|
||||
else:
|
||||
icon = "".join([w[0] for w in str(self).split()])
|
||||
return mark_safe("<span class='badge badge-{} badge-pill' data-toggle='tooltip' title='{}'>{}</span>".format(self.department_colour, str(self), icon))
|
||||
return mark_safe(f"<span class='badge badge-{self.department_colour} badge-pill' data-toggle='tooltip' title='{str(self)}'>{icon}</span>")
|
||||
|
||||
|
||||
@reversion.register
|
||||
@@ -257,8 +353,6 @@ class TrainingLevelQualification(models.Model, RevisionMixin):
|
||||
confirmed_on = models.DateTimeField(null=True)
|
||||
confirmed_by = models.ForeignKey('Trainee', related_name='confirmer', on_delete=models.CASCADE, null=True)
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
@property
|
||||
def get_icon(self):
|
||||
return self.level.get_icon
|
||||
@@ -273,6 +367,13 @@ class TrainingLevelQualification(models.Model, RevisionMixin):
|
||||
return f"{self.trainee} is qualified in the {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:
|
||||
unique_together = ["trainee", "level"]
|
||||
ordering = ['-confirmed_on']
|
||||
|
||||
@@ -32,11 +32,7 @@
|
||||
{% endif %}
|
||||
<form id="requirement-form" action="{{ form.action|default:request.path }}" method="post">{% csrf_token %}
|
||||
{% render_field form.level|attr:'hidden' value=form.level.initial %}
|
||||
<div class="form-group form-row">
|
||||
<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>
|
||||
{% include 'partials/item_field.html' %}
|
||||
<div class="form-group form-row">
|
||||
<label for="depth" class="col-sm-2 col-form-label">Depth</label>
|
||||
{% render_field form.depth|add_class:'form-control col-sm'|attr:'required' %}
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
<a class="dropdown-item" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> Item List</a>
|
||||
</div>
|
||||
</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>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -28,25 +28,13 @@
|
||||
{% include 'form_errors.html' %}
|
||||
{% csrf_token %}
|
||||
{% render_field form.trainee|attr:'hidden' value=form.trainee.initial %}
|
||||
<div class="form-group form-row">
|
||||
<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>
|
||||
{% include 'partials/item_field.html' %}
|
||||
<div class="form-group form-row">
|
||||
<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 class="form-group form-row">
|
||||
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
|
||||
<select name="supervisor" id="supervisor_id" class="form-control selectpicker custom-select col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials&filters=is_supervisor" required>
|
||||
{% if object.supervisor %}
|
||||
<option value="{{object.supervisor.pk}}" selected>{{object.supervisor}}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
{% include 'partials/supervisor_field.html' %}
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<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 %}
|
||||
{% endwith %}
|
||||
</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 class="form-group form-row">
|
||||
<label for="id_notes" class="col-sm-2 col-form-label">Notes</label>
|
||||
|
||||
@@ -13,11 +13,19 @@
|
||||
<div class="card-body">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for item in category.items.all %}
|
||||
{% if item.active %}
|
||||
<li class="list-group-item">{{ item }}</li>
|
||||
{% elif request.user.is_superuser %}
|
||||
<li class="list-group-item text-warning">{{ item }}</li>
|
||||
{% endif %}
|
||||
<li class="list-group-item {% if not item.active%}text-warning{%endif%}">{{ item }}
|
||||
{% if item.prerequisites.exists %}
|
||||
<div class="ml-3 font-italic">
|
||||
<p class="text-info mb-0">Passed Out Prerequisites:</p>
|
||||
<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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
49
training/templates/item_qualification_list.html
Normal file
49
training/templates/item_qualification_list.html
Normal 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 %}
|
||||
@@ -44,7 +44,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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">
|
||||
<a type="button" class="btn btn-success mb-3" href="{% url 'add_requirement' pk=object.pk %}" id="requirement_button">
|
||||
<span class="fas fa-plus"></span> Add New Requirement
|
||||
@@ -79,9 +79,9 @@
|
||||
{% endfor %}
|
||||
<tr><th colspan="3" class="text-center">{{object}}</th></tr>
|
||||
<tr>
|
||||
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.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 %}<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 %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -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">
|
||||
<span class="fas fa-plus"></span> Add New Training Record
|
||||
</a>
|
||||
|
||||
8
training/templates/partials/item_field.html
Normal file
8
training/templates/partials/item_field.html
Normal 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>
|
||||
6
training/templates/partials/supervisor_field.html
Normal file
6
training/templates/partials/supervisor_field.html
Normal 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>
|
||||
@@ -1,7 +1,8 @@
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% extends 'base_training.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load button from filters %}
|
||||
{% load colour_from_depth from tags %}
|
||||
|
||||
{% block css %}
|
||||
{{ block.super }}
|
||||
@@ -23,27 +24,33 @@
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<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>
|
||||
<div class="form-group">
|
||||
<label for="selectpicker">Select Supervisor</label>
|
||||
<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 class="form-group row" id="supervisor_group">
|
||||
{% include 'partials/supervisor_field.html' %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="selectpicker">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">
|
||||
<div class="form-group row" id="trainees_group">
|
||||
<label for="trainees_id" class="col-sm-2">Select Attendees</label>
|
||||
<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>
|
||||
</div>
|
||||
<h3>Training Items</h3>
|
||||
<div class="row">
|
||||
{% for depth in depths %}
|
||||
<div class="col">
|
||||
<h4>{{ depth.1 }}</h4>
|
||||
<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>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for depth in depths %}
|
||||
<div class="form-group row" id="{{depth.0}}">
|
||||
<label for="selectpicker" class="col-sm-2 text-{% colour_from_depth depth.0 %} py-1">{{ depth.1 }} Items</label>
|
||||
<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>
|
||||
</div>
|
||||
{% 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 class="col-sm-12 text-right my-3">
|
||||
{% button 'submit' %}
|
||||
|
||||
@@ -5,6 +5,41 @@
|
||||
{% load linkornone from filters %}
|
||||
{% load button from filters %}
|
||||
{% 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 %}
|
||||
<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>Supervisor</th>
|
||||
<th>Notes</th>
|
||||
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
|
||||
{% if request.user.is_supervisor %}
|
||||
<th></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
@@ -32,8 +67,8 @@
|
||||
<td>{{ object.date }}</td>
|
||||
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
|
||||
<td>{{ object.notes }}</td>
|
||||
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
|
||||
<td>{% button 'edit' 'edit_qualification' trainee.pk %}</td>
|
||||
{% if request.user.is_supervisor %}
|
||||
<td>{% button 'edit' 'edit_qualification' object.pk id="edit" %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
{% load button from filters %}
|
||||
{% load get_levels_of_depth from tags %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static "js/tooltip.js" %}"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
@@ -16,7 +19,17 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'partials/list_search.html' %}
|
||||
<form method="GET" class="ml-auto w-25 d-flex flex-column justify-content-end">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<input type="search" name="q" placeholder="Search" value="{{ request.GET.q }}"
|
||||
class="form-control" id="id_search_text"/>
|
||||
<span class="input-group-append">{% button 'search' id="id_search" %}</span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mt-2 {% if request.GET.is_supervisor %}active{%endif%}" data-toggle="button" aria-pressed="{% if request.GET.is_supervisor %}true{%endif%}" name="is_supervisor" value="{% if request.GET.is_supervisor %}{% else %}True{% endif %}">
|
||||
Only Supervisors
|
||||
</button>
|
||||
</form>
|
||||
<div class="row pt-2">
|
||||
<div class="col">
|
||||
<div class="table-responsive">
|
||||
@@ -27,7 +40,7 @@
|
||||
<th>Van Driver?</th>
|
||||
<th>Technician?</th>
|
||||
<th>Supervisor?</th>
|
||||
<th>Qualification Count</th>
|
||||
<th>Competency Assessed Count</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -24,7 +24,16 @@ def supervisor(db):
|
||||
@pytest.fixture
|
||||
def training_item(db):
|
||||
training_category = models.TrainingCategory.objects.create(reference_number=1, name="The Basics")
|
||||
training_item = models.TrainingItem.objects.create(category=training_category, reference_number=1, name="How Not To Die")
|
||||
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
|
||||
training_category.delete()
|
||||
training_item.delete()
|
||||
|
||||
@@ -40,3 +40,42 @@ class AddQualification(FormPage):
|
||||
@property
|
||||
def success(self):
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user