mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-22 06:18:24 +00:00
Compare commits
10 Commits
63a2f6d47b
...
subhire
| Author | SHA1 | Date | |
|---|---|---|---|
| a4f240e581 | |||
| 2e4b84c94e | |||
| 8863d86ed0 | |||
|
|
6550ed2318 | ||
|
|
c9759a6339 | ||
|
|
b637c4e452 | ||
|
|
8986b94b07 | ||
| 87f2de46a1 | |||
| 1615e27767 | |||
| 773f55ac84 |
294
Pipfile.lock
generated
294
Pipfile.lock
generated
@@ -233,11 +233,11 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:18ba8efa36b69cfcd4b670d0fa187c6fe7506596f0ababe580e16909bcdec121",
|
||||
"sha256:3adc285124244724a394fa9b9839cc8cd116faf7d159554c43ecdaa8cdf0b94d"
|
||||
"sha256:08208dfe892eb64fff073ca743b3b952311104f939e7f6dae954fe72dcc533ba",
|
||||
"sha256:4d492d9024c7b3dfababf49f94511ab6a58e2c9c3c7207786f1ba4eb77750706"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.16"
|
||||
"version": "==3.2.18"
|
||||
},
|
||||
"django-debug-toolbar": {
|
||||
"hashes": [
|
||||
@@ -304,11 +304,11 @@
|
||||
},
|
||||
"django-registration-redux": {
|
||||
"hashes": [
|
||||
"sha256:5079dd36980cc0faddf91a6e991129680410611b1059d8154d064cc0146744b2",
|
||||
"sha256:88eb98530d98a7e3451bf728c0a5f6fe7ea2f45c65ef18f619ef37b940c854f5"
|
||||
"sha256:2213bbe8732be72724034f4146f0255a7bd666eb5a5e1b2d8d8aa633fe8af894",
|
||||
"sha256:56fbc7b01a7f0f48812fe4d4e0729d2dac916e16f8aaed36b3f10129f2df9d0f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.11"
|
||||
"version": "==2.12"
|
||||
},
|
||||
"django-reversion": {
|
||||
"hashes": [
|
||||
@@ -367,11 +367,11 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b",
|
||||
"sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313"
|
||||
"sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad",
|
||||
"sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.1.0"
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
@@ -392,6 +392,7 @@
|
||||
"sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947",
|
||||
"sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1",
|
||||
"sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd",
|
||||
"sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3",
|
||||
"sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92",
|
||||
"sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3",
|
||||
"sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457",
|
||||
@@ -417,6 +418,7 @@
|
||||
"sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de",
|
||||
"sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f",
|
||||
"sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b",
|
||||
"sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5",
|
||||
"sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7",
|
||||
"sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a",
|
||||
"sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c",
|
||||
@@ -464,69 +466,79 @@
|
||||
},
|
||||
"msgpack": {
|
||||
"hashes": [
|
||||
"sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467",
|
||||
"sha256:0a68d3ac0104e2d3510de90a1091720157c319ceeb90d74f7b5295a6bee51bae",
|
||||
"sha256:0df96d6eaf45ceca04b3f3b4b111b86b33785683d682c655063ef8057d61fd92",
|
||||
"sha256:0dfe3947db5fb9ce52aaea6ca28112a170db9eae75adf9339a1aec434dc954ef",
|
||||
"sha256:0e3590f9fb9f7fbc36df366267870e77269c03172d086fa76bb4eba8b2b46624",
|
||||
"sha256:11184bc7e56fd74c00ead4f9cc9a3091d62ecb96e97653add7a879a14b003227",
|
||||
"sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88",
|
||||
"sha256:1276e8f34e139aeff1c77a3cefb295598b504ac5314d32c8c3d54d24fadb94c9",
|
||||
"sha256:1576bd97527a93c44fa856770197dec00d223b0b9f36ef03f65bac60197cedf8",
|
||||
"sha256:1e91d641d2bfe91ba4c52039adc5bccf27c335356055825c7f88742c8bb900dd",
|
||||
"sha256:26b8feaca40a90cbe031b03d82b2898bf560027160d3eae1423f4a67654ec5d6",
|
||||
"sha256:2999623886c5c02deefe156e8f869c3b0aaeba14bfc50aa2486a0415178fce55",
|
||||
"sha256:2a2df1b55a78eb5f5b7d2a4bb221cd8363913830145fad05374a80bf0877cb1e",
|
||||
"sha256:2bb8cdf50dd623392fa75525cce44a65a12a00c98e1e37bf0fb08ddce2ff60d2",
|
||||
"sha256:2cc5ca2712ac0003bcb625c96368fd08a0f86bbc1a5578802512d87bc592fe44",
|
||||
"sha256:35bc0faa494b0f1d851fd29129b2575b2e26d41d177caacd4206d81502d4c6a6",
|
||||
"sha256:3c11a48cf5e59026ad7cb0dc29e29a01b5a66a3e333dc11c04f7e991fc5510a9",
|
||||
"sha256:449e57cc1ff18d3b444eb554e44613cffcccb32805d16726a5494038c3b93dab",
|
||||
"sha256:462497af5fd4e0edbb1559c352ad84f6c577ffbbb708566a0abaaa84acd9f3ae",
|
||||
"sha256:4733359808c56d5d7756628736061c432ded018e7a1dff2d35a02439043321aa",
|
||||
"sha256:48f5d88c99f64c456413d74a975bd605a9b0526293218a3b77220a2c15458ba9",
|
||||
"sha256:49565b0e3d7896d9ea71d9095df15b7f75a035c49be733051c34762ca95bbf7e",
|
||||
"sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250",
|
||||
"sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce",
|
||||
"sha256:4dea20515f660aa6b7e964433b1808d098dcfcabbebeaaad240d11f909298075",
|
||||
"sha256:545e3cf0cf74f3e48b470f68ed19551ae6f9722814ea969305794645da091236",
|
||||
"sha256:63e29d6e8c9ca22b21846234913c3466b7e4ee6e422f205a2988083de3b08cae",
|
||||
"sha256:6916c78f33602ecf0509cc40379271ba0f9ab572b066bd4bdafd7434dee4bc6e",
|
||||
"sha256:6a4192b1ab40f8dca3f2877b70e63799d95c62c068c84dc028b40a6cb03ccd0f",
|
||||
"sha256:6c9566f2c39ccced0a38d37c26cc3570983b97833c365a6044edef3574a00c08",
|
||||
"sha256:76ee788122de3a68a02ed6f3a16bbcd97bc7c2e39bd4d94be2f1821e7c4a64e6",
|
||||
"sha256:7760f85956c415578c17edb39eed99f9181a48375b0d4a94076d84148cf67b2d",
|
||||
"sha256:77ccd2af37f3db0ea59fb280fa2165bf1b096510ba9fe0cc2bf8fa92a22fdb43",
|
||||
"sha256:81fc7ba725464651190b196f3cd848e8553d4d510114a954681fd0b9c479d7e1",
|
||||
"sha256:85f279d88d8e833ec015650fd15ae5eddce0791e1e8a59165318f371158efec6",
|
||||
"sha256:9667bdfdf523c40d2511f0e98a6c9d3603be6b371ae9a238b7ef2dc4e7a427b0",
|
||||
"sha256:a75dfb03f8b06f4ab093dafe3ddcc2d633259e6c3f74bb1b01996f5d8aa5868c",
|
||||
"sha256:ac5bd7901487c4a1dd51a8c58f2632b15d838d07ceedaa5e4c080f7190925bff",
|
||||
"sha256:aca0f1644d6b5a73eb3e74d4d64d5d8c6c3d577e753a04c9e9c87d07692c58db",
|
||||
"sha256:b17be2478b622939e39b816e0aa8242611cc8d3583d1cd8ec31b249f04623243",
|
||||
"sha256:c1683841cd4fa45ac427c18854c3ec3cd9b681694caf5bff04edb9387602d661",
|
||||
"sha256:c23080fdeec4716aede32b4e0ef7e213c7b1093eede9ee010949f2a418ced6ba",
|
||||
"sha256:d5b5b962221fa2c5d3a7f8133f9abffc114fe218eb4365e40f17732ade576c8e",
|
||||
"sha256:d603de2b8d2ea3f3bcb2efe286849aa7a81531abc52d8454da12f46235092bcb",
|
||||
"sha256:e83f80a7fec1a62cf4e6c9a660e39c7f878f603737a0cdac8c13131d11d97f52",
|
||||
"sha256:eb514ad14edf07a1dbe63761fd30f89ae79b42625731e1ccf5e1f1092950eaa6",
|
||||
"sha256:eba96145051ccec0ec86611fe9cf693ce55f2a3ce89c06ed307de0e085730ec1",
|
||||
"sha256:ed6f7b854a823ea44cf94919ba3f727e230da29feb4a99711433f25800cf747f",
|
||||
"sha256:f0029245c51fd9473dc1aede1160b0a29f4a912e6b1dd353fa6d317085b219da",
|
||||
"sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f",
|
||||
"sha256:fb62ea4b62bfcb0b380d5680f9a4b3f9a2d166d9394e9bbd9666c0ee09a3645c",
|
||||
"sha256:fcb8a47f43acc113e24e910399376f7277cf8508b27e5b88499f053de6b115a8"
|
||||
"sha256:04366c754ac3bfecf589ea0578599f0c26a3b6558e44cc94d5078bedc67ebfb8",
|
||||
"sha256:0a8fed756d52f8e8e45e1cb1eac83d96349d563997eed417ffd80eaac426e49e",
|
||||
"sha256:12a5f5e5279a37909ed41dab91b20cc41d6423ddf944141e2d2cf41517f3b119",
|
||||
"sha256:13eb94148866fe4f6f93a5253bab1b12b3976c1c859b6b11f3ca7be581f20c12",
|
||||
"sha256:1c19803007800ed7ff492b21dc84872ea2ef7577800c97939a50f1ecef099fb2",
|
||||
"sha256:1e600cb89997f4cda23f93b29c9ad4ae09884573ec87476d46df264b86a92cc3",
|
||||
"sha256:20a26548e6fbd0998846d51835d79e2c9a1542d11228872baec61baf87264e92",
|
||||
"sha256:2371e14ff3b17f5774f50602fb139e1df39ee3ca44eb3ae82683ac9b1db5e4ed",
|
||||
"sha256:290f9a656d34aa20cb672ee11ebd5c6647d08419c88614823562997ecb566c16",
|
||||
"sha256:2cd4e24daff07eedf168f6e7db1b2c0831bed748d8b7254053d4b2334c206ed5",
|
||||
"sha256:318956e96edd3c02a183e96af10f471c1fa18c29add5c317871de3532302609c",
|
||||
"sha256:31b4112b43af2a78d005c9192d2a5f0cec62c6a731ca93e77a0d3979da585d9b",
|
||||
"sha256:3729619996e9a0db56d5dc00de1d72e401aee6695d59cbfb62815a5605c66cdb",
|
||||
"sha256:42418455bb0aba4591f8f90ac4b783834e6cb0d880c0b92a71423bf59ccc38b9",
|
||||
"sha256:44b913a7b9a4a7726bb004aed024670682669a15f77dc2ad8d87a179d9e26e94",
|
||||
"sha256:4655afa670c7f05bb560a00640d725629c3f2d4f36267c0d3b9645bdecee9b74",
|
||||
"sha256:469c8f3d9458b0d4fc2fa691b914eced40465a95a623e87f75bc40a74e31dfea",
|
||||
"sha256:47d9123a621b18b4c7a63739acbb56de4f89b92b3e493cb165593474cff3c60f",
|
||||
"sha256:4df078e1a38a26d9f8addabf0df24fcf0abc2161bb7b43b2cfdd178d8a127a12",
|
||||
"sha256:4e4d1c09fe6a3104a001e6197e46e34237f1858ca470b97a87cb7d29fdc359fe",
|
||||
"sha256:512df5ec1f97ae44c3307049be05cc901b255b297aae5c88508e3058a3874270",
|
||||
"sha256:53cbf882e4b11aba6cdeec41abe576d4cc7dbf22e7a431f95d8127b32768709f",
|
||||
"sha256:556c17b6bbfeb5e31e52baa3e39d04e863dabd98b459538f73aa958bc4bc4043",
|
||||
"sha256:5629026acea9c4e2c2e684de7b313ef82e516e2e88049b3eefcc6316da43ce40",
|
||||
"sha256:5d73c893dd03129c67cb2bea65733bdf1c52cf78e51fb599b81146c1ae8a51f0",
|
||||
"sha256:61b202019a014ad3e7e5953430fe5838125196ad4fb27c15e521b22724add939",
|
||||
"sha256:631bdeacad61e2bdee929835622025131d9971bd9aed4cbad9e44a46caa42069",
|
||||
"sha256:6322b441d0ddab56ca5e79904dd2f79494d33636fdf53be0d01a23ebb56d2613",
|
||||
"sha256:669450ebc749e8ac27d07b750643e8e2ff8976ba95ebcc2e12eb00999f3cf500",
|
||||
"sha256:68726d2404250b6b3b3e63df7e2c4243d46846c630d356a8d129f4aec72ced56",
|
||||
"sha256:6e733b50bbcedd04e82922c80e7f045530f8bd19ce004c006316eef511b623bb",
|
||||
"sha256:7d18a179e7e26da21f85e3b807f317316da28c62f4213e6864191fa9aabe482a",
|
||||
"sha256:90703d9c8eae435fcb2f84a545183a23670b5662e6e9e7ee6dfdcd8f69a373f5",
|
||||
"sha256:969e6ee8f82b7ff0f831b1d3ceb84eafe9b58f5300cc024a96041c7a8c20d559",
|
||||
"sha256:9c57c6730e94801b341c87d56edbf923165dda6d000f2c1c1d5fb74f257cd802",
|
||||
"sha256:a34b0dfb71eb8807cf082d59c0666715df51fc49e734c0f171df5bbb86e02570",
|
||||
"sha256:a43019ea96dc4632dc2626c76b5413e5a4e1294781e9f5241435076897140594",
|
||||
"sha256:aa9a797de3c755e9bb47a8c6f592b4c0dbb296cee584d3cd0e36b53be0c31e80",
|
||||
"sha256:bbe299a9e7b7d24e688f1e4dac09eb5b01d8eb8eaca944aae5d8f8aef6c73c37",
|
||||
"sha256:bea6b16a3537ad712bc9b7189970bdf28c56a0cec0a0b46a9f3db3ac0a853335",
|
||||
"sha256:c65fd6feb88efe81765b51ad1150b9db682794fb2ab6ddf0e77a6fb4750eca92",
|
||||
"sha256:c81463959da83fc74ff9bfba7d0a5c6d21b44e799f78c28fe57c75b300160f5d",
|
||||
"sha256:cb4a0545afb15189601c1e4e7cf82765456ef45985dc293297c854c4045afe31",
|
||||
"sha256:cbd3af673fa93706c59e66519f6110d4a317892ddeae7a9718dde3e0e9a9a6df",
|
||||
"sha256:ceed735d624af7e1834db1995ad293389e66306025c7c791db2ac42e006dbd25",
|
||||
"sha256:cf7aec2bf2ff7bf7e8a07de04b593c1076f51941a28dd23d2af5b07c23f60ee9",
|
||||
"sha256:d1960d6c57e30f60c132e2649e5fefb0bd29b1b55c707c0c5ecfa7f08def82d1",
|
||||
"sha256:d6788d652256e38b19f7578eb7dd4f96de10fe20546ebf5519bef22aa18c6109",
|
||||
"sha256:d6a73d8f30e06562efc35f5f9699221eb240b18691807b32ef29bae7f66e0da1",
|
||||
"sha256:d896df74ce25ff2e0b2d5bdd0344eff01e05814cd9b168f9321bd459f476981e",
|
||||
"sha256:d98a89e53df1540f3f465a510b511e97d21e1b1777b9f5e030184e1cc68d1072",
|
||||
"sha256:da5db8a4d8b532bbe1e4aa1fabfb21f49f30ee7db49d4885c448c7a9ea032138",
|
||||
"sha256:dfdacd510bc0f73125aa3e496243ebf768f0eb6478243867607f3b247451fb6f",
|
||||
"sha256:dff7f7c68435a7b7b570b75f8c71ab986681e04767e10eefc178105c698495b1",
|
||||
"sha256:e4f6a2b90746c8bca7f3742e38b8ce8fc6ad4a0b63e938c135ea0d578857aff8",
|
||||
"sha256:e63c6d85f23243d9ed15aaff826a2330a8be33d09b8d808602dbe8d2b596a89f",
|
||||
"sha256:e8667a1ecb0a70d612992516a9483dce35d5e452430832cca4f01899e8da6da7",
|
||||
"sha256:f25c3553c5b7b07ecff4a3b88024477a08b568edf9566cccb662b31803649919",
|
||||
"sha256:f2c3692b13e8c26aa54a87318861d80b1b0d2adbfa3fb81b05d54a6e56083958",
|
||||
"sha256:f9b6d3689fac019f10091cdaf5ff95458a8ccdadfd5598bb0be92cf888feeace",
|
||||
"sha256:fb0db88c3db68a938f4f930c34570b9b5b050e43ac611bcfd8506303d0ff2d4f",
|
||||
"sha256:ff54f758e67d2ed70121b99f35929801a02086bfd544dfc40a9cee59a3f04c8d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.4"
|
||||
"version": "==1.0.5rc1"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
|
||||
"sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"
|
||||
"sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
|
||||
"sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==22.0"
|
||||
"version": "==23.0"
|
||||
},
|
||||
"pep517": {
|
||||
"hashes": [
|
||||
@@ -538,42 +550,42 @@
|
||||
},
|
||||
"pikepdf": {
|
||||
"hashes": [
|
||||
"sha256:005e6908ab572cde909873ca5177013c066167567a3ae53520e0fe5b1197f8f0",
|
||||
"sha256:1495449007b34985409650f1f99bd9d469a1c1d90d43dd576aee2ad60c7788ab",
|
||||
"sha256:28803e1b730ffa65e4668baefc8a602c2d6f620f1caea6cb25672b15b1e9473b",
|
||||
"sha256:37370179e4f742623f08c6d7d70322ee167be8605053eb6f23ad6135fecea9db",
|
||||
"sha256:38107a9048cd6a6b0146b7227e6acef11a0c6975f61fe9a14020e226fb88ec27",
|
||||
"sha256:40a649c71ec36795ee9d487024fbcc2f483b868ef37a5398dc49a3c6823fbbcf",
|
||||
"sha256:4acbf4e711ce93e8130f38ede1e497d5fe35b4c1eaa8628ffa1ca21284b66d4e",
|
||||
"sha256:4c331fc1bb8ffdb5f04502610d1f010fffceea122d9f8429c35e4e0207afc36b",
|
||||
"sha256:78c5c9476edf890e4bb078c446a86f676ff78f49fc33d2f10cac90affe84a1b3",
|
||||
"sha256:799e981f03718a9d9a7db1467121e987f21582925c049287572c9f982f8d77ea",
|
||||
"sha256:7b59f96e8ce9faa35e1ebce662cf33ad33a3351bd6026136c849170876139ca0",
|
||||
"sha256:7e461cc756765544b85cec43b2af99609732d27680bb96e6fc9c81b2d27944cc",
|
||||
"sha256:80f31a4565e17f5f65dc38f1a5b41d469cc7ce1449b32c2a1fb36cccc746a99a",
|
||||
"sha256:87c7ca31ab818121edd4d9f12d81c1b56f61eec56ada1644d68ec8b10bf27622",
|
||||
"sha256:87d04107a048537ca132b88e12b4a9d1236362ccd0d3351b80da42f31fcfcda5",
|
||||
"sha256:897c0bc83c03b8e65153afb482a790d4b0e7adf603bc11de6293aff3c123a457",
|
||||
"sha256:8c8a622c2bd82b4e86fd174f5739abd95872733d0d50760d664a5abcc5b5b533",
|
||||
"sha256:96eb80ce4f9661987033b73bf0106cf7cc804745e8381436cda7f5cc7224b8b5",
|
||||
"sha256:980d49f443f872728cfab76d56c5a330b049e58c5e47354281fb410dde81c1ad",
|
||||
"sha256:9e25cb1efb4242080f8bed1d51f7541a39b8124df8342ef01970e8fa69a7594b",
|
||||
"sha256:a43f8d8d389d7b5999a5ef76623fa88ec54791ad746d77409ec6956ce8de6a34",
|
||||
"sha256:a68da2e0be0264442eead7fa1e63aab63a491c80ba9c5eee527ff99ec5883bcc",
|
||||
"sha256:ac0ef9a15d125157955f5001d3eaf7dc62f37863d2172e12434b9efad4b89bf0",
|
||||
"sha256:acc9f49f3b45f158295c7539268d88d00b41f660d4aa42dd507a1469ce81f0eb",
|
||||
"sha256:b4b05498f2d80478302cdece2385ff4b4ce62874f883622a52f0efee8cfdb68e",
|
||||
"sha256:bb7a7da762976e43ff96cdb3056997280ba47b45ac319cce89a709f34c24ed58",
|
||||
"sha256:bda12c977eaee121c34c0d3869a0cd87c95c42ee3c5b9e1c5a66b88c8beb3886",
|
||||
"sha256:c42eef99232f6aef231466bf9ebeddc47f81784e7e04df25ba6dce482f45ee0b",
|
||||
"sha256:c80d5455965171ad60db38e1a10e063a03e40e4549f8d616c186bfdfcc40475a",
|
||||
"sha256:de30dbf6e847bbac7df424d98e550a97ab0cea9b327eddc95d4c26350b20022b",
|
||||
"sha256:e2252fb75f6b8b6441b880505e8f2484492821b8f3c8a94e628b8104bbeeb66f",
|
||||
"sha256:ecd7ada50e556d6364553f80106e05a3fc11dcba5e45337684e85108d18e8560",
|
||||
"sha256:fffdfd4535952756b1f078fa98be5c3da06728f2afc9dff39aed56419a5d1bf6"
|
||||
"sha256:0a0914d482aa1b80584659c44aef1b2770b473a504cedc209fa6db3f24575ef6",
|
||||
"sha256:1b8dfdc2184aca33e271b104e0ec468e52ac6591ab51bcd32c2e53bc8cdeeffe",
|
||||
"sha256:1faed2d2553aafb6f4969f0a970958d1847869631eb1c82f5a91ef636817b93a",
|
||||
"sha256:260efb3c6aa44c013da2278872593bc4712facd5b766de2b2d88c53c5f524449",
|
||||
"sha256:3af2fd5762e222bf5133acec4f7d56719dda4a9b7dd468eb1c37a10055ff64b2",
|
||||
"sha256:3e1490beb13b2d1a509aeb98fb0669ab7dea4035abd1df0e12085393f556654a",
|
||||
"sha256:422cb31ed4b489b9e18f4a803fab7c6ea10ef6916960a5d8b5e531c5af3bfbca",
|
||||
"sha256:4290059bdf8d05cf3a7ba185d64b5756c745f178fc102aec41bbcd4a057e02f8",
|
||||
"sha256:5aca06b88b2d53122baaf3009bcfaec291b3c408846e401cddf8b2e89a0e0fe6",
|
||||
"sha256:7eef9fe4d06cb01482486561292b3c3675d7506328f990cb60b26994dd7ac1d8",
|
||||
"sha256:80d4eb3624980c1292d7e2db66c569f146012e86004b8739a3580346ee8f69a9",
|
||||
"sha256:81caa67a08fdf683c521497fafd48d9b7bdf02549625329d8a1bb8ce706ef362",
|
||||
"sha256:910a45cc6506dd899032638c3775f708278d99ccc9c3681fb75a57b55051d262",
|
||||
"sha256:93cd0357140dfa79e16c1d9249775d11eebb392665fcdb1528684aae71966b4e",
|
||||
"sha256:93d98f460eb209b89ce855a5defb059ca82326042ee52dcb692a05e1c1a24bef",
|
||||
"sha256:9411824a7aa477fcad209e6e01cf23f0ecbd6833805812eca2026e372c724096",
|
||||
"sha256:a32215111f6713c934b9ed9a6fd686940559953539e81f70bfb860efbddfe3c3",
|
||||
"sha256:a59559a1e480f4a7229f600d6fce22b1d32729df8552099542e37a95c02a572f",
|
||||
"sha256:ab07529096da5ea410dea81add8d724ac91f093b982b86f503771bfe8864e48c",
|
||||
"sha256:b0f1ca24118517970cfc78f902ded681c2e399dfb21ff04745143b4156dd551e",
|
||||
"sha256:b7711e2c0ff1ad265f5c0497f519584c029c9906d72ad78e89e260c57964a5c0",
|
||||
"sha256:b860c44503b6bb237b97c2ae06d47dc645140649d61d2f5dd5276438bc60e2e0",
|
||||
"sha256:b90bb4e77f9ea8a21d94c5d25e1b416f08a668377ea48edf8808be49f09f906a",
|
||||
"sha256:ba75e70c932830fb7253b343f43f0ce03317661971cd3df03fc27c7cdb06992e",
|
||||
"sha256:bf9129507b7258dda27845e1ee6c7c4121674a04c0a1a0ac1115c19e5b4a2edf",
|
||||
"sha256:cf912ab70313fd0f23a32811f8c3ea815a716ecfd6ee84a564c833d0c06f5483",
|
||||
"sha256:dec854f908973c5c3760d246539c58c03b7b701a2bed45173f9a4e4d766d3eab",
|
||||
"sha256:e5bebaca43757c9c357637954b8e49c9221c21a40260394ec4c4fbafada5ee87",
|
||||
"sha256:e900d2314a1019c98c9d3d50445af475ce2f16e8a54e44875201869f8561a3b2",
|
||||
"sha256:eb310e903b9a172de352446458390ccce31a2bb19a387f63d37b135cb4cca3f1",
|
||||
"sha256:edea85799240a3b7534650b275bc7e87519ddf0f47ccf9bdad09b22381da5442",
|
||||
"sha256:f7f0bfa897ecabbee3f7dc2ac10a8dd954c42a6f858fbef13019d0cef1b60127",
|
||||
"sha256:fcfd391b3a255b460a9040ca8e47dd6f36c6ea43d2b61cd00b5ecb06000a6b8f"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==6.2.6"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==7.1.1"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
@@ -861,19 +873,19 @@
|
||||
},
|
||||
"sentry-sdk": {
|
||||
"hashes": [
|
||||
"sha256:3c9bc64025976842c1103cd75d45cff94a7c0cc48fa07770d07a09d2ab8dac30",
|
||||
"sha256:dc0fe6ef2f77a3853b399c75c97d87be7666098817c1c314f8fcdf68a6865914"
|
||||
"sha256:69ecbb2e1ff4db02a06c4f20f6f69cb5dfe3ebfbc06d023e40d77cf78e9c37e7",
|
||||
"sha256:7ad4d37dd093f4a7cb5ad804c6efe9e8fab8873f7ffc06042dc3f3fd700a93ec"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.12.0"
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54",
|
||||
"sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"
|
||||
"sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330",
|
||||
"sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==65.6.3"
|
||||
"version": "==67.4.0"
|
||||
},
|
||||
"simplejson": {
|
||||
"hashes": [
|
||||
@@ -952,11 +964,11 @@
|
||||
},
|
||||
"soupsieve": {
|
||||
"hashes": [
|
||||
"sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759",
|
||||
"sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"
|
||||
"sha256:49e5368c2cda80ee7e84da9dbe3e110b70a4575f196efb74e51b94549d921955",
|
||||
"sha256:e28dba9ca6c7c00173e34e4ba57448f0688bb681b7c5e8bf4971daafc093d69a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.3.2.post1"
|
||||
"version": "==2.4"
|
||||
},
|
||||
"sqlparse": {
|
||||
"hashes": [
|
||||
@@ -1015,11 +1027,11 @@
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc",
|
||||
"sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"
|
||||
"sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72",
|
||||
"sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.26.13"
|
||||
"version": "==1.26.14"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
@@ -1253,11 +1265,11 @@
|
||||
"develop": {
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6",
|
||||
"sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"
|
||||
"sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836",
|
||||
"sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==22.1.0"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==22.2.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
@@ -1356,11 +1368,11 @@
|
||||
},
|
||||
"exceptiongroup": {
|
||||
"hashes": [
|
||||
"sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828",
|
||||
"sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"
|
||||
"sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e",
|
||||
"sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==1.0.4"
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"execnet": {
|
||||
"hashes": [
|
||||
@@ -1380,18 +1392,19 @@
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
|
||||
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
|
||||
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
|
||||
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
|
||||
"sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"
|
||||
"sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
|
||||
"sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==22.0"
|
||||
"version": "==23.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
@@ -1453,11 +1466,11 @@
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71",
|
||||
"sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"
|
||||
"sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5",
|
||||
"sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.2.0"
|
||||
"version": "==7.2.1"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
@@ -1496,12 +1509,11 @@
|
||||
"psutil"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:40fdb8f3544921c5dfcd486ac080ce22870e71d82ced6d2e78fa97c2addd480c",
|
||||
"sha256:70a76f191d8a1d2d6be69fc440cdf85f3e4c03c08b520fd5dc5d338d6cf07d89"
|
||||
"sha256:336098e3bbd8193276867cc87db8b22903c3927665dff9d1ac8684c02f597b68",
|
||||
"sha256:fa10f95a2564cd91652f2d132725183c3b590d9fdcdec09d3677386ecf4c1ce9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==3.1.0"
|
||||
"version": "==3.2.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
@@ -1521,18 +1533,18 @@
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54",
|
||||
"sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"
|
||||
"sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330",
|
||||
"sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==65.6.3"
|
||||
"version": "==67.4.0"
|
||||
},
|
||||
"splinter": {
|
||||
"hashes": [
|
||||
"sha256:4a14a9d1f9d1372c64b666627ef4e103d759379bc1a9bde0c487e00d70976b1e",
|
||||
"sha256:616da85a0c99bef00b59e75eb29e2e48162027c68ccb81a12d1dfe6d26209692"
|
||||
"sha256:1f072570c084f5f7e0c685b6b5d93b1a7959da06cb5da4c3b548dc1b3b0757a0",
|
||||
"sha256:ba9603385deb91ffb92b2e0edeed3da58dce3bfa0e0db1a37143c5e64b83ceb2"
|
||||
],
|
||||
"version": "==0.18.1"
|
||||
"version": "==0.19.0"
|
||||
},
|
||||
"tomli": {
|
||||
"hashes": [
|
||||
@@ -1544,11 +1556,11 @@
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc",
|
||||
"sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"
|
||||
"sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72",
|
||||
"sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.26.13"
|
||||
"version": "==1.26.14"
|
||||
},
|
||||
"zope.component": {
|
||||
"hashes": [
|
||||
|
||||
@@ -321,45 +321,60 @@ class OEmbedView(generic.View):
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
def get_info_string(user):
|
||||
user_str = f"by {user.name} " if user else ""
|
||||
time = timezone.now().strftime('%d/%m/%Y %H:%I')
|
||||
return f"[Paperwork generated {user_str}on {time}"
|
||||
|
||||
|
||||
def render_pdf_response(template, context, append_terms):
|
||||
merger = PdfFileMerger()
|
||||
rml = template.render(context)
|
||||
buffer = rml2pdf.parseString(rml)
|
||||
merger.append(PdfFileReader(buffer))
|
||||
buffer.close()
|
||||
|
||||
if 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
|
||||
|
||||
|
||||
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}]",
|
||||
'info_string': get_info_string(self.request.user) + f"- {obj.current_version_id}]",
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
def get(self, request, pk):
|
||||
template = get_template(self.template_name)
|
||||
return render_pdf_response(get_template(self.template_name), self.get_context_data(), self.append_terms)
|
||||
|
||||
merger = PdfFileMerger()
|
||||
|
||||
context = self.get_context_data()
|
||||
class PrintListView(generic.ListView):
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['current_user'] = self.request.user
|
||||
context['info_string'] = get_info_string(self.request.user) + "]"
|
||||
return context
|
||||
|
||||
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
|
||||
def get(self, request):
|
||||
self.object_list = self.get_queryset()
|
||||
return render_pdf_response(get_template(self.template_name), self.get_context_data(), False)
|
||||
|
||||
1010
RIGS/models.py
1010
RIGS/models.py
File diff suppressed because it is too large
Load Diff
4
RIGS/models/__init__.py
Normal file
4
RIGS/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .models import *
|
||||
from .finance import *
|
||||
from .hs import *
|
||||
from .events import *
|
||||
467
RIGS/models/events.py
Normal file
467
RIGS/models/events.py
Normal file
@@ -0,0 +1,467 @@
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import pytz
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from reversion import revisions as reversion
|
||||
from versioning.versioning import RevisionMixin
|
||||
|
||||
from RIGS.validators import validate_url
|
||||
from .utils import filter_by_pk
|
||||
from .finance import VatRate
|
||||
|
||||
|
||||
class BaseEventManager(models.Manager):
|
||||
def event_search(self, q, start, end, status):
|
||||
filt = Q()
|
||||
if end:
|
||||
filt &= Q(start_date__lte=end)
|
||||
if start:
|
||||
filt &= Q(start_date__gte=start)
|
||||
|
||||
objects = self.all()
|
||||
|
||||
if q:
|
||||
objects = self.search(q)
|
||||
|
||||
if len(status) > 0:
|
||||
filt &= Q(status__in=status)
|
||||
|
||||
qs = objects.filter(filt).order_by('-start_date')
|
||||
|
||||
# Preselect related for efficiency
|
||||
qs.select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
return qs
|
||||
|
||||
class EventManager(BaseEventManager):
|
||||
def current_events(self):
|
||||
events = self.filter(
|
||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Starts after with no end
|
||||
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Ends after
|
||||
(models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Active dry hire
|
||||
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
|
||||
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
|
||||
models.Q(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started
|
||||
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
return events
|
||||
|
||||
def events_in_bounds(self, start, end):
|
||||
events = self.filter(
|
||||
(models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds
|
||||
(models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds
|
||||
(models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds
|
||||
(models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds
|
||||
|
||||
(models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after
|
||||
(models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after
|
||||
(models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after
|
||||
(models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after
|
||||
(models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after
|
||||
|
||||
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
|
||||
'organisation',
|
||||
'venue', 'mic')
|
||||
return events
|
||||
|
||||
def active_dry_hires(self):
|
||||
return self.filter(dry_hire=True, start_date__gte=timezone.now(), is_rig=True)
|
||||
|
||||
def rig_count(self):
|
||||
event_count = self.exclude(status=BaseEvent.CANCELLED).filter(
|
||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
|
||||
is_rig=True)) | # Starts after with no end
|
||||
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True)) | # Ends after
|
||||
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True)) # Active dry hire
|
||||
).count()
|
||||
return event_count
|
||||
|
||||
def waiting_invoices(self):
|
||||
events = self.filter(
|
||||
(
|
||||
models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
||||
models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before
|
||||
) & models.Q(invoice__isnull=True) & # Has not already been invoiced
|
||||
models.Q(is_rig=True) # Is a rig (not non-rig)
|
||||
).order_by('start_date') \
|
||||
.select_related('person', 'organisation', 'venue', 'mic') \
|
||||
.prefetch_related('items')
|
||||
|
||||
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
|
||||
|
||||
|
||||
def find_earliest_event_time(event, datetime_list):
|
||||
# If there is no start time defined, pretend it's midnight
|
||||
startTimeFaked = False
|
||||
if event.has_start_time:
|
||||
startDateTime = datetime.datetime.combine(event.start_date, event.start_time)
|
||||
else:
|
||||
startDateTime = datetime.datetime.combine(event.start_date, datetime.time(00, 00))
|
||||
startTimeFaked = True
|
||||
|
||||
# timezoneIssues - apply the default timezone to the naiive datetime
|
||||
tz = pytz.timezone(settings.TIME_ZONE)
|
||||
startDateTime = tz.localize(startDateTime)
|
||||
datetime_list.append(startDateTime) # then add it to the list
|
||||
|
||||
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
|
||||
|
||||
# if we faked it & it's the earliest, better own up
|
||||
if startTimeFaked and earliest == startDateTime:
|
||||
return event.start_date
|
||||
return earliest
|
||||
|
||||
|
||||
class BaseEvent(models.Model, RevisionMixin):
|
||||
# Done to make it much nicer on the database
|
||||
PROVISIONAL = 0
|
||||
CONFIRMED = 1
|
||||
BOOKED = 2
|
||||
CANCELLED = 3
|
||||
EVENT_STATUS_CHOICES = (
|
||||
(PROVISIONAL, 'Provisional'),
|
||||
(CONFIRMED, 'Confirmed'),
|
||||
(BOOKED, 'Booked'),
|
||||
(CANCELLED, 'Cancelled'),
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
||||
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
||||
description = models.TextField(blank=True, default='')
|
||||
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
||||
|
||||
# Timing
|
||||
start_date = models.DateField()
|
||||
start_time = models.TimeField(blank=True, null=True)
|
||||
end_date = models.DateField(blank=True, null=True)
|
||||
end_time = models.TimeField(blank=True, null=True)
|
||||
|
||||
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def cancelled(self):
|
||||
return (self.status == self.CANCELLED)
|
||||
|
||||
@property
|
||||
def confirmed(self):
|
||||
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
||||
|
||||
@property
|
||||
def has_start_time(self):
|
||||
return self.start_time is not None
|
||||
|
||||
@property
|
||||
def has_end_time(self):
|
||||
return self.end_time is not None
|
||||
|
||||
@property
|
||||
def latest_time(self):
|
||||
"""Returns the end of the event - this function could return either a tzaware datetime, or a naiive date object"""
|
||||
tz = pytz.timezone(settings.TIME_ZONE)
|
||||
endDate = self.end_date
|
||||
if endDate is None:
|
||||
endDate = self.start_date
|
||||
|
||||
if self.has_end_time:
|
||||
endDateTime = datetime.datetime.combine(endDate, self.end_time)
|
||||
tz = pytz.timezone(settings.TIME_ZONE)
|
||||
endDateTime = tz.localize(endDateTime)
|
||||
|
||||
return endDateTime
|
||||
|
||||
else:
|
||||
return endDate
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
start = self.earliest_time
|
||||
if isinstance(self.earliest_time, datetime.datetime):
|
||||
start = self.earliest_time.date()
|
||||
end = self.latest_time
|
||||
if isinstance(self.latest_time, datetime.datetime):
|
||||
end = self.latest_time.date()
|
||||
return (end - start).days + 1
|
||||
|
||||
def clean(self):
|
||||
errdict = {}
|
||||
if self.end_date and self.start_date > self.end_date:
|
||||
errdict['end_date'] = ["Unless you've invented time travel, the event can't finish before it has started."]
|
||||
|
||||
startEndSameDay = not self.end_date or self.end_date == self.start_date
|
||||
hasStartAndEnd = self.has_start_time and self.has_end_time
|
||||
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
|
||||
errdict['end_time'] = ["Unless you've invented time travel, the event can't finish before it has started."]
|
||||
return errdict
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.display_id}: {self.name}"
|
||||
|
||||
@reversion.register(follow=['items'])
|
||||
class Event(BaseEvent):
|
||||
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
|
||||
verbose_name="MIC", on_delete=models.CASCADE)
|
||||
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
||||
notes = models.TextField(blank=True, default='')
|
||||
dry_hire = models.BooleanField(default=False)
|
||||
is_rig = models.BooleanField(default=True)
|
||||
based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True,
|
||||
null=True)
|
||||
|
||||
access_at = models.DateTimeField(blank=True, null=True)
|
||||
meet_at = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
# Dry-hire only
|
||||
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
# Monies
|
||||
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
|
||||
|
||||
# Authorisation request details
|
||||
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
|
||||
auth_request_at = models.DateTimeField(null=True, blank=True)
|
||||
auth_request_to = models.EmailField(blank=True, default='')
|
||||
|
||||
@property
|
||||
def display_id(self):
|
||||
if self.pk:
|
||||
if self.is_rig:
|
||||
return f"N{self.pk:05d}"
|
||||
return self.pk
|
||||
return "????"
|
||||
|
||||
# Calculated values
|
||||
"""
|
||||
EX Vat
|
||||
"""
|
||||
|
||||
@property
|
||||
def sum_total(self):
|
||||
total = self.items.aggregate(
|
||||
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
|
||||
output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
||||
)['sum_total']
|
||||
if total:
|
||||
return total
|
||||
return Decimal("0.00")
|
||||
|
||||
@cached_property
|
||||
def vat_rate(self):
|
||||
return VatRate.objects.find_rate(self.start_date)
|
||||
|
||||
@property
|
||||
def vat(self):
|
||||
# No VAT is owed on internal transfers
|
||||
if self.internal:
|
||||
return 0
|
||||
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
|
||||
|
||||
"""
|
||||
Inc VAT
|
||||
"""
|
||||
@property
|
||||
def total(self):
|
||||
return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
|
||||
|
||||
@property
|
||||
def hs_done(self):
|
||||
return self.riskassessment is not None and len(self.checklists.all()) > 0
|
||||
|
||||
@property
|
||||
def earliest_time(self):
|
||||
"""Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object"""
|
||||
|
||||
# Put all the datetimes in a list
|
||||
datetime_list = []
|
||||
|
||||
if self.access_at:
|
||||
datetime_list.append(self.access_at)
|
||||
|
||||
if self.meet_at:
|
||||
datetime_list.append(self.meet_at)
|
||||
|
||||
earliest = find_earliest_event_time(self, datetime_list)
|
||||
|
||||
return earliest
|
||||
|
||||
@property
|
||||
def internal(self):
|
||||
return bool(self.organisation and self.organisation.union_account)
|
||||
|
||||
@property
|
||||
def authorised(self):
|
||||
if self.internal and hasattr(self, 'authorisation'):
|
||||
return self.authorisation.amount == self.total
|
||||
else:
|
||||
return bool(self.purchase_order)
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
if self.cancelled:
|
||||
return "secondary"
|
||||
elif not self.is_rig:
|
||||
return "info"
|
||||
elif not self.mic:
|
||||
return "danger"
|
||||
elif self.confirmed and self.authorised:
|
||||
if self.dry_hire or self.riskassessment:
|
||||
return "success"
|
||||
return "warning"
|
||||
else:
|
||||
return "warning"
|
||||
|
||||
objects = EventManager()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('event_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def get_edit_url(self):
|
||||
return reverse('event_update', kwargs={'pk': self.pk})
|
||||
|
||||
def clean(self):
|
||||
errdict = super().clean()
|
||||
|
||||
if self.access_at is not None:
|
||||
if self.access_at.date() > self.start_date:
|
||||
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
|
||||
elif self.start_time is not None and self.start_date == self.access_at.date() and self.access_at.time() > self.start_time:
|
||||
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
|
||||
|
||||
if errdict: # If there was an error when validation
|
||||
raise ValidationError(errdict)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Call :meth:`full_clean` before saving."""
|
||||
self.full_clean()
|
||||
super(Event, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
@reversion.register
|
||||
class EventItem(models.Model, RevisionMixin):
|
||||
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True, default='')
|
||||
quantity = models.IntegerField()
|
||||
cost = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
order = models.IntegerField()
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
@property
|
||||
def total_cost(self):
|
||||
return self.cost * self.quantity
|
||||
|
||||
class Meta:
|
||||
ordering = ['order']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return f"item {self.name}"
|
||||
|
||||
|
||||
@reversion.register
|
||||
class EventAuthorisation(models.Model, RevisionMixin):
|
||||
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
|
||||
email = models.EmailField()
|
||||
name = models.CharField(max_length=255)
|
||||
uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID")
|
||||
account_code = models.CharField(max_length=50, default='', blank=True)
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
|
||||
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('event_detail', kwargs={'pk': self.event_id})
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
|
||||
|
||||
|
||||
class SubhireManager(BaseEventManager):
|
||||
def current_events(self):
|
||||
events = self.exclude(status=BaseEvent.CANCELLED).filter(
|
||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True)) | # Starts after with no end
|
||||
(models.Q(end_date__gte=timezone.now().date())) # Ends after
|
||||
).order_by('start_date', 'end_date', 'start_time', 'end_time').select_related('person', 'organisation')
|
||||
|
||||
return events
|
||||
|
||||
def event_count(self):
|
||||
event_count = self.exclude(status=BaseEvent.CANCELLED).filter(
|
||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True)) | # Starts after with no end
|
||||
(models.Q(end_date__gte=timezone.now()))
|
||||
).count()
|
||||
return event_count
|
||||
|
||||
@reversion.register
|
||||
class Subhire(BaseEvent):
|
||||
insurance_value = models.DecimalField(max_digits=10, decimal_places=2) # TODO Validate if this is over notifiable threshold
|
||||
events = models.ManyToManyField(Event)
|
||||
quote = models.URLField(default='', validators=[validate_url])
|
||||
|
||||
objects = SubhireManager()
|
||||
|
||||
@property
|
||||
def is_rig(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def dry_hire(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def display_id(self):
|
||||
return f"S{self.pk:05d}"
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return "purple"
|
||||
|
||||
def get_edit_url(self):
|
||||
return reverse('subhire_update', kwargs={'pk': self.pk})
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('subhire_detail', kwargs={'pk': self.pk})
|
||||
|
||||
@property
|
||||
def earliest_time(self):
|
||||
return find_earliest_event_time(self, [])
|
||||
|
||||
class Meta:
|
||||
permissions = [
|
||||
('subhire_finance', 'Can see financial data for subhire - insurance values')
|
||||
]
|
||||
170
RIGS/models/finance.py
Normal file
170
RIGS/models/finance.py
Normal file
@@ -0,0 +1,170 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import Q
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from reversion import revisions as reversion
|
||||
from versioning.versioning import RevisionMixin
|
||||
from .utils import filter_by_pk
|
||||
|
||||
|
||||
class VatManager(models.Manager):
|
||||
def current_rate(self):
|
||||
return self.find_rate(timezone.now())
|
||||
|
||||
def find_rate(self, date):
|
||||
try:
|
||||
return self.filter(start_at__lte=date).latest()
|
||||
except VatRate.DoesNotExist:
|
||||
r = VatRate
|
||||
r.rate = 0
|
||||
return r
|
||||
|
||||
|
||||
@reversion.register
|
||||
class VatRate(models.Model, RevisionMixin):
|
||||
start_at = models.DateField()
|
||||
rate = models.DecimalField(max_digits=6, decimal_places=6)
|
||||
comment = models.CharField(max_length=255)
|
||||
|
||||
objects = VatManager()
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
@property
|
||||
def as_percent(self):
|
||||
return self.rate * 100
|
||||
|
||||
class Meta:
|
||||
ordering = ['-start_at']
|
||||
get_latest_by = 'start_at'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.comment} {self.start_at} @ {self.as_percent}%"
|
||||
|
||||
|
||||
class InvoiceManager(models.Manager):
|
||||
def outstanding_invoices(self):
|
||||
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||
sql = "SELECT * FROM " \
|
||||
"(SELECT " \
|
||||
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
||||
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
||||
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
||||
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
||||
"AS sub " \
|
||||
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
||||
"ORDER BY invoice_date"
|
||||
|
||||
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):
|
||||
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
||||
invoice_date = models.DateField(auto_now_add=True)
|
||||
void = models.BooleanField(default=False)
|
||||
|
||||
reversion_perm = 'RIGS.view_invoice'
|
||||
|
||||
objects = InvoiceManager()
|
||||
|
||||
@property
|
||||
def sum_total(self):
|
||||
return self.event.sum_total
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
return self.event.total
|
||||
|
||||
@property
|
||||
def payment_total(self):
|
||||
total = self.payment_set.aggregate(total=models.Sum('amount'))['total']
|
||||
if total:
|
||||
return total
|
||||
return Decimal("0.00")
|
||||
|
||||
@property
|
||||
def balance(self):
|
||||
return self.sum_total - self.payment_total
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
return self.balance == 0 or self.void
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('invoice_detail', kwargs={'pk': self.pk})
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return f"{self.display_id} for Event {self.event.display_id}"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.display_id}: {self.event} (£{self.balance:.2f})"
|
||||
|
||||
@property
|
||||
def display_id(self):
|
||||
return f"#{self.pk:05d}"
|
||||
|
||||
class Meta:
|
||||
ordering = ['-invoice_date']
|
||||
|
||||
|
||||
@reversion.register
|
||||
class Payment(models.Model, RevisionMixin):
|
||||
CASH = 'C'
|
||||
INTERNAL = 'I'
|
||||
EXTERNAL = 'E'
|
||||
SUCORE = 'SU'
|
||||
ADJUSTMENT = 'T'
|
||||
METHODS = (
|
||||
(CASH, 'Cash'),
|
||||
(INTERNAL, 'Internal'),
|
||||
(EXTERNAL, 'External'),
|
||||
(SUCORE, 'SU Core'),
|
||||
(ADJUSTMENT, 'TEC Adjustment'),
|
||||
)
|
||||
|
||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
|
||||
date = models.DateField()
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
|
||||
method = models.CharField(max_length=2, choices=METHODS, default='', blank=True)
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_method_display()}: {self.amount}"
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return f"payment of £{self.amount}"
|
||||
243
RIGS/models/hs.py
Normal file
243
RIGS/models/hs.py
Normal file
@@ -0,0 +1,243 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from reversion import revisions as reversion
|
||||
from versioning.versioning import RevisionMixin
|
||||
|
||||
from RIGS.validators import validate_url
|
||||
|
||||
|
||||
@reversion.register
|
||||
class RiskAssessment(models.Model, RevisionMixin):
|
||||
SMALL = (0, 'Small')
|
||||
MEDIUM = (1, 'Medium')
|
||||
LARGE = (2, 'Large')
|
||||
SIZES = (SMALL, MEDIUM, LARGE)
|
||||
|
||||
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
||||
# General
|
||||
nonstandard_equipment = models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by <a href='https://nottinghamtec.sharepoint.com/:f:/g/HealthAndSafety/Eo4xED_DrqFFsfYIjKzMZIIB6Gm_ZfR-a8l84RnzxtBjrA?e=Bf0Haw'>"
|
||||
"TEC's standard risk assessments and method statements?</a>")
|
||||
nonstandard_use = models.BooleanField(help_text="Are TEC using their equipment in a way that is abnormal?<br><small>i.e. Not covered by TECs standard health and safety documentation</small>")
|
||||
contractors = models.BooleanField(help_text="Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>")
|
||||
other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>")
|
||||
crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?")
|
||||
general_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||
|
||||
# Power
|
||||
big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
|
||||
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
|
||||
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)")
|
||||
outside = models.BooleanField(help_text="Is the event outdoors?")
|
||||
generators = models.BooleanField(help_text="Will generators be used?")
|
||||
other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?")
|
||||
nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?")
|
||||
multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?")
|
||||
power_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||
power_plan = models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
||||
|
||||
# Sound
|
||||
noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?")
|
||||
sound_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||
|
||||
# Site
|
||||
known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
|
||||
safe_loading = models.BooleanField(help_text="Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)")
|
||||
safe_storage = models.BooleanField(help_text="Are there any problems with safe and secure equipment storage?")
|
||||
area_outside_of_control = models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")
|
||||
barrier_required = models.BooleanField(help_text="Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?")
|
||||
nonstandard_emergency_procedure = models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")
|
||||
|
||||
# Structures
|
||||
special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?")
|
||||
suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")
|
||||
persons_responsible_structures = models.TextField(blank=True, default='', help_text="Who are the persons on site responsible for their use?")
|
||||
rigging_plan = models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
||||
|
||||
# Blimey that was a lot of options
|
||||
|
||||
reviewed_at = models.DateTimeField(null=True)
|
||||
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||
verbose_name="Reviewer", on_delete=models.CASCADE)
|
||||
|
||||
supervisor_consulted = models.BooleanField(null=True)
|
||||
|
||||
expected_values = {
|
||||
'nonstandard_equipment': False,
|
||||
'nonstandard_use': False,
|
||||
'contractors': False,
|
||||
'other_companies': False,
|
||||
'crew_fatigue': False,
|
||||
# 'big_power': False Doesn't require checking with a super either way
|
||||
'generators': False,
|
||||
'other_companies_power': False,
|
||||
'nonstandard_equipment_power': False,
|
||||
'multiple_electrical_environments': False,
|
||||
'noise_monitoring': False,
|
||||
'known_venue': False,
|
||||
'safe_loading': False,
|
||||
'safe_storage': False,
|
||||
'area_outside_of_control': False,
|
||||
'barrier_required': False,
|
||||
'nonstandard_emergency_procedure': False,
|
||||
'special_structures': False,
|
||||
'suspended_structures': False,
|
||||
}
|
||||
inverted_fields = {key: value for (key, value) in expected_values.items() if not value}.keys()
|
||||
|
||||
def clean(self):
|
||||
# Check for idiots
|
||||
if not self.outside and self.generators:
|
||||
raise forms.ValidationError("Engage brain, please. <strong>No generators indoors!(!)</strong>")
|
||||
|
||||
class Meta:
|
||||
ordering = ['event']
|
||||
permissions = [
|
||||
('review_riskassessment', 'Can review Risk Assessments')
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def fieldz(self):
|
||||
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||
|
||||
@property
|
||||
def event_size(self):
|
||||
# Confirm event size. Check all except generators, since generators entails outside
|
||||
if self.outside or self.other_companies_power or self.nonstandard_equipment_power or self.multiple_electrical_environments:
|
||||
return self.LARGE[0]
|
||||
elif self.big_power:
|
||||
return self.MEDIUM[0]
|
||||
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 f"{self.pk} | {self.event}"
|
||||
|
||||
|
||||
@reversion.register(follow=['vehicles', 'crew'])
|
||||
class EventChecklist(models.Model, RevisionMixin):
|
||||
event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
|
||||
|
||||
# General
|
||||
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists',
|
||||
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?")
|
||||
venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
|
||||
date = models.DateField()
|
||||
|
||||
# Safety Checks
|
||||
safe_parking = models.BooleanField(blank=True, null=True, help_text="Vehicles parked safely?<br><small>(does not obstruct venue access)</small>")
|
||||
safe_packing = models.BooleanField(blank=True, null=True, help_text="Equipment packed away safely?<br><small>(including flightcases)</small>")
|
||||
exits = models.BooleanField(blank=True, null=True, help_text="Emergency exits clear?")
|
||||
trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?")
|
||||
warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>")
|
||||
ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?")
|
||||
hs_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of Safety Bag/Box")
|
||||
extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers")
|
||||
|
||||
# Small Electrical Checks
|
||||
rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
|
||||
supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested?<br><small>(using socket tester)</small>")
|
||||
|
||||
# Shared electrical checks
|
||||
earthing = models.BooleanField(blank=True, null=True, help_text="Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>")
|
||||
pat = models.BooleanField(blank=True, null=True, help_text="All equipment in PAT period?")
|
||||
|
||||
# Medium Electrical Checks
|
||||
source_rcd = models.BooleanField(blank=True, null=True, help_text="Source RCD protected?<br><small>(if cable is more than 3m long) </small>")
|
||||
labelling = models.BooleanField(blank=True, null=True, help_text="Appropriate and clear labelling on distribution and cabling?")
|
||||
# First Distro
|
||||
fd_voltage_l1 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L1-N", help_text="L1 - N")
|
||||
fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N")
|
||||
fd_voltage_l3 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N")
|
||||
fd_phase_rotation = models.BooleanField(blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation<br><small>(if required)</small>")
|
||||
fd_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
|
||||
# Worst case points
|
||||
w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||
w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||
w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||
w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||
w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||
w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||
w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
|
||||
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</small>")
|
||||
public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
|
||||
|
||||
reviewed_at = models.DateTimeField(null=True)
|
||||
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||
verbose_name="Reviewer", on_delete=models.CASCADE)
|
||||
|
||||
inverted_fields = []
|
||||
|
||||
class Meta:
|
||||
ordering = ['event']
|
||||
permissions = [
|
||||
('review_eventchecklist', 'Can review Event Checklists')
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def fieldz(self):
|
||||
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return str(self.event)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ec_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.pk} | {self.event}"
|
||||
|
||||
|
||||
@reversion.register
|
||||
class EventChecklistVehicle(models.Model, RevisionMixin):
|
||||
checklist = models.ForeignKey('EventChecklist', related_name='vehicles', blank=True, on_delete=models.CASCADE)
|
||||
vehicle = models.CharField(max_length=255)
|
||||
driver = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='vehicles', on_delete=models.CASCADE)
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.vehicle} driven by {self.driver}"
|
||||
|
||||
|
||||
@reversion.register
|
||||
class EventChecklistCrew(models.Model, RevisionMixin):
|
||||
checklist = models.ForeignKey('EventChecklist', related_name='crew', blank=True, on_delete=models.CASCADE)
|
||||
crewmember = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='crewed', on_delete=models.CASCADE)
|
||||
role = models.CharField(max_length=255)
|
||||
start = models.DateTimeField()
|
||||
end = models.DateTimeField()
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
def clean(self):
|
||||
if self.start > self.end:
|
||||
raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.crewmember} ({self.role})"
|
||||
173
RIGS/models/models.py
Normal file
173
RIGS/models/models.py
Normal file
@@ -0,0 +1,173 @@
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
from collections import Counter
|
||||
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from versioning.versioning import RevisionMixin
|
||||
from .events import Event
|
||||
from .utils import filter_by_pk
|
||||
|
||||
|
||||
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, 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)
|
||||
is_supervisor = models.BooleanField(default=False)
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
@classmethod
|
||||
def make_api_key(cls):
|
||||
size = 20
|
||||
chars = string.ascii_letters + string.digits
|
||||
new_api_key = ''.join(random.choice(chars) for x in range(size))
|
||||
return new_api_key
|
||||
|
||||
@property
|
||||
def profile_picture(self):
|
||||
url = ""
|
||||
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
|
||||
url = "https://www.gravatar.com/avatar/" + hashlib.md5(
|
||||
self.email.encode('utf-8')).hexdigest() + "?d=wavatar&s=500"
|
||||
return url
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
name = self.get_full_name()
|
||||
if self.initials:
|
||||
name += f' "{self.initials}"'
|
||||
return name
|
||||
|
||||
@property
|
||||
def latest_events(self):
|
||||
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
|
||||
|
||||
@classmethod
|
||||
def admins(cls):
|
||||
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
|
||||
|
||||
@classmethod
|
||||
def users_awaiting_approval_count(cls):
|
||||
return Profile.objects.filter(models.Q(is_approved=False)).count()
|
||||
|
||||
def __str__(self):
|
||||
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:
|
||||
if len(self.notes) > 0:
|
||||
string += "*"
|
||||
return string
|
||||
|
||||
@property
|
||||
def organisations(self):
|
||||
o = []
|
||||
for e in Event.objects.filter(person=self).select_related('organisation'):
|
||||
if e.organisation:
|
||||
o.append(e.organisation)
|
||||
|
||||
# Count up occurances and put them in descending order
|
||||
c = Counter(o)
|
||||
stats = c.most_common()
|
||||
return stats
|
||||
|
||||
@property
|
||||
def latest_events(self):
|
||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('person_detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
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:
|
||||
if len(self.notes) > 0:
|
||||
string += "*"
|
||||
return string
|
||||
|
||||
@property
|
||||
def persons(self):
|
||||
p = []
|
||||
for e in Event.objects.filter(organisation=self).select_related('person'):
|
||||
if e.person:
|
||||
p.append(e.person)
|
||||
|
||||
# Count up occurances and put them in descending order
|
||||
c = Counter(p)
|
||||
stats = c.most_common()
|
||||
return stats
|
||||
|
||||
@property
|
||||
def latest_events(self):
|
||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('organisation_detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
class Venue(models.Model, RevisionMixin):
|
||||
name = models.CharField(max_length=255)
|
||||
phone = models.CharField(max_length=15, blank=True, default='')
|
||||
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:
|
||||
string += "*"
|
||||
return string
|
||||
|
||||
@property
|
||||
def latest_events(self):
|
||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('venue_detail', kwargs={'pk': self.pk})
|
||||
9
RIGS/models/utils.py
Normal file
9
RIGS/models/utils.py
Normal file
@@ -0,0 +1,9 @@
|
||||
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
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 63 KiB |
@@ -11,6 +11,7 @@
|
||||
<initialize>
|
||||
<color id="LightGray" RGB="#D3D3D3"/>
|
||||
<color id="DarkGray" RGB="#707070"/>
|
||||
<color id="Brand" RGB="#3853a4"/>
|
||||
</initialize>
|
||||
|
||||
<paraStyle name="style.para" fontName="OpenSans" />
|
||||
@@ -27,6 +28,8 @@
|
||||
<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" />
|
||||
<paraStyle name="style.emheader" fontName="OpenSans" textColor="White" fontSize="12" backColor="Brand" leading="20" borderPadding="4"/>
|
||||
<paraStyle name="style.breakbefore" parent="emheader" pageBreakBefore="1"/>
|
||||
|
||||
<blockTableStyle id="eventSpecifics">
|
||||
<blockValign value="top"/>
|
||||
|
||||
@@ -15,36 +15,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 py-2">
|
||||
<form class="form-inline" method="GET">
|
||||
<div class="input-group mx-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Start</span>
|
||||
</div>
|
||||
<input type="date" name="start" id="start" value="{{ start|default_if_none:'' }}" placeholder="Start" class="form-control" />
|
||||
</div>
|
||||
<div class="input-group mx-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">End</span>
|
||||
</div>
|
||||
<input type="date" name="end" id="end" value="{{ end|default_if_none:'' }}" placeholder="End" class="form-control" />
|
||||
</div>
|
||||
<div class="input-group mx-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Keyword</span>
|
||||
</div>
|
||||
<input type="search" name="q" placeholder="Keyword" value="{{ request.GET.q }}" class="form-control" />
|
||||
</div>
|
||||
<select class="selectpicker pr-3" multiple data-actions-box="true" data-none-selected-text="Status" data-actions-box="true" id="status" name="status">
|
||||
{% for status in statuses %}
|
||||
<option value="{{status.0}}" {% if status.0|safe in request.GET|get_list:'status' %}selected=""{% endif %}>{{status.1}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% button 'search' %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'partials/archive_form.html' %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% with object_list as events %}
|
||||
|
||||
32
RIGS/templates/partials/archive_form.html
Normal file
32
RIGS/templates/partials/archive_form.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% load get_list from filters %}
|
||||
{% load button from filters %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 py-2">
|
||||
<form class="form-inline" method="GET">
|
||||
<div class="input-group mx-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Start</span>
|
||||
</div>
|
||||
<input type="date" name="start" id="start" value="{{ start|default_if_none:'' }}" placeholder="Start" class="form-control" />
|
||||
</div>
|
||||
<div class="input-group mx-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">End</span>
|
||||
</div>
|
||||
<input type="date" name="end" id="end" value="{{ end|default_if_none:'' }}" placeholder="End" class="form-control" />
|
||||
</div>
|
||||
<div class="input-group mx-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Keyword</span>
|
||||
</div>
|
||||
<input type="search" name="q" placeholder="Keyword" value="{{ request.GET.q }}" class="form-control" />
|
||||
</div>
|
||||
<select class="selectpicker pr-3" multiple data-actions-box="true" data-none-selected-text="Status" data-actions-box="true" id="status" name="status">
|
||||
{% for status in statuses %}
|
||||
<option value="{{status.0}}" {% if status.0|safe in request.GET|get_list:'status' %}selected=""{% endif %}>{{status.1}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% button 'search' %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
84
RIGS/templates/partials/subhire_table.html
Normal file
84
RIGS/templates/partials/subhire_table.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{% load linked_name from filters %}
|
||||
{% load markdown_tags %}
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0" id="event_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Dates & Times</th>
|
||||
<th scope="col">Hire Details</th>
|
||||
<th scope="col">Associated Event(s)</th>
|
||||
{% if perms.RIGS.subhire_finance %}
|
||||
<th scope="col">Insurance Value</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in events %}
|
||||
<tr {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
||||
<!---Number-->
|
||||
<th scope="row" id="event_number">{{ event.display_id }}</th>
|
||||
<!--Dates & Times-->
|
||||
<td id="event_dates" style="text-align: justify;">
|
||||
<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" }}
|
||||
{% endif %}</strong>
|
||||
</span>
|
||||
{% if event.end_date %}
|
||||
<br>
|
||||
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}{% endif %}
|
||||
{% if event.has_end_time %}
|
||||
{{ event.end_time|date:"H:i" }}
|
||||
{% endif %}</strong>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<!---Details-->
|
||||
<td id="event_details" class="w-100">
|
||||
<h4>
|
||||
<a href="{{event.get_absolute_url}}">
|
||||
{{ event.name }}
|
||||
</a>
|
||||
</h4>
|
||||
<h5>
|
||||
Primary Contact: {{ event.person|linked_name }}
|
||||
{% if event.organisation %}
|
||||
({{ event.organisation|linked_name }})
|
||||
{% endif %}
|
||||
</h5>
|
||||
{% if not event.cancelled and event.description %}
|
||||
<p>{{ event.description|markdown }}</p>
|
||||
{% endif %}
|
||||
{% include 'partials/event_status.html' %}
|
||||
</td>
|
||||
<td class="p-0 text-nowrap">
|
||||
<ul class="list-group">
|
||||
{% for event in event.events.all %}
|
||||
<li class="list-group-item"><a href="{{event.get_absolute_url}}">{{ event }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
{% if perms.RIGS.subhire_finance %}
|
||||
<td id="insurance_value" class="text-nowrap">
|
||||
£{{ event.insurance_value }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr class="bg-warning">
|
||||
<td colspan="4">No events found</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>Total Value:</td>
|
||||
<td>£{{ total_value }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
27
RIGS/templates/subhire_list.html
Normal file
27
RIGS/templates/subhire_list.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends 'base_rigs.html' %}
|
||||
{% load paginator from filters %}
|
||||
{% 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 content %}
|
||||
{% include 'partials/archive_form.html' %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% with object_list as events %}
|
||||
{% include 'partials/subhire_table.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% paginator %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -175,6 +175,9 @@ def namewithnotes(obj, url, autoescape=True):
|
||||
else:
|
||||
return obj.name
|
||||
|
||||
@register.filter(needs_autoescape=True)
|
||||
def linked_name(object, autoescape=True):
|
||||
return mark_safe(f"<a href='{object.get_absolute_url()}'>{object.name}</a>")
|
||||
|
||||
@register.filter(needs_autoescape=True)
|
||||
def linkornone(target, namespace=None, autoescape=True):
|
||||
@@ -196,8 +199,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
|
||||
text = "Edit"
|
||||
elif type == 'print':
|
||||
clazz += " btn-primary "
|
||||
icon = "fa-print"
|
||||
text = "Print"
|
||||
icon = "fa-download"
|
||||
text = "Export"
|
||||
elif type == 'duplicate':
|
||||
clazz += " btn-info "
|
||||
icon = "fa-copy"
|
||||
|
||||
@@ -74,7 +74,7 @@ urlpatterns = [
|
||||
name='subhire_create'),
|
||||
path('subhire/<int:pk>/edit', permission_required_with_403('RIGS.change_event')(views.SubhireEdit.as_view()),
|
||||
name='subhire_update'),
|
||||
path('subhire/upcoming', login_required(views.SubhireList.as_view()),
|
||||
path('subhire/list/', login_required(views.SubhireList.as_view()),
|
||||
name='subhire_list'),
|
||||
|
||||
# Dashboards
|
||||
|
||||
@@ -14,16 +14,18 @@ class Calendar(HTMLCalendar):
|
||||
return f"<a href='{event.get_absolute_url()}' class='modal-href' style='display: contents;'><div class='event event-start event-end bg-{event.color}' data-span='{event.length}' style='grid-column-start: calc({day[1]} + 1)'>{event}</div></a>"
|
||||
|
||||
def formatmonth(self, withyear=True):
|
||||
events = Event.objects.filter(start_date__year=self.year, start_date__month=self.month)
|
||||
subhires = Subhire.objects.filter(start_date__year=self.year, start_date__month=self.month)
|
||||
events = Event.objects.filter(start_date__year=self.year, start_date__month=self.month).order_by("start_date")
|
||||
subhires = Subhire.objects.filter(start_date__year=self.year, start_date__month=self.month).order_by("start_date")
|
||||
weeks = self.monthdays2calendar(self.year, self.month)
|
||||
data = []
|
||||
|
||||
for week in weeks:
|
||||
weeks_events = []
|
||||
|
||||
for day in week:
|
||||
events_per_day = events.order_by("start_date").filter(start_date__day=day[0])
|
||||
subhires_per_day = subhires.order_by("start_date").filter(start_date__day=day[0])
|
||||
# Events that have started this week
|
||||
events_per_day = events.filter(start_date__day=day[0])
|
||||
subhires_per_day = subhires.filter(start_date__day=day[0])
|
||||
event_html = ""
|
||||
for event in events_per_day:
|
||||
event_html += self.get_html(day, event)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import datetime
|
||||
|
||||
import pytz
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django_ical.views import ICalFeed
|
||||
|
||||
from RIGS import models
|
||||
|
||||
from itertools import chain
|
||||
|
||||
|
||||
class CalendarICS(ICalFeed):
|
||||
"""
|
||||
@@ -31,6 +33,7 @@ class CalendarICS(ICalFeed):
|
||||
params['dry-hire'] = request.GET.get('dry-hire', 'true') == 'true'
|
||||
params['non-rig'] = request.GET.get('non-rig', 'true') == 'true'
|
||||
params['rig'] = request.GET.get('rig', 'true') == 'true'
|
||||
params['subhire'] = request.GET.get('subhire', 'true') == 'true'
|
||||
|
||||
params['cancelled'] = request.GET.get('cancelled', 'false') == 'true'
|
||||
params['provisional'] = request.GET.get('provisional', 'true') == 'true'
|
||||
@@ -40,42 +43,46 @@ class CalendarICS(ICalFeed):
|
||||
|
||||
def description(self, params):
|
||||
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + (
|
||||
'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
|
||||
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
|
||||
'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire, ' if params['dry-hire'] else '') + ('Subhires' if params['subhire'] else '') + '\n'
|
||||
desc += "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
|
||||
'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
|
||||
|
||||
return desc
|
||||
|
||||
def items(self, params):
|
||||
# include events from up to 1 year ago
|
||||
start = datetime.datetime.now() - datetime.timedelta(days=365)
|
||||
start = timezone.now() - datetime.timedelta(days=365)
|
||||
filter = Q(start_date__gte=start)
|
||||
|
||||
typeFilters = Q(pk=None) # Need something that is false for every entry
|
||||
type_filters = Q(pk=None) # Need something that is false for every entry
|
||||
|
||||
if params['dry-hire']:
|
||||
typeFilters = typeFilters | Q(dry_hire=True, is_rig=True)
|
||||
type_filters = type_filters | Q(dry_hire=True, is_rig=True)
|
||||
|
||||
if params['non-rig']:
|
||||
typeFilters = typeFilters | Q(is_rig=False)
|
||||
type_filters = type_filters | Q(is_rig=False)
|
||||
|
||||
if params['rig']:
|
||||
typeFilters = typeFilters | Q(is_rig=True, dry_hire=False)
|
||||
type_filters = type_filters | Q(is_rig=True, dry_hire=False)
|
||||
|
||||
statusFilters = Q(pk=None) # Need something that is false for every entry
|
||||
status_filters = Q(pk=None) # Need something that is false for every entry
|
||||
|
||||
if params['cancelled']:
|
||||
statusFilters = statusFilters | Q(status=models.Event.CANCELLED)
|
||||
status_filters = status_filters | Q(status=models.Event.CANCELLED)
|
||||
if params['provisional']:
|
||||
statusFilters = statusFilters | Q(status=models.Event.PROVISIONAL)
|
||||
status_filters = status_filters | Q(status=models.Event.PROVISIONAL)
|
||||
if params['confirmed']:
|
||||
statusFilters = statusFilters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
|
||||
status_filters = status_filters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
|
||||
|
||||
filter = filter & typeFilters & statusFilters
|
||||
filter = filter & type_filters & status_filters
|
||||
|
||||
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
||||
events = models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
||||
'venue', 'mic')
|
||||
|
||||
subhires = models.Subhire.objects.filter(status_filters).order_by('-start_date').select_related('person', 'organisation')
|
||||
|
||||
return list(chain(events, subhires))
|
||||
|
||||
def item_title(self, item):
|
||||
title = ''
|
||||
|
||||
@@ -106,30 +113,32 @@ class CalendarICS(ICalFeed):
|
||||
return item.latest_time
|
||||
|
||||
def item_location(self, item):
|
||||
return item.venue
|
||||
if hasattr(item, 'venue'):
|
||||
return item.venue
|
||||
return ""
|
||||
|
||||
def item_description(self, item):
|
||||
# Create a nice information-rich description
|
||||
# note: only making use of information available to "non-keyholders"
|
||||
|
||||
tz = pytz.timezone(self.timezone)
|
||||
|
||||
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 hasattr(item, 'venue'):
|
||||
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 += f'Status = {item.get_status_display()}\n'
|
||||
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
||||
if hasattr(item, 'mic'):
|
||||
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
||||
|
||||
desc += '\n'
|
||||
if item.meet_at:
|
||||
if hasattr(item, 'meet_at') and item.meet_at:
|
||||
desc += 'Crew Meet = ' + (
|
||||
item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
||||
if item.access_at:
|
||||
timezone.make_aware(item.meet_at).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
||||
if hasattr(item, 'access_at') and item.access_at:
|
||||
desc += 'Access At = ' + (
|
||||
item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
||||
timezone.make_aware(item.access_at).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
||||
if item.start_date:
|
||||
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + (
|
||||
(' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
|
||||
@@ -140,8 +149,6 @@ class CalendarICS(ICalFeed):
|
||||
desc += '\n'
|
||||
if item.description:
|
||||
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'
|
||||
|
||||
desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}'
|
||||
|
||||
|
||||
@@ -204,27 +204,7 @@ class EventArchive(generic.ListView):
|
||||
"Muppet! Check the dates, it has been fixed for you.")
|
||||
start, end = end, start # Stop the impending fail
|
||||
|
||||
filter = Q()
|
||||
if end != "":
|
||||
filter &= Q(start_date__lte=end)
|
||||
if start:
|
||||
filter &= Q(start_date__gte=start)
|
||||
|
||||
q = self.request.GET.get('q', "")
|
||||
objects = self.model.objects.all()
|
||||
|
||||
if q:
|
||||
objects = self.model.objects.search(q)
|
||||
|
||||
status = self.request.GET.getlist('status', "")
|
||||
|
||||
if len(status) > 0:
|
||||
filter &= Q(status__in=status)
|
||||
|
||||
qs = objects.filter(filter).order_by('-start_date')
|
||||
|
||||
# Preselect related for efficiency
|
||||
qs.select_related('person', 'organisation', 'venue', 'mic')
|
||||
qs = self.model.objects.event_search(self.request.GET.get('q', None), start, end, self.request.GET.get('status', ""))
|
||||
|
||||
if not qs.exists():
|
||||
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import generic
|
||||
from django.db.models import Sum
|
||||
from PyRIGS.views import ModalURLMixin, get_related
|
||||
from RIGS import models, forms
|
||||
from RIGS.views import EventArchive
|
||||
|
||||
|
||||
class SubhireDetail(generic.DetailView, ModalURLMixin):
|
||||
@@ -48,11 +50,13 @@ class SubhireEdit(generic.UpdateView):
|
||||
return reverse_lazy('subhire_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
class SubhireList(generic.TemplateView):
|
||||
template_name = 'rigboard.html'
|
||||
class SubhireList(EventArchive):
|
||||
template_name = 'subhire_list.html'
|
||||
model = models.Subhire
|
||||
paginate_by = 25
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['events'] = models.Subhire.objects.current_events()
|
||||
context['page_title'] = "Upcoming Subhire"
|
||||
context['total_value'] = self.get_queryset().aggregate(sum=Sum('insurance_value'))['sum']
|
||||
context['page_title'] = "Subhire List"
|
||||
return context
|
||||
|
||||
@@ -105,8 +105,7 @@ class Command(BaseCommand):
|
||||
|
||||
for i in range(100):
|
||||
prefix = random.choice(asset_prefixes)
|
||||
asset_id = str(get_available_asset_id(wanted_prefix=prefix))
|
||||
asset_id = prefix + asset_id
|
||||
asset_id = get_available_asset_id(wanted_prefix=prefix)
|
||||
asset = models.Asset(
|
||||
asset_id=asset_id,
|
||||
description=random.choice(asset_description),
|
||||
|
||||
@@ -102,7 +102,8 @@ class AssetManager(models.Manager):
|
||||
|
||||
def get_available_asset_id(wanted_prefix=""):
|
||||
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)
|
||||
last_asset_id = last_asset.asset_id_number if last_asset else 0
|
||||
return wanted_prefix + str(last_asset_id + 1)
|
||||
|
||||
|
||||
def validate_positive(value):
|
||||
|
||||
@@ -168,7 +168,7 @@ 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)
|
||||
initial["asset_id"] = models.get_available_asset_id()
|
||||
return initial
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
153
package-lock.json
generated
153
package-lock.json
generated
@@ -37,7 +37,7 @@
|
||||
"uglify-js": "^3.14.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browser-sync": "^2.27.10"
|
||||
"browser-sync": "^2.27.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -1026,13 +1026,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browser-sync": {
|
||||
"version": "2.27.10",
|
||||
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.10.tgz",
|
||||
"integrity": "sha512-xKm+6KJmJu6RuMWWbFkKwOCSqQOxYe3nOrFkKI5Tr/ZzjPxyU3pFShKK3tWnazBo/3lYQzN7fzjixG8fwJh1Xw==",
|
||||
"version": "2.27.11",
|
||||
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.11.tgz",
|
||||
"integrity": "sha512-U5f9u97OYJH66T0MGWWzG9rOQTW6ZmDMj97vsmtqwNS03JAwdLVES8eel2lD3rvAqQCNAFqaJ74NMacBI57vJg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"browser-sync-client": "^2.27.10",
|
||||
"browser-sync-ui": "^2.27.10",
|
||||
"browser-sync-client": "^2.27.11",
|
||||
"browser-sync-ui": "^2.27.11",
|
||||
"bs-recipes": "1.3.4",
|
||||
"bs-snippet-injector": "^2.0.1",
|
||||
"chokidar": "^3.5.1",
|
||||
@@ -1050,7 +1050,7 @@
|
||||
"micromatch": "^4.0.2",
|
||||
"opn": "5.3.0",
|
||||
"portscanner": "2.2.0",
|
||||
"qs": "6.2.3",
|
||||
"qs": "^6.11.0",
|
||||
"raw-body": "^2.3.2",
|
||||
"resp-modifier": "6.0.2",
|
||||
"rx": "4.1.0",
|
||||
@@ -1070,9 +1070,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browser-sync-client": {
|
||||
"version": "2.27.10",
|
||||
"resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.10.tgz",
|
||||
"integrity": "sha512-KCFKA1YDj6cNul0VsA28apohtBsdk5Wv8T82ClOZPZMZWxPj4Ny5AUbrj9UlAb/k6pdxE5HABrWDhP9+cjt4HQ==",
|
||||
"version": "2.27.11",
|
||||
"resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.11.tgz",
|
||||
"integrity": "sha512-okMNfD2NasL/XD1/BclP3onXjhahisk3e/kTQ5HPDT/lLqdBqNDd6QFcjI5I1ak7na2hxKQSLjryql+7fp5gKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"etag": "1.8.1",
|
||||
@@ -1086,9 +1086,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browser-sync-ui": {
|
||||
"version": "2.27.10",
|
||||
"resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.10.tgz",
|
||||
"integrity": "sha512-elbJILq4Uo6OQv6gsvS3Y9vRAJlWu+h8j0JDkF0X/ua+3S6SVbbiWnZc8sNOFlG7yvVGIwBED3eaYQ0iBo1Dtw==",
|
||||
"version": "2.27.11",
|
||||
"resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.11.tgz",
|
||||
"integrity": "sha512-1T/Y8Pp1R68aUL7zVSFq0nxtr258xWd/nTasCAHX2M6EsGaswVOFtXsw3bKqsr35z+J+LfVfOdz1HFLYKxdgrA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"async-each-series": "0.1.1",
|
||||
@@ -4204,9 +4204,9 @@
|
||||
"integrity": "sha512-rmglSaNttGo4LY33PFW51mgeD1ItvHyfS9cRCD+Cj9Msj/xFaG/sZjLGVtPbtxYJmhY/c8jtw6G07yWhC2ifEw=="
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
|
||||
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
@@ -5952,6 +5952,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/object-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
@@ -6905,12 +6914,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz",
|
||||
"integrity": "sha512-AY4g8t3LMboim0t6XWFdz6J5OuJ1ZNYu54SXihS/OMpgyCqYmcAJnWqkNSOjSjWmq3xxy+GF9uWQI2lI/7tKIA==",
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
@@ -7626,6 +7641,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
@@ -7836,15 +7865,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.3.tgz",
|
||||
"integrity": "sha512-I/hqDYpQ6JKwtJOf5ikM+Qz+YujZPMEl6qBLhxiP0nX+TfXKhW4KZZG8lamrD6Y5ngjmYHreESVasVCgi5Kl3A==",
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.4.tgz",
|
||||
"integrity": "sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.2.3",
|
||||
"socket.io-parser": "~4.2.0"
|
||||
"socket.io-parser": "~4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -8662,9 +8691,9 @@
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
|
||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -9986,13 +10015,13 @@
|
||||
}
|
||||
},
|
||||
"browser-sync": {
|
||||
"version": "2.27.10",
|
||||
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.10.tgz",
|
||||
"integrity": "sha512-xKm+6KJmJu6RuMWWbFkKwOCSqQOxYe3nOrFkKI5Tr/ZzjPxyU3pFShKK3tWnazBo/3lYQzN7fzjixG8fwJh1Xw==",
|
||||
"version": "2.27.11",
|
||||
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.11.tgz",
|
||||
"integrity": "sha512-U5f9u97OYJH66T0MGWWzG9rOQTW6ZmDMj97vsmtqwNS03JAwdLVES8eel2lD3rvAqQCNAFqaJ74NMacBI57vJg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"browser-sync-client": "^2.27.10",
|
||||
"browser-sync-ui": "^2.27.10",
|
||||
"browser-sync-client": "^2.27.11",
|
||||
"browser-sync-ui": "^2.27.11",
|
||||
"bs-recipes": "1.3.4",
|
||||
"bs-snippet-injector": "^2.0.1",
|
||||
"chokidar": "^3.5.1",
|
||||
@@ -10010,7 +10039,7 @@
|
||||
"micromatch": "^4.0.2",
|
||||
"opn": "5.3.0",
|
||||
"portscanner": "2.2.0",
|
||||
"qs": "6.2.3",
|
||||
"qs": "^6.11.0",
|
||||
"raw-body": "^2.3.2",
|
||||
"resp-modifier": "6.0.2",
|
||||
"rx": "4.1.0",
|
||||
@@ -10024,9 +10053,9 @@
|
||||
}
|
||||
},
|
||||
"browser-sync-client": {
|
||||
"version": "2.27.10",
|
||||
"resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.10.tgz",
|
||||
"integrity": "sha512-KCFKA1YDj6cNul0VsA28apohtBsdk5Wv8T82ClOZPZMZWxPj4Ny5AUbrj9UlAb/k6pdxE5HABrWDhP9+cjt4HQ==",
|
||||
"version": "2.27.11",
|
||||
"resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.11.tgz",
|
||||
"integrity": "sha512-okMNfD2NasL/XD1/BclP3onXjhahisk3e/kTQ5HPDT/lLqdBqNDd6QFcjI5I1ak7na2hxKQSLjryql+7fp5gKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"etag": "1.8.1",
|
||||
@@ -10037,9 +10066,9 @@
|
||||
}
|
||||
},
|
||||
"browser-sync-ui": {
|
||||
"version": "2.27.10",
|
||||
"resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.10.tgz",
|
||||
"integrity": "sha512-elbJILq4Uo6OQv6gsvS3Y9vRAJlWu+h8j0JDkF0X/ua+3S6SVbbiWnZc8sNOFlG7yvVGIwBED3eaYQ0iBo1Dtw==",
|
||||
"version": "2.27.11",
|
||||
"resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.11.tgz",
|
||||
"integrity": "sha512-1T/Y8Pp1R68aUL7zVSFq0nxtr258xWd/nTasCAHX2M6EsGaswVOFtXsw3bKqsr35z+J+LfVfOdz1HFLYKxdgrA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"async-each-series": "0.1.1",
|
||||
@@ -12490,9 +12519,9 @@
|
||||
"integrity": "sha512-rmglSaNttGo4LY33PFW51mgeD1ItvHyfS9cRCD+Cj9Msj/xFaG/sZjLGVtPbtxYJmhY/c8jtw6G07yWhC2ifEw=="
|
||||
},
|
||||
"http-cache-semantics": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
|
||||
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "2.0.0",
|
||||
@@ -13857,6 +13886,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"dev": true
|
||||
},
|
||||
"object-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
@@ -14490,10 +14525,13 @@
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz",
|
||||
"integrity": "sha512-AY4g8t3LMboim0t6XWFdz6J5OuJ1ZNYu54SXihS/OMpgyCqYmcAJnWqkNSOjSjWmq3xxy+GF9uWQI2lI/7tKIA==",
|
||||
"dev": true
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"quick-lru": {
|
||||
"version": "4.0.1",
|
||||
@@ -15057,6 +15095,17 @@
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
@@ -15244,15 +15293,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"socket.io-client": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.3.tgz",
|
||||
"integrity": "sha512-I/hqDYpQ6JKwtJOf5ikM+Qz+YujZPMEl6qBLhxiP0nX+TfXKhW4KZZG8lamrD6Y5ngjmYHreESVasVCgi5Kl3A==",
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.4.tgz",
|
||||
"integrity": "sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.2.3",
|
||||
"socket.io-parser": "~4.2.0"
|
||||
"socket.io-parser": "~4.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
@@ -15879,9 +15928,9 @@
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
|
||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true
|
||||
},
|
||||
"typo-js": {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"uglify-js": "^3.14.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browser-sync": "^2.27.10"
|
||||
"browser-sync": "^2.27.11"
|
||||
},
|
||||
"scripts": {
|
||||
"gulp": "gulp",
|
||||
|
||||
@@ -143,6 +143,15 @@ class Command(BaseCommand):
|
||||
"Bin Diving",
|
||||
"Wiki Editing"]
|
||||
|
||||
descriptions = [
|
||||
"Physical training concentrates on mechanistic goals: training programs in this area develop specific motor skills, agility, strength or physical fitness, often with an intention of peaking at a particular time.",
|
||||
"In military use, training means gaining the physical ability to perform and survive in combat, and learn the many skills needed in a time of war.",
|
||||
"These include how to use a variety of weapons, outdoor survival skills, and how to survive being captured by the enemy, among many others. See military education and training.",
|
||||
"While some studies have indicated relaxation training is useful for some medical conditions, autogenic training has limited results or has been the result of few studies.",
|
||||
"Some occupations are inherently hazardous, and require a minimum level of competence before the practitioners can perform the work at an acceptable level of safety to themselves or others in the vicinity.",
|
||||
"Occupational diving, rescue, firefighting and operation of certain types of machinery and vehicles may require assessment and certification of a minimum acceptable competence before the person is allowed to practice as a licensed instructor."
|
||||
]
|
||||
|
||||
for i, name in enumerate(names):
|
||||
category = random.choice(self.categories)
|
||||
previous_item = models.TrainingItem.objects.filter(category=category).last()
|
||||
@@ -150,7 +159,7 @@ class Command(BaseCommand):
|
||||
number = previous_item.reference_number + 1
|
||||
else:
|
||||
number = 0
|
||||
item = models.TrainingItem.objects.create(category=category, reference_number=number, description=name)
|
||||
item = models.TrainingItem.objects.create(category=category, reference_number=number, name=name, description=random.choice(descriptions) + random.choice(descriptions) + random.choice(descriptions))
|
||||
self.items.append(item)
|
||||
|
||||
def setup_levels(self):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.18 on 2023-02-19 14:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('training', '0005_auto_20220223_1535'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='trainingitem',
|
||||
old_name='description',
|
||||
new_name='name',
|
||||
),
|
||||
]
|
||||
18
training/migrations/0007_trainingitem_description.py
Normal file
18
training/migrations/0007_trainingitem_description.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.18 on 2023-02-19 14:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('training', '0006_rename_description_trainingitem_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trainingitem',
|
||||
name='description',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -85,7 +85,7 @@ 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))
|
||||
or_lookup = (Q(name__icontains=query) | Q(description__icontains=query) | Q(display_id=query))
|
||||
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||
return qs
|
||||
|
||||
@@ -94,16 +94,13 @@ class TrainingItemManager(QueryablePropertiesManager):
|
||||
class TrainingItem(models.Model):
|
||||
reference_number = models.IntegerField()
|
||||
category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.CASCADE)
|
||||
description = models.CharField(max_length=50)
|
||||
name = models.CharField(max_length=50)
|
||||
description = models.TextField(blank=True)
|
||||
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}"
|
||||
@@ -121,7 +118,7 @@ class TrainingItem(models.Model):
|
||||
return models.Q()
|
||||
|
||||
def __str__(self):
|
||||
name = f"{self.display_id} {self.description}"
|
||||
name = f"{self.display_id} {self.name}"
|
||||
if not self.active:
|
||||
name += " (inactive)"
|
||||
return name
|
||||
@@ -149,7 +146,7 @@ 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))
|
||||
or_lookup = (Q(item__name__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
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
{% extends 'base_training.html' %}
|
||||
|
||||
{% load button from filters %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-12 text-right py-2 pr-0">
|
||||
{% button 'print' 'item_list_export' %}
|
||||
</div>
|
||||
<div id="accordion">
|
||||
{% for category in categories %}
|
||||
<div class="card">
|
||||
@@ -13,7 +18,8 @@
|
||||
<div class="card-body">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for item in category.items.all %}
|
||||
<li class="list-group-item {% if not item.active%}text-warning{%endif%}">{{ item }}
|
||||
<li class="list-group-item {% if not item.active%}text-warning{%endif%}">{{ item }} <a href="{% url 'item_qualification' item.pk %}" class="btn btn-info float-right"><span class="fas fa-user"></span> Qualified Users</a>
|
||||
<br><small>{{ item.description }}</small>
|
||||
{% if item.prerequisites.exists %}
|
||||
<div class="ml-3 font-italic">
|
||||
<p class="text-info mb-0">Passed Out Prerequisites:</p>
|
||||
@@ -24,7 +30,6 @@
|
||||
</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>
|
||||
|
||||
25
training/templates/item_list.xml
Normal file
25
training/templates/item_list.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends 'base_print.xml' %}
|
||||
|
||||
{% block content %}
|
||||
<h1 style="page-head">TEC Training Item List</h1>
|
||||
<spacer length="15" />
|
||||
{% for category in categories %}
|
||||
<h2 {% if not forloop.first %}style="breakbefore"{%else%}style="emheader"{%endif%}>{{category}}</h2>
|
||||
<spacer length="10" />
|
||||
{% for item in category.items.all %}
|
||||
<h3>{{ item }}</h3>
|
||||
<spacer length="4" />
|
||||
<para>{{ item.description }}</para>
|
||||
{% if item.prerequisites.exists %}
|
||||
<h4>Prerequisites:</h4>
|
||||
<ul bulletFontSize="5">
|
||||
{% for p in item.prerequisites.all %}
|
||||
<li><para>{{p}}</para></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<spacer length="8" />
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<namedString id="lastPage"><pageNumber/></namedString>
|
||||
{% endblock %}
|
||||
@@ -8,6 +8,7 @@ from versioning.views import VersionHistory
|
||||
|
||||
urlpatterns = [
|
||||
path('items/', login_required(views.ItemList.as_view()), name='item_list'),
|
||||
path('items/export/', login_required(views.ItemListExport.as_view()), name='item_list_export'),
|
||||
path('item/<int:pk>/qualified_users/', login_required(views.ItemQualifications.as_view()), name='item_qualification'),
|
||||
|
||||
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.db import transaction
|
||||
from django.db.models import Q, Count
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from PyRIGS.views import is_ajax, ModalURLMixin, get_related
|
||||
from PyRIGS.views import is_ajax, ModalURLMixin, get_related, PrintListView
|
||||
from training import models, forms
|
||||
from users import views
|
||||
from reversion.views import RevisionMixin
|
||||
@@ -24,6 +24,17 @@ class ItemList(generic.ListView):
|
||||
return context
|
||||
|
||||
|
||||
class ItemListExport(PrintListView):
|
||||
model = models.TrainingItem
|
||||
template_name = 'item_list.xml'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['filename'] = "TrainingItemList.pdf"
|
||||
context["categories"] = models.TrainingCategory.objects.all()
|
||||
return context
|
||||
|
||||
|
||||
class TraineeDetail(views.ProfileDetail):
|
||||
template_name = "trainee_detail.html"
|
||||
model = models.Trainee
|
||||
|
||||
@@ -118,6 +118,11 @@
|
||||
<input type="checkbox" value="dry-hire" data-default="true" checked> Dry-Hires
|
||||
</label>
|
||||
<label class="checkbox-inline mx-lg-2">
|
||||
<input type="checkbox" value="subhire" data-default="false"> Subhires
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group d-flex flex-column flex-lg-row">
|
||||
<label class="checkbox-inline mr-lg-2">
|
||||
<input type="checkbox" value="cancelled" data-default="false" > Cancelled
|
||||
</label>
|
||||
<label class="checkbox-inline mx-lg-2">
|
||||
|
||||
Reference in New Issue
Block a user