Compare commits

..

4 Commits

56 changed files with 2293 additions and 2485 deletions

17
Pipfile
View File

@@ -21,7 +21,8 @@ dj-static = "~=0.0.6"
Django = "~=3.2" Django = "~=3.2"
django-debug-toolbar = "~=3.2" django-debug-toolbar = "~=3.2"
django-filter = "~=2.4.0" django-filter = "~=2.4.0"
django-ical = "~=1.8.3" django-ical = "~=1.7.1"
django-recurrence = "~=1.10.3"
django-registration-redux = "~=2.9" django-registration-redux = "~=2.9"
django-reversion = "~=3.0.9" django-reversion = "~=3.0.9"
django-widget-tweaks = "~=1.4.8" django-widget-tweaks = "~=1.4.8"
@@ -75,6 +76,7 @@ django-hCaptcha = "*"
importlib-metadata = "*" importlib-metadata = "*"
django-hcaptcha = "*" django-hcaptcha = "*"
"z3c.rml" = "*" "z3c.rml" = "*"
pikepdf = "*"
django-queryable-properties = "*" django-queryable-properties = "*"
django-mass-edit = "*" django-mass-edit = "*"
selenium = "~=3.141.0" selenium = "~=3.141.0"
@@ -89,11 +91,14 @@ pluggy = "*"
pytest-splinter = "*" pytest-splinter = "*"
pytest = "*" pytest = "*"
pytest-reverse = "*" pytest-reverse = "*"
pytest-xdist = {extras = [ "psutil",], version = "*"}
PyPOM = {extras = [ "splinter",], version = "*"}
[requires] [requires]
python_version = "3.10" python_version = "3.9"
[pipenv] [dev-packages.pytest-xdist]
allow_prereleases = true extras = [ "psutil",]
version = "*"
[dev-packages.PyPOM]
extras = [ "splinter",]
version = "*"

602
Pipfile.lock generated
View File

@@ -1,11 +1,11 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "96e25ba04c8709eba5d40776ff20cb82e3abecfd9916b8bd9dd4c594d46e2098" "sha256": "2e2fb4b609c10fc42db6bbd69ca73800629fbcaceec664e1fcc79d4b37bc0eb1"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
"python_version": "3.10" "python_version": "3.9"
}, },
"sources": [ "sources": [
{ {
@@ -272,11 +272,11 @@
}, },
"django-ical": { "django-ical": {
"hashes": [ "hashes": [
"sha256:0d5595c5bc954e401b59b27a9a86962557f0d3b965e9f5860244cd6bc450e8ab", "sha256:6df4dc61eb4abc55816bd16a949e497bea99828c7de648438ace7f1f85eeb405",
"sha256:d3f97d163c03ea795e0722d5031e7f3806037ac913c814b0cfee54464f06978e" "sha256:bd5c874d2eb81329f220174cc0dde7be385f4574ce6c8a2d1579d7fd564a94f3"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.8.3" "version": "==1.7.3"
}, },
"django-mass-edit": { "django-mass-edit": {
"hashes": [ "hashes": [
@@ -296,19 +296,19 @@
}, },
"django-recurrence": { "django-recurrence": {
"hashes": [ "hashes": [
"sha256:0c65f30872599b5813a9bab6952dada23c55894f28674490a753ada559f14bc5", "sha256:715f681f6af029ff3a8d73c7b1460abd8cbc5d5a5001efcb127032e84d9cb963",
"sha256:9c89444e651a78c587f352c5f63eda48ab2f53996347b9fcdff2d248f4fcff70" "sha256:9053b44b78b7fbfe3530673edfdd6d2f562105f8a192bc6a4b906a3df4f95f59"
], ],
"markers": "python_version >= '3.7'", "index": "pypi",
"version": "==1.11.1" "version": "==1.10.3"
}, },
"django-registration-redux": { "django-registration-redux": {
"hashes": [ "hashes": [
"sha256:2213bbe8732be72724034f4146f0255a7bd666eb5a5e1b2d8d8aa633fe8af894", "sha256:5079dd36980cc0faddf91a6e991129680410611b1059d8154d064cc0146744b2",
"sha256:56fbc7b01a7f0f48812fe4d4e0729d2dac916e16f8aaed36b3f10129f2df9d0f" "sha256:88eb98530d98a7e3451bf728c0a5f6fe7ea2f45c65ef18f619ef37b940c854f5"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.12" "version": "==2.11"
}, },
"django-reversion": { "django-reversion": {
"hashes": [ "hashes": [
@@ -367,11 +367,11 @@
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", "sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b",
"sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" "sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.0.0" "version": "==5.1.0"
}, },
"lxml": { "lxml": {
"hashes": [ "hashes": [
@@ -466,71 +466,61 @@
}, },
"msgpack": { "msgpack": {
"hashes": [ "hashes": [
"sha256:04366c754ac3bfecf589ea0578599f0c26a3b6558e44cc94d5078bedc67ebfb8", "sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467",
"sha256:0a8fed756d52f8e8e45e1cb1eac83d96349d563997eed417ffd80eaac426e49e", "sha256:0a68d3ac0104e2d3510de90a1091720157c319ceeb90d74f7b5295a6bee51bae",
"sha256:12a5f5e5279a37909ed41dab91b20cc41d6423ddf944141e2d2cf41517f3b119", "sha256:0df96d6eaf45ceca04b3f3b4b111b86b33785683d682c655063ef8057d61fd92",
"sha256:13eb94148866fe4f6f93a5253bab1b12b3976c1c859b6b11f3ca7be581f20c12", "sha256:0dfe3947db5fb9ce52aaea6ca28112a170db9eae75adf9339a1aec434dc954ef",
"sha256:1c19803007800ed7ff492b21dc84872ea2ef7577800c97939a50f1ecef099fb2", "sha256:0e3590f9fb9f7fbc36df366267870e77269c03172d086fa76bb4eba8b2b46624",
"sha256:1e600cb89997f4cda23f93b29c9ad4ae09884573ec87476d46df264b86a92cc3", "sha256:11184bc7e56fd74c00ead4f9cc9a3091d62ecb96e97653add7a879a14b003227",
"sha256:20a26548e6fbd0998846d51835d79e2c9a1542d11228872baec61baf87264e92", "sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88",
"sha256:2371e14ff3b17f5774f50602fb139e1df39ee3ca44eb3ae82683ac9b1db5e4ed", "sha256:1276e8f34e139aeff1c77a3cefb295598b504ac5314d32c8c3d54d24fadb94c9",
"sha256:290f9a656d34aa20cb672ee11ebd5c6647d08419c88614823562997ecb566c16", "sha256:1576bd97527a93c44fa856770197dec00d223b0b9f36ef03f65bac60197cedf8",
"sha256:2cd4e24daff07eedf168f6e7db1b2c0831bed748d8b7254053d4b2334c206ed5", "sha256:1e91d641d2bfe91ba4c52039adc5bccf27c335356055825c7f88742c8bb900dd",
"sha256:318956e96edd3c02a183e96af10f471c1fa18c29add5c317871de3532302609c", "sha256:26b8feaca40a90cbe031b03d82b2898bf560027160d3eae1423f4a67654ec5d6",
"sha256:31b4112b43af2a78d005c9192d2a5f0cec62c6a731ca93e77a0d3979da585d9b", "sha256:2999623886c5c02deefe156e8f869c3b0aaeba14bfc50aa2486a0415178fce55",
"sha256:3729619996e9a0db56d5dc00de1d72e401aee6695d59cbfb62815a5605c66cdb", "sha256:2a2df1b55a78eb5f5b7d2a4bb221cd8363913830145fad05374a80bf0877cb1e",
"sha256:42418455bb0aba4591f8f90ac4b783834e6cb0d880c0b92a71423bf59ccc38b9", "sha256:2bb8cdf50dd623392fa75525cce44a65a12a00c98e1e37bf0fb08ddce2ff60d2",
"sha256:44b913a7b9a4a7726bb004aed024670682669a15f77dc2ad8d87a179d9e26e94", "sha256:2cc5ca2712ac0003bcb625c96368fd08a0f86bbc1a5578802512d87bc592fe44",
"sha256:4655afa670c7f05bb560a00640d725629c3f2d4f36267c0d3b9645bdecee9b74", "sha256:35bc0faa494b0f1d851fd29129b2575b2e26d41d177caacd4206d81502d4c6a6",
"sha256:469c8f3d9458b0d4fc2fa691b914eced40465a95a623e87f75bc40a74e31dfea", "sha256:3c11a48cf5e59026ad7cb0dc29e29a01b5a66a3e333dc11c04f7e991fc5510a9",
"sha256:47d9123a621b18b4c7a63739acbb56de4f89b92b3e493cb165593474cff3c60f", "sha256:449e57cc1ff18d3b444eb554e44613cffcccb32805d16726a5494038c3b93dab",
"sha256:4df078e1a38a26d9f8addabf0df24fcf0abc2161bb7b43b2cfdd178d8a127a12", "sha256:462497af5fd4e0edbb1559c352ad84f6c577ffbbb708566a0abaaa84acd9f3ae",
"sha256:4e4d1c09fe6a3104a001e6197e46e34237f1858ca470b97a87cb7d29fdc359fe", "sha256:4733359808c56d5d7756628736061c432ded018e7a1dff2d35a02439043321aa",
"sha256:512df5ec1f97ae44c3307049be05cc901b255b297aae5c88508e3058a3874270", "sha256:48f5d88c99f64c456413d74a975bd605a9b0526293218a3b77220a2c15458ba9",
"sha256:53cbf882e4b11aba6cdeec41abe576d4cc7dbf22e7a431f95d8127b32768709f", "sha256:49565b0e3d7896d9ea71d9095df15b7f75a035c49be733051c34762ca95bbf7e",
"sha256:556c17b6bbfeb5e31e52baa3e39d04e863dabd98b459538f73aa958bc4bc4043", "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250",
"sha256:5629026acea9c4e2c2e684de7b313ef82e516e2e88049b3eefcc6316da43ce40", "sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce",
"sha256:5d73c893dd03129c67cb2bea65733bdf1c52cf78e51fb599b81146c1ae8a51f0", "sha256:4dea20515f660aa6b7e964433b1808d098dcfcabbebeaaad240d11f909298075",
"sha256:61b202019a014ad3e7e5953430fe5838125196ad4fb27c15e521b22724add939", "sha256:545e3cf0cf74f3e48b470f68ed19551ae6f9722814ea969305794645da091236",
"sha256:631bdeacad61e2bdee929835622025131d9971bd9aed4cbad9e44a46caa42069", "sha256:63e29d6e8c9ca22b21846234913c3466b7e4ee6e422f205a2988083de3b08cae",
"sha256:6322b441d0ddab56ca5e79904dd2f79494d33636fdf53be0d01a23ebb56d2613", "sha256:6916c78f33602ecf0509cc40379271ba0f9ab572b066bd4bdafd7434dee4bc6e",
"sha256:669450ebc749e8ac27d07b750643e8e2ff8976ba95ebcc2e12eb00999f3cf500", "sha256:6a4192b1ab40f8dca3f2877b70e63799d95c62c068c84dc028b40a6cb03ccd0f",
"sha256:68726d2404250b6b3b3e63df7e2c4243d46846c630d356a8d129f4aec72ced56", "sha256:6c9566f2c39ccced0a38d37c26cc3570983b97833c365a6044edef3574a00c08",
"sha256:6e733b50bbcedd04e82922c80e7f045530f8bd19ce004c006316eef511b623bb", "sha256:76ee788122de3a68a02ed6f3a16bbcd97bc7c2e39bd4d94be2f1821e7c4a64e6",
"sha256:7d18a179e7e26da21f85e3b807f317316da28c62f4213e6864191fa9aabe482a", "sha256:7760f85956c415578c17edb39eed99f9181a48375b0d4a94076d84148cf67b2d",
"sha256:90703d9c8eae435fcb2f84a545183a23670b5662e6e9e7ee6dfdcd8f69a373f5", "sha256:77ccd2af37f3db0ea59fb280fa2165bf1b096510ba9fe0cc2bf8fa92a22fdb43",
"sha256:969e6ee8f82b7ff0f831b1d3ceb84eafe9b58f5300cc024a96041c7a8c20d559", "sha256:81fc7ba725464651190b196f3cd848e8553d4d510114a954681fd0b9c479d7e1",
"sha256:9c57c6730e94801b341c87d56edbf923165dda6d000f2c1c1d5fb74f257cd802", "sha256:85f279d88d8e833ec015650fd15ae5eddce0791e1e8a59165318f371158efec6",
"sha256:a34b0dfb71eb8807cf082d59c0666715df51fc49e734c0f171df5bbb86e02570", "sha256:9667bdfdf523c40d2511f0e98a6c9d3603be6b371ae9a238b7ef2dc4e7a427b0",
"sha256:a43019ea96dc4632dc2626c76b5413e5a4e1294781e9f5241435076897140594", "sha256:a75dfb03f8b06f4ab093dafe3ddcc2d633259e6c3f74bb1b01996f5d8aa5868c",
"sha256:aa9a797de3c755e9bb47a8c6f592b4c0dbb296cee584d3cd0e36b53be0c31e80", "sha256:ac5bd7901487c4a1dd51a8c58f2632b15d838d07ceedaa5e4c080f7190925bff",
"sha256:bbe299a9e7b7d24e688f1e4dac09eb5b01d8eb8eaca944aae5d8f8aef6c73c37", "sha256:aca0f1644d6b5a73eb3e74d4d64d5d8c6c3d577e753a04c9e9c87d07692c58db",
"sha256:bea6b16a3537ad712bc9b7189970bdf28c56a0cec0a0b46a9f3db3ac0a853335", "sha256:b17be2478b622939e39b816e0aa8242611cc8d3583d1cd8ec31b249f04623243",
"sha256:c65fd6feb88efe81765b51ad1150b9db682794fb2ab6ddf0e77a6fb4750eca92", "sha256:c1683841cd4fa45ac427c18854c3ec3cd9b681694caf5bff04edb9387602d661",
"sha256:c81463959da83fc74ff9bfba7d0a5c6d21b44e799f78c28fe57c75b300160f5d", "sha256:c23080fdeec4716aede32b4e0ef7e213c7b1093eede9ee010949f2a418ced6ba",
"sha256:cb4a0545afb15189601c1e4e7cf82765456ef45985dc293297c854c4045afe31", "sha256:d5b5b962221fa2c5d3a7f8133f9abffc114fe218eb4365e40f17732ade576c8e",
"sha256:cbd3af673fa93706c59e66519f6110d4a317892ddeae7a9718dde3e0e9a9a6df", "sha256:d603de2b8d2ea3f3bcb2efe286849aa7a81531abc52d8454da12f46235092bcb",
"sha256:ceed735d624af7e1834db1995ad293389e66306025c7c791db2ac42e006dbd25", "sha256:e83f80a7fec1a62cf4e6c9a660e39c7f878f603737a0cdac8c13131d11d97f52",
"sha256:cf7aec2bf2ff7bf7e8a07de04b593c1076f51941a28dd23d2af5b07c23f60ee9", "sha256:eb514ad14edf07a1dbe63761fd30f89ae79b42625731e1ccf5e1f1092950eaa6",
"sha256:d1960d6c57e30f60c132e2649e5fefb0bd29b1b55c707c0c5ecfa7f08def82d1", "sha256:eba96145051ccec0ec86611fe9cf693ce55f2a3ce89c06ed307de0e085730ec1",
"sha256:d6788d652256e38b19f7578eb7dd4f96de10fe20546ebf5519bef22aa18c6109", "sha256:ed6f7b854a823ea44cf94919ba3f727e230da29feb4a99711433f25800cf747f",
"sha256:d6a73d8f30e06562efc35f5f9699221eb240b18691807b32ef29bae7f66e0da1", "sha256:f0029245c51fd9473dc1aede1160b0a29f4a912e6b1dd353fa6d317085b219da",
"sha256:d896df74ce25ff2e0b2d5bdd0344eff01e05814cd9b168f9321bd459f476981e", "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f",
"sha256:d98a89e53df1540f3f465a510b511e97d21e1b1777b9f5e030184e1cc68d1072", "sha256:fb62ea4b62bfcb0b380d5680f9a4b3f9a2d166d9394e9bbd9666c0ee09a3645c",
"sha256:da5db8a4d8b532bbe1e4aa1fabfb21f49f30ee7db49d4885c448c7a9ea032138", "sha256:fcb8a47f43acc113e24e910399376f7277cf8508b27e5b88499f053de6b115a8"
"sha256:dfdacd510bc0f73125aa3e496243ebf768f0eb6478243867607f3b247451fb6f",
"sha256:dff7f7c68435a7b7b570b75f8c71ab986681e04767e10eefc178105c698495b1",
"sha256:e4f6a2b90746c8bca7f3742e38b8ce8fc6ad4a0b63e938c135ea0d578857aff8",
"sha256:e63c6d85f23243d9ed15aaff826a2330a8be33d09b8d808602dbe8d2b596a89f",
"sha256:e8667a1ecb0a70d612992516a9483dce35d5e452430832cca4f01899e8da6da7",
"sha256:f25c3553c5b7b07ecff4a3b88024477a08b568edf9566cccb662b31803649919",
"sha256:f2c3692b13e8c26aa54a87318861d80b1b0d2adbfa3fb81b05d54a6e56083958",
"sha256:f9b6d3689fac019f10091cdaf5ff95458a8ccdadfd5598bb0be92cf888feeace",
"sha256:fb0db88c3db68a938f4f930c34570b9b5b050e43ac611bcfd8506303d0ff2d4f",
"sha256:ff54f758e67d2ed70121b99f35929801a02086bfd544dfc40a9cee59a3f04c8d"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.0.5rc1" "version": "==1.0.4"
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
@@ -550,42 +540,45 @@
}, },
"pikepdf": { "pikepdf": {
"hashes": [ "hashes": [
"sha256:0a0914d482aa1b80584659c44aef1b2770b473a504cedc209fa6db3f24575ef6", "sha256:08ca2566c3ddcb633bbfb90c9e2b7134f222ab48f642f056c266fd69a4c0844e",
"sha256:1b8dfdc2184aca33e271b104e0ec468e52ac6591ab51bcd32c2e53bc8cdeeffe", "sha256:15eff12d7b5dc4eeeb9fbe5148d5e93cdc9f632a352d7df5191409837cdada6f",
"sha256:1faed2d2553aafb6f4969f0a970958d1847869631eb1c82f5a91ef636817b93a", "sha256:24717a49cab8cf0ccb360ae38fefe4b09e23d394086ef6da740071165a2542d8",
"sha256:260efb3c6aa44c013da2278872593bc4712facd5b766de2b2d88c53c5f524449", "sha256:2c1a3a1ae0d787a6819ffb9d456b0ccbf2b8d48ad8b0748f765a6a306b92184b",
"sha256:3af2fd5762e222bf5133acec4f7d56719dda4a9b7dd468eb1c37a10055ff64b2", "sha256:2c653bc771cc6065642f5a734ecb771ff16b5412cf05a5c3435072ac01f84fbe",
"sha256:3e1490beb13b2d1a509aeb98fb0669ab7dea4035abd1df0e12085393f556654a", "sha256:2e9c9cc68bc123b88007b321a494ab054c3ce378ee2f77bba1b90c8bdaaad59e",
"sha256:422cb31ed4b489b9e18f4a803fab7c6ea10ef6916960a5d8b5e531c5af3bfbca", "sha256:32a7775f8b2117e72fa101427edf4526f0886e785146a7bfdf2a877d27f94dbf",
"sha256:4290059bdf8d05cf3a7ba185d64b5756c745f178fc102aec41bbcd4a057e02f8", "sha256:4675c743ac873d8a1ff9d06c1d5fab15e9ca66aa1abf73fcbca4b86c976b214e",
"sha256:5aca06b88b2d53122baaf3009bcfaec291b3c408846e401cddf8b2e89a0e0fe6", "sha256:4800fd878876d9a780e2d1125e971d680a0de466440bd6bcbc54ac7cfb925fd6",
"sha256:7eef9fe4d06cb01482486561292b3c3675d7506328f990cb60b26994dd7ac1d8", "sha256:5b1271b200ece3e16ffa97c76ff93c057d3676f2ecee89ae52efd5e4804874ab",
"sha256:80d4eb3624980c1292d7e2db66c569f146012e86004b8739a3580346ee8f69a9", "sha256:5b1c975d02e75aa8a0e09679ed23f85b6eddf91ac7c85189dc18f1ef8599ce70",
"sha256:81caa67a08fdf683c521497fafd48d9b7bdf02549625329d8a1bb8ce706ef362", "sha256:5b295d9579081ab46ce7bb463eccbc62e0c60cbe546f29c0a9135abd5cf76a24",
"sha256:910a45cc6506dd899032638c3775f708278d99ccc9c3681fb75a57b55051d262", "sha256:5f040aa7d9327b6d9087848dc8e35d2b8515bf17a04e2bdb20f8c51931810213",
"sha256:93cd0357140dfa79e16c1d9249775d11eebb392665fcdb1528684aae71966b4e", "sha256:62d724e37a2f004f63615513d3f877334e679dd43b6c55cdfe9b8af56ff52400",
"sha256:93d98f460eb209b89ce855a5defb059ca82326042ee52dcb692a05e1c1a24bef", "sha256:6452fa9d10b15bb60b2343b7ac08123e0c5b87f852250538d32bf556f09b8c3a",
"sha256:9411824a7aa477fcad209e6e01cf23f0ecbd6833805812eca2026e372c724096", "sha256:66679ee6b364b3b9659a72697e053554e2f4c231371173745b59114f00a821e5",
"sha256:a32215111f6713c934b9ed9a6fd686940559953539e81f70bfb860efbddfe3c3", "sha256:6b21fde29bd006cb0889e05a0407f0825b5650ae22272ba70720fc68a2e44a0e",
"sha256:a59559a1e480f4a7229f600d6fce22b1d32729df8552099542e37a95c02a572f", "sha256:788df6789df19a381976f6644b78a65da92c5d4fd45714682a1cffb0cd3ff410",
"sha256:ab07529096da5ea410dea81add8d724ac91f093b982b86f503771bfe8864e48c", "sha256:7a6de8d04df3dc4f37ddbdfdbecdb4c3d20d479699260902ec06fa531cbfbae6",
"sha256:b0f1ca24118517970cfc78f902ded681c2e399dfb21ff04745143b4156dd551e", "sha256:80a4546f59bb38121c20fc37305a388b8f9d561c6bbcc77a3415afdd8a7ab16d",
"sha256:b7711e2c0ff1ad265f5c0497f519584c029c9906d72ad78e89e260c57964a5c0", "sha256:856e527970fa2516858662906173004568a38f95b46ef95c8231c529f3e71581",
"sha256:b860c44503b6bb237b97c2ae06d47dc645140649d61d2f5dd5276438bc60e2e0", "sha256:8874d4d38b2da71837352576c5ec10c03b2a7976124b95721564d8c456c9824b",
"sha256:b90bb4e77f9ea8a21d94c5d25e1b416f08a668377ea48edf8808be49f09f906a", "sha256:913437609c3e5b9109a2fe1100f52bdcead4e56e8c83ca8a9c3b520cd7720016",
"sha256:ba75e70c932830fb7253b343f43f0ce03317661971cd3df03fc27c7cdb06992e", "sha256:91fd023805733163927ae04b16d48374f04f03f204f2cfa45d442b052314de83",
"sha256:bf9129507b7258dda27845e1ee6c7c4121674a04c0a1a0ac1115c19e5b4a2edf", "sha256:9d9fe5e3fbd54a45fe7678c08d24c1e8347a1e48adcb4f32c6c25ef380f056ca",
"sha256:cf912ab70313fd0f23a32811f8c3ea815a716ecfd6ee84a564c833d0c06f5483", "sha256:a98fcebb4303b8b2f5cde0d56f2406a53e74a9ad4bc1e1bc12bdc2b3dd4def1f",
"sha256:dec854f908973c5c3760d246539c58c03b7b701a2bed45173f9a4e4d766d3eab", "sha256:aa7d3831fa016634f2ead255717644ba2dddf462117b8558b783ac2ba94583bb",
"sha256:e5bebaca43757c9c357637954b8e49c9221c21a40260394ec4c4fbafada5ee87", "sha256:ae0ba99841d80107becaec6ed067b63c455f51a9bb83b0e88efd3a098753735b",
"sha256:e900d2314a1019c98c9d3d50445af475ce2f16e8a54e44875201869f8561a3b2", "sha256:b77e2e134315d25dd004b4ffbd2d5032352da164b21bc3bd92768379610fa2b5",
"sha256:eb310e903b9a172de352446458390ccce31a2bb19a387f63d37b135cb4cca3f1", "sha256:c5a2710aacb1fa25edd77feba51dcadf85a9b735f2190e8b45edf0e1e79b5d1d",
"sha256:edea85799240a3b7534650b275bc7e87519ddf0f47ccf9bdad09b22381da5442", "sha256:c768e55caf96024218866a5b5f95fdded2ef001b2d5463d1c89e57d07bdab928",
"sha256:f7f0bfa897ecabbee3f7dc2ac10a8dd954c42a6f858fbef13019d0cef1b60127", "sha256:d55b1d8eb3b17e55682b91c042c48e2d0c67be4c1e30b7e539a82e03977eed29",
"sha256:fcfd391b3a255b460a9040ca8e47dd6f36c6ea43d2b61cd00b5ecb06000a6b8f" "sha256:d57a35b3ed3f8ef98aa528d4981d1376ccc1ba64ab937f2ac2ed0aadee1d5911",
"sha256:e35110f69406e5b9cf6330d16106877372f6aa955fdc1a920d53babcd6a5ae3d",
"sha256:e3ff858d504312ea07519121d3ecdf1b5bc40541f957d2b85d271a2ca6284b70",
"sha256:f721cd86d8ddf0624306fcd1fed3e086387fcb2e32baeafbd1ba92a7c40c5563"
], ],
"markers": "python_version >= '3.8'", "index": "pypi",
"version": "==7.1.1" "version": "==6.2.5"
}, },
"pillow": { "pillow": {
"hashes": [ "hashes": [
@@ -873,19 +866,19 @@
}, },
"sentry-sdk": { "sentry-sdk": {
"hashes": [ "hashes": [
"sha256:69ecbb2e1ff4db02a06c4f20f6f69cb5dfe3ebfbc06d023e40d77cf78e9c37e7", "sha256:675f6279b6bb1fea09fd61751061f9a90dca3b5929ef631dd50dc8b3aeb245e9",
"sha256:7ad4d37dd093f4a7cb5ad804c6efe9e8fab8873f7ffc06042dc3f3fd700a93ec" "sha256:8b4ff696c0bdcceb3f70bbb87a57ba84fd3168b1332d493fcd16c137f709578c"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.15.0" "version": "==1.11.1"
}, },
"setuptools": { "setuptools": {
"hashes": [ "hashes": [
"sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330", "sha256:95f00380ef2ffa41d9bba85d95b27689d923c93dfbafed4aecd7cf988a25e012",
"sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251" "sha256:bb6d8e508de562768f2027902929f8523932fcd1fb784e6d573d2cafac995a48"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==67.4.0" "version": "==67.3.2"
}, },
"simplejson": { "simplejson": {
"hashes": [ "hashes": [
@@ -964,11 +957,11 @@
}, },
"soupsieve": { "soupsieve": {
"hashes": [ "hashes": [
"sha256:49e5368c2cda80ee7e84da9dbe3e110b70a4575f196efb74e51b94549d921955", "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759",
"sha256:e28dba9ca6c7c00173e34e4ba57448f0688bb681b7c5e8bf4971daafc093d69a" "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.4" "version": "==2.3.2.post1"
}, },
"sqlparse": { "sqlparse": {
"hashes": [ "hashes": [
@@ -1027,11 +1020,11 @@
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc",
"sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.26.14" "version": "==1.26.13"
}, },
"webencodings": { "webencodings": {
"hashes": [ "hashes": [
@@ -1263,6 +1256,14 @@
} }
}, },
"develop": { "develop": {
"async-generator": {
"hashes": [
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
],
"markers": "python_version >= '3.5'",
"version": "==1.10"
},
"attrs": { "attrs": {
"hashes": [ "hashes": [
"sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836",
@@ -1279,70 +1280,154 @@
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==2022.12.7" "version": "==2022.12.7"
}, },
"chardet": { "charset-normalizer": {
"hashes": [ "hashes": [
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42",
"sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d",
"sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b",
"sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a",
"sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59",
"sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154",
"sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1",
"sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c",
"sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a",
"sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d",
"sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6",
"sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b",
"sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b",
"sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783",
"sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5",
"sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918",
"sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555",
"sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639",
"sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786",
"sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e",
"sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed",
"sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820",
"sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8",
"sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3",
"sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541",
"sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14",
"sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be",
"sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e",
"sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76",
"sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b",
"sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c",
"sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b",
"sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3",
"sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc",
"sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6",
"sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59",
"sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4",
"sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d",
"sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d",
"sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3",
"sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a",
"sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea",
"sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6",
"sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e",
"sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603",
"sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24",
"sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a",
"sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58",
"sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678",
"sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a",
"sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c",
"sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6",
"sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18",
"sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174",
"sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317",
"sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f",
"sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc",
"sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837",
"sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41",
"sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c",
"sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579",
"sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753",
"sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8",
"sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291",
"sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087",
"sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866",
"sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3",
"sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d",
"sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1",
"sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca",
"sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e",
"sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db",
"sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72",
"sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d",
"sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc",
"sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539",
"sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d",
"sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af",
"sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b",
"sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602",
"sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f",
"sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478",
"sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c",
"sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e",
"sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479",
"sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7",
"sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"
], ],
"index": "pypi", "version": "==3.0.1"
"version": "==4.0.0"
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
"sha256:00858a6213ea829ab417b6e05ac0a4c22eac7d3aae67c0187de2935d0548786b", "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79",
"sha256:08fd9ad5dfc490b7403027b20eebb8ac470621ae1ce0b33a13cab9ec8d4aed0a", "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a",
"sha256:0c52c8f0243a2e4c0b81db2f6468a9084dd380e0b69e931253aa24529eb812f3", "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f",
"sha256:0e857ef99769a54595c8801086e310dabb8205a1e742d66f6702544aeddfb1ba", "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a",
"sha256:1b1cca186e74d258d983a1e1a134ffba0b991effbc8e46ee65c5fbf4009dfce1", "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa",
"sha256:1dcb5c17b361b35d2a339c6031417f8dcce915b09ea55e7214a398833ec9a63f", "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398",
"sha256:22cca1925841e2655ce35a4e17c21b42dd0de2b85c5d6fe9c5bf4a45f58950f3", "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba",
"sha256:25ab1d4ae4bce324d427732bf0f7967493405daa0c2675385016102b0a5e87bf", "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d",
"sha256:2ca9c7735da025b0f0ca00ab15c5290798b62a49feaf312cb895ab4c4bc1575e", "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf",
"sha256:3008ace59d566e110e9351c855c6bf2f2b4037f772caffdfaa977c485bf96e8e", "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b",
"sha256:3de7363b0f21ac6fc97767f78036b900006e06eadd3cc72f040d57494405f44c", "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518",
"sha256:409d14e37de692f94689578cbbb0a26408da9d9354f8ff658e148f1750940b2f", "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d",
"sha256:425ba4ae75be4e2c9ce336a523265e6e1214ad624e8d18fb638771475dec2ebf", "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795",
"sha256:42d7ee01583d4db8098510d08e7505db0f5dbb70edc88a7350cafc336ae81048", "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2",
"sha256:4e07fc0e0dc8bdeae4f23b8ff821c711dcb2537bbc782f61ff726ca07fcfdb9b", "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e",
"sha256:4f32113f131edcd26266b8bfc9e24698b6dee4d9ea63362b7dd3cf0a351231a6", "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32",
"sha256:539ca9a37aaab0ed31ccb535039e33170cf2144b8cd5006c48ad724ba2ab5797", "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745",
"sha256:572867facf73374a9c8686691bf1b43abe3425f31a2a9b48043d0de9f669ad0d", "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b",
"sha256:583f5b9a414fcabe1c14d82519b9d24dfd69c3505e9030415c3c6b692bff9062", "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e",
"sha256:590411083d46c182e852e879a533fa99988937a3af96f836cacbc16a1bcfd058", "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d",
"sha256:5a0bc8377854cc2f447093149bc9774b0628e9db218f85026d7982466840040e", "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f",
"sha256:5f3ee269b6d32913eccd78eefce6da7d5120d8fbef059c463c028267c1a0d1ce", "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660",
"sha256:639bc5e8cf323a50d17b52554269e72c21c2cb5ad14ca1b43e095ce60abbc2b3", "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62",
"sha256:644b9c4e7e951aa210a8150b09f9c02dcea8701d14bff1564a50e054ce0ae48d", "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6",
"sha256:6591db6f6bbd5120c9475fdb12305a3216355ae4797b0e44528040f6d0d8f73f", "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04",
"sha256:7538a24505abb5dc61ac3bbf58d5232a76ad6fd2be63cf797c2e1caa9c60077c", "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c",
"sha256:75598efc204f513cc4d5ca99a8f9103867993c091e5cd62d78c1020a0affc7be", "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5",
"sha256:7911833a156476096d209569cbe600faf22a057a46c5e8bb19fffc387abad101", "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef",
"sha256:7f96cd694673191583acaef50ed01c8db3b47f49602b7046a15775fa6f753e9f", "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc",
"sha256:89230ec0b1f3817237a8f98fc593dec061eebd753cea097772e7abcf5fb9c6bc", "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae",
"sha256:959d65e8c5f84878a741dcddcbf71ccc22270c6981e5dfe0806517d49be0c1f2", "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578",
"sha256:98220679df217b9635c3c6a7a490c408f4de169c33ac4f708a86f9e97b2d9b14", "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466",
"sha256:996c74a93f6fac2099c288e709e7d0bcc37f3c700d878d7d52accdcc2b6550bd", "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4",
"sha256:9ec68b342a82dc821d4384e7a5b266c2b78bc5ec3a59fcadf8e96445f4002366", "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91",
"sha256:b3b6582423aeece24478028b8c9127cd1392d584dfade6c925421c91710cdad5", "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0",
"sha256:b48273db5287a185017f2150eb49581245777ba30c6e749bfd5567afcab27c3f", "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4",
"sha256:c1b862d4718a103cd090b6b91155503574918c498a381a13970e22785c7ae5a3", "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b",
"sha256:c87885ca7357e85e9e1550d804c7b2c42d6e4e8260849af499fc2b0dfe58962c", "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe",
"sha256:c976faf3bed96d2b94ee8b005ff26a075cfbc00782b532342119cbd172481f81", "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b",
"sha256:ca922b6558e1fe09c2ffc772faaa411f94cb47845d366d7aa6a887d934a25200", "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75",
"sha256:e0101d0cb004db88891ffb87ddfccd93ee76abbe4c0bf784c4214f467d026dd2", "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b",
"sha256:e7a0f9ab01ccd873d21584ddcad488ad752944f6a9e5bdff1aefbe5289ffd823", "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c",
"sha256:e83b73d8edf255187388b8d14c0c0df580bbf8e7099060e590915e3fbcf39598", "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72",
"sha256:e8b80f94e15676dbaa7ce2cef6e5433cdb2427d3d81ea9fe4c3d788fae3bc4a2", "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b",
"sha256:e9c1a662c837cf9b4b815f977404475b555fafc0fb12ae92667d0cdbf0f3c9eb", "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f",
"sha256:eb54a60e3819d60de6828b5bd197996f9ecb2306d280bd532a4a4291f3285658", "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e",
"sha256:efb09e5004fd1e4d05cf433d7ad7a6784d090c0afd68b46e8ef785ae169a31ed", "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53",
"sha256:f0ae2fd15eedb5f749cd9b3da01087b7dba2f76cc783866459d8b3f3feb7c969", "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3",
"sha256:f0aefc3015ae4a188dc48f2ea934ddbdf158c8c4b0b3d5691acfdad684857702", "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84",
"sha256:f47e5f3c5acbc3b843ae89b042faf64b366a4976099813487161ce3c50649db3", "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"
"sha256:fd868a0eb8eb35a84847935fe36a5b285fed2e4b99c2b90cf44778fa0e9418e9"
], ],
"markers": "python_version >= '3.7'", "markers": "python_full_version >= '3.7.0'",
"version": "==6.6.0b1" "version": "==6.5.0"
}, },
"coveralls": { "coveralls": {
"hashes": [ "hashes": [
@@ -1382,6 +1467,14 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.9.0" "version": "==1.9.0"
}, },
"h11": {
"hashes": [
"sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
"sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==0.14.0"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
@@ -1395,9 +1488,17 @@
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
], ],
"markers": "python_version >= '3.7'", "markers": "python_full_version >= '3.7.0'",
"version": "==2.0.0" "version": "==2.0.0"
}, },
"outcome": {
"hashes": [
"sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672",
"sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==1.2.0"
},
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
@@ -1464,13 +1565,21 @@
"index": "pypi", "index": "pypi",
"version": "==2.2.4" "version": "==2.2.4"
}, },
"pysocks": {
"hashes": [
"sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
"sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
"sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
],
"version": "==1.7.1"
},
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71",
"sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"
], ],
"index": "pypi", "index": "pypi",
"version": "==7.2.1" "version": "==7.2.0"
}, },
"pytest-cov": { "pytest-cov": {
"hashes": [ "hashes": [
@@ -1509,11 +1618,11 @@
"psutil" "psutil"
], ],
"hashes": [ "hashes": [
"sha256:336098e3bbd8193276867cc87db8b22903c3927665dff9d1ac8684c02f597b68", "sha256:40fdb8f3544921c5dfcd486ac080ce22870e71d82ced6d2e78fa97c2addd480c",
"sha256:fa10f95a2564cd91652f2d132725183c3b590d9fdcdec09d3677386ecf4c1ce9" "sha256:70a76f191d8a1d2d6be69fc440cdf85f3e4c03c08b520fd5dc5d338d6cf07d89"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.2.0" "version": "==3.1.0"
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
@@ -1533,11 +1642,26 @@
}, },
"setuptools": { "setuptools": {
"hashes": [ "hashes": [
"sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330", "sha256:95f00380ef2ffa41d9bba85d95b27689d923c93dfbafed4aecd7cf988a25e012",
"sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251" "sha256:bb6d8e508de562768f2027902929f8523932fcd1fb784e6d573d2cafac995a48"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==67.4.0" "version": "==67.3.2"
},
"sniffio": {
"hashes": [
"sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
"sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==1.3.0"
},
"sortedcontainers": {
"hashes": [
"sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
"sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
],
"version": "==2.4.0"
}, },
"splinter": { "splinter": {
"hashes": [ "hashes": [
@@ -1554,13 +1678,37 @@
"markers": "python_version < '3.11'", "markers": "python_version < '3.11'",
"version": "==2.0.1" "version": "==2.0.1"
}, },
"trio": {
"hashes": [
"sha256:ce68f1c5400a47b137c5a4de72c7c901bd4e7a24fbdebfe9b41de8c6c04eaacf",
"sha256:f1dd0780a89bfc880c7c7994519cb53f62aacb2c25ff487001c0052bd721cdf0"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==0.22.0"
},
"trio-websocket": {
"hashes": [
"sha256:5b558f6e83cc20a37c3b61202476c5295d1addf57bd65543364e0337e37ed2bc",
"sha256:a3d34de8fac26023eee701ed1e7bf4da9a8326b61a62934ec9e53b64970fd8fe"
],
"markers": "python_version >= '3.5'",
"version": "==0.9.2"
},
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc",
"sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.26.14" "version": "==1.26.13"
},
"wsproto": {
"hashes": [
"sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
"sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==1.2.0"
}, },
"zope.component": { "zope.component": {
"hashes": [ "hashes": [
@@ -1570,22 +1718,6 @@
"index": "pypi", "index": "pypi",
"version": "==4.6.2" "version": "==4.6.2"
}, },
"zope.deferredimport": {
"hashes": [
"sha256:57b2345e7b5eef47efcd4f634ff16c93e4265de3dcf325afc7315ade48d909e1",
"sha256:9a0c211df44aa95f1c4e6d2626f90b400f56989180d3ef96032d708da3d23e0a"
],
"index": "pypi",
"version": "==4.3.1"
},
"zope.deprecation": {
"hashes": [
"sha256:0d453338f04bacf91bbfba545d8bcdf529aa829e67b705eac8c1a7fdce66e2df",
"sha256:f1480b74995958b24ce37b0ef04d3663d2683e5d6debc96726eff18acf4ea113"
],
"index": "pypi",
"version": "==4.4.0"
},
"zope.event": { "zope.event": {
"hashes": [ "hashes": [
"sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42", "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42",
@@ -1697,52 +1829,6 @@
], ],
"index": "pypi", "index": "pypi",
"version": "==5.2.0" "version": "==5.2.0"
},
"zope.proxy": {
"hashes": [
"sha256:00573dfa755d0703ab84bb23cb6ecf97bb683c34b340d4df76651f97b0bab068",
"sha256:092049280f2848d2ba1b57b71fe04881762a220a97b65288bcb0968bb199ec30",
"sha256:0cbd27b4d3718b5ec74fc65ffa53c78d34c65c6fd9411b8352d2a4f855220cf1",
"sha256:17fc7e16d0c81f833a138818a30f366696653d521febc8e892858041c4d88785",
"sha256:19577dfeb70e8a67249ba92c8ad20589a1a2d86a8d693647fa8385408a4c17b0",
"sha256:207aa914576b1181597a1516e1b90599dc690c095343ae281b0772e44945e6a4",
"sha256:219a7db5ed53e523eb4a4769f13105118b6d5b04ed169a283c9775af221e231f",
"sha256:2b50ea79849e46b5f4f2b0247a3687505d32d161eeb16a75f6f7e6cd81936e43",
"sha256:5903d38362b6c716e66bbe470f190579c530a5baf03dbc8500e5c2357aa569a5",
"sha256:5c24903675e271bd688c6e9e7df5775ac6b168feb87dbe0e4bcc90805f21b28f",
"sha256:5ef6bc5ed98139e084f4e91100f2b098a0cd3493d4e76f9d6b3f7b95d7ad0f06",
"sha256:61b55ae3c23a126a788b33ffb18f37d6668e79a05e756588d9e4d4be7246ab1c",
"sha256:63ddb992931a5e616c87d3d89f5a58db086e617548005c7f9059fac68c03a5cc",
"sha256:6943da9c09870490dcfd50c4909c0cc19f434fa6948f61282dc9cb07bcf08160",
"sha256:6ad40f85c1207803d581d5d75e9ea25327cd524925699a83dfc03bf8e4ba72b7",
"sha256:6b44433a79bdd7af0e3337bd7bbcf53dd1f9b0fa66bf21bcb756060ce32a96c1",
"sha256:6bbaa245015d933a4172395baad7874373f162955d73612f0b66b6c2c33b6366",
"sha256:7007227f4ea85b40a2f5e5a244479f6a6dfcf906db9b55e812a814a8f0e2c28d",
"sha256:74884a0aec1f1609190ec8b34b5d58fb3b5353cf22b96161e13e0e835f13518f",
"sha256:7d25fe5571ddb16369054f54cdd883f23de9941476d97f2b92eb6d7d83afe22d",
"sha256:7e162bdc5e3baad26b2262240be7d2bab36991d85a6a556e48b9dfb402370261",
"sha256:814d62678dc3a30f4aa081982d830b7c342cf230ffc9d030b020cb154eeebf9e",
"sha256:8878a34c5313ee52e20aa50b03138af8d472bae465710fb954d133a9bfd3c38d",
"sha256:a66a0d94e5b081d5d695e66d6667e91e74d79e273eee95c1747717ba9cb70792",
"sha256:a69f5cbf4addcfdf03dda564a671040127a6b7c34cf9fe4973582e68441b63fa",
"sha256:b00f9f0c334d07709d3f73a7cb8ae63c6ca1a90c790a63b5e7effa666ef96021",
"sha256:b6ed71e4a7b4690447b626f499d978aa13197a0e592950e5d7020308f6054698",
"sha256:bdf5041e5851526e885af579d2f455348dba68d74f14a32781933569a327fddf",
"sha256:be034360dd34e62608419f86e799c97d389c10a0e677a25f236a971b2f40dac9",
"sha256:cc8f590a5eed30b314ae6b0232d925519ade433f663de79cc3783e4b10d662ba",
"sha256:cd7a318a15fe6cc4584bf3c4426f092ed08c0fd012cf2a9173114234fe193e11",
"sha256:cf19b5f63a59c20306e034e691402b02055c8f4e38bf6792c23cad489162a642",
"sha256:cfc781ce442ec407c841e9aa51d0e1024f72b6ec34caa8fdb6ef9576d549acf2",
"sha256:dea9f6f8633571e18bc20cad83603072e697103a567f4b0738d52dd0211b4527",
"sha256:e4a86a1d5eb2cce83c5972b3930c7c1eac81ab3508464345e2b8e54f119d5505",
"sha256:e7106374d4a74ed9ff00c46cc00f0a9f06a0775f8868e423f85d4464d2333679",
"sha256:e98a8a585b5668aa9e34d10f7785abf9545fe72663b4bfc16c99a115185ae6a5",
"sha256:f64840e68483316eb58d82c376ad3585ca995e69e33b230436de0cdddf7363f9",
"sha256:f8f4b0a9e6683e43889852130595c8854d8ae237f2324a053cdd884de936aa9b",
"sha256:fc45a53219ed30a7f670a6d8c98527af0020e6fd4ee4c0a8fb59f147f06d816c"
],
"index": "pypi",
"version": "==4.3.5"
} }
} }
} }

View File

@@ -27,7 +27,6 @@ STAGING = env('STAGING', cast=bool, default=False)
CI = env('CI', cast=bool, default=False) CI = env('CI', cast=bool, default=False)
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com'] ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
CSRF_TRUSTED_ORIGINS = []
if STAGING: if STAGING:
ALLOWED_HOSTS.append('.herokuapp.com') ALLOWED_HOSTS.append('.herokuapp.com')
@@ -36,7 +35,6 @@ if DEBUG:
ALLOWED_HOSTS.append('localhost') ALLOWED_HOSTS.append('localhost')
ALLOWED_HOSTS.append('example.com') ALLOWED_HOSTS.append('example.com')
ALLOWED_HOSTS.append('127.0.0.1') ALLOWED_HOSTS.append('127.0.0.1')
CSRF_TRUSTED_ORIGINS.append('.preview.app.github.dev')
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
if not DEBUG: if not DEBUG:

View File

@@ -206,3 +206,8 @@ class RiskAssessmentAdmin(VersionAdmin):
@admin.register(models.EventChecklist) @admin.register(models.EventChecklist)
class EventChecklistAdmin(VersionAdmin): class EventChecklistAdmin(VersionAdmin):
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by') list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
@admin.register(models.PowerTestRecord)
class EventChecklistAdmin(VersionAdmin):
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')

View File

@@ -124,22 +124,6 @@ class EventForm(forms.ModelForm):
'purchase_order', 'collector'] 'purchase_order', 'collector']
class SubhireForm(forms.ModelForm):
related_models = {
'person': models.Person,
'organisation': models.Organisation,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['start_date'].widget.format = '%Y-%m-%d'
self.fields['end_date'].widget.format = '%Y-%m-%d'
class Meta:
model = models.Subhire
fields = '__all__'
class BaseClientEventAuthorisationForm(forms.ModelForm): class BaseClientEventAuthorisationForm(forms.ModelForm):
tos = forms.BooleanField(required=True, label="Terms of hire") tos = forms.BooleanField(required=True, label="Terms of hire")
name = forms.CharField(label="Your Name") name = forms.CharField(label="Your Name")
@@ -299,3 +283,17 @@ class EventChecklistForm(forms.ModelForm):
model = models.EventChecklist model = models.EventChecklist
fields = '__all__' fields = '__all__'
exclude = ['reviewed_at', 'reviewed_by'] exclude = ['reviewed_at', 'reviewed_by']
class PowerTestRecordForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
if field.__class__ == forms.NullBooleanField:
# Only display yes/no to user, the 'none' is only ever set in the background
field.widget = forms.CheckboxInput()
class Meta:
model = models.PowerTestRecord
fields = '__all__'
exclude = ['reviewed_at', 'reviewed_by']

View File

@@ -0,0 +1,71 @@
# Generated by Django 3.2.16 on 2023-05-08 15:58
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import versioning.versioning
def migrate_old_data(apps, schema_editor):
EventChecklist = apps.get_model('RIGS', 'EventChecklist')
PowerTestRecord = apps.get_model('RIGS', 'PowerTestRecord')
for ec in EventChecklist.objects.all():
# New highscore for the most pythonic BS I've ever written.
PowerTestRecord.objects.create(event=ec.event, **{i.name:getattr(ec, i.attname) for i in PowerTestRecord._meta.get_fields() if not (i.is_relation or i.auto_created)})
def revert(apps, schema_editor):
apps.get_model('RIGS', 'PowerTestRecord').objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0045_alter_profile_is_approved'),
]
operations = [
migrations.CreateModel(
name='PowerTestRecord',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_tests', to='RIGS.event')),
('notes', models.TextField(blank=True, default='')),
('venue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.venue')),
('reviewed_at', models.DateTimeField(null=True)),
('rcds', models.BooleanField(blank=True, help_text='RCDs installed where needed and tested?', null=True)),
('supply_test', models.BooleanField(blank=True, help_text='Electrical supplies tested?<br><small>(using socket tester)</small>', null=True)),
('earthing', models.BooleanField(blank=True, help_text='Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>', null=True)),
('pat', models.BooleanField(blank=True, help_text='All equipment in PAT period?', null=True)),
('source_rcd', models.BooleanField(blank=True, help_text='Source RCD protected?<br><small>(if cable is more than 3m long) </small>', null=True)),
('labelling', models.BooleanField(blank=True, help_text='Appropriate and clear labelling on distribution and cabling?', null=True)),
('fd_voltage_l1', models.IntegerField(blank=True, help_text='L1 - N', null=True, verbose_name='First Distro Voltage L1-N')),
('fd_voltage_l2', models.IntegerField(blank=True, help_text='L2 - N', null=True, verbose_name='First Distro Voltage L2-N')),
('fd_voltage_l3', models.IntegerField(blank=True, help_text='L3 - N', null=True, verbose_name='First Distro Voltage L3-N')),
('fd_phase_rotation', models.BooleanField(blank=True, help_text='Phase Rotation<br><small>(if required)</small>', null=True, verbose_name='Phase Rotation')),
('fd_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
('fd_pssc', models.IntegerField(blank=True, help_text='Prospective Short Circuit Current', null=True, verbose_name='PSCC')),
('w1_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
('w1_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
('w1_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
('w1_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
('w2_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
('w2_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
('w2_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
('w2_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
('w3_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
('w3_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
('w3_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
('w3_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
('all_rcds_tested', models.BooleanField(blank=True, help_text='All circuit RCDs tested?<br><small>(using test button)</small>', null=True)),
('public_sockets_tested', models.BooleanField(blank=True, help_text='Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>', null=True)),
('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer')),
],
options={
'abstract': False,
'ordering': ['event'],
'permissions': [('review_power', 'Can review Power Test Records')],
},
bases=(models.Model, versioning.versioning.RevisionMixin),
),
migrations.RunPython(migrate_old_data, reverse_code=revert),
]

View File

@@ -1,39 +0,0 @@
# Generated by Django 3.2.16 on 2022-12-16 14:41
import RIGS.validators
from django.db import migrations, models
import django.db.models.deletion
import versioning.versioning
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0045_alter_profile_is_approved'),
]
operations = [
migrations.CreateModel(
name='Subhire',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, default='')),
('status', models.IntegerField(choices=[(0, 'Provisional'), (1, 'Confirmed'), (2, 'Booked'), (3, 'Cancelled')], default=0)),
('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(blank=True, default='', max_length=255, verbose_name='PO')),
('insurance_value', models.DecimalField(decimal_places=2, max_digits=10)),
('quote', models.URLField(default='', validators=[RIGS.validators.validate_url])),
('events', models.ManyToManyField(to='RIGS.Event')),
('organisation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='RIGS.organisation')),
('person', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='RIGS.person')),
],
options={
'permissions': [('subhire_finance', 'Can see financial data for subhire - insurance values')],
},
bases=(models.Model, versioning.versioning.RevisionMixin),
),
]

View File

@@ -0,0 +1,117 @@
# Generated by Django 3.2.16 on 2023-05-08 18:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0046_create_powertests'),
]
operations = [
migrations.RemoveField(
model_name='eventchecklist',
name='all_rcds_tested',
),
migrations.RemoveField(
model_name='eventchecklist',
name='earthing',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_phase_rotation',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_pssc',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_voltage_l1',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_voltage_l2',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_voltage_l3',
),
migrations.RemoveField(
model_name='eventchecklist',
name='labelling',
),
migrations.RemoveField(
model_name='eventchecklist',
name='pat',
),
migrations.RemoveField(
model_name='eventchecklist',
name='public_sockets_tested',
),
migrations.RemoveField(
model_name='eventchecklist',
name='rcds',
),
migrations.RemoveField(
model_name='eventchecklist',
name='source_rcd',
),
migrations.RemoveField(
model_name='eventchecklist',
name='supply_test',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_description',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_polarity',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_voltage',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_description',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_polarity',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_voltage',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_description',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_polarity',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_voltage',
),
]

934
RIGS/models.py Normal file
View File

@@ -0,0 +1,934 @@
import datetime
import hashlib
import random
import string
from collections import Counter
from decimal import Decimal
from urllib.parse import urlparse
import pytz
from django import forms
from django.db.models import Q, F
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
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 reversion.models import Version
from versioning.versioning import RevisionMixin
def filter_by_pk(filt, query):
# try and parse an int
try:
val = int(query)
filt = filt | Q(pk=val)
except: # noqa
# not an integer
pass
return filt
class Profile(AbstractUser):
initials = models.CharField(max_length=5, null=True, blank=False)
phone = models.CharField(max_length=13, blank=True, default='')
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
is_approved = models.BooleanField(default=False, 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 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 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})
class EventManager(models.Manager):
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 rig_count(self):
event_count = self.filter(
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q(
status=Event.CANCELLED)) # 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
@reversion.register(follow=['items'])
class Event(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)
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
description = models.TextField(blank=True, default='')
notes = models.TextField(blank=True, default='')
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
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)
# 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)
access_at = models.DateTimeField(blank=True, null=True)
meet_at = models.DateTimeField(blank=True, null=True)
# Crew management
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
on_delete=models.CASCADE)
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
verbose_name="MIC", on_delete=models.CASCADE)
# Monies
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
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 cancelled(self):
return (self.status == self.CANCELLED)
@property
def confirmed(self):
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
@property
def hs_done(self):
return self.riskassessment is not None and len(self.checklists.all()) > 0
@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 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)
# If there is no start time defined, pretend it's midnight
startTimeFaked = False
if self.has_start_time:
startDateTime = datetime.datetime.combine(self.start_date, self.start_time)
else:
startDateTime = datetime.datetime.combine(self.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 self.start_date
return earliest
@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 internal(self):
return bool(self.organisation and self.organisation.union_account)
@property
def authorised(self):
if self.internal:
return self.authorisation.amount == self.total
else:
return bool(self.purchase_order)
objects = EventManager()
def get_absolute_url(self):
return reverse('event_detail', kwargs={'pk': self.pk})
def __str__(self):
return f"{self.display_id}: {self.name}"
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.']
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 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}"
def validate_url(value):
if not value:
return # Required error is done the field
obj = urlparse(value)
if obj.hostname not in ('nottinghamtec.sharepoint.com'):
raise ValidationError('URL must point to a location on the TEC Sharepoint')
class ReviewableModel(models.Model):
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)
@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]
class Meta:
abstract = True
@reversion.register
class RiskAssessment(ReviewableModel, 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
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')
]
@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(ReviewableModel, 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")
inverted_fields = []
class Meta:
ordering = ['event']
permissions = [
('review_eventchecklist', 'Can review Event Checklists')
]
@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 PowerTestRecord(ReviewableModel, RevisionMixin):
event = models.ForeignKey('Event', related_name='power_tests', on_delete=models.CASCADE)
venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
notes = models.TextField(blank=True, default='')
# 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>")
def __str__(self):
return f"{self.pk} - {self.event}"
class Meta:
ordering = ['event']
permissions = [
('review_power', 'Can review Power Test Records')
]
@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})"

View File

@@ -1,4 +0,0 @@
from .models import *
from .finance import *
from .hs import *
from .events import *

View File

@@ -1,467 +0,0 @@
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')
]

View File

@@ -1,170 +0,0 @@
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}"

View File

@@ -1,243 +0,0 @@
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})"

View File

@@ -1,173 +0,0 @@
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})

View File

@@ -1,9 +0,0 @@
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

View File

@@ -30,22 +30,11 @@
{% if perms.RIGS.add_event %} {% if perms.RIGS.add_event %}
<a class="dropdown-item" href="{% url 'event_create' %}"><span class="fas fa-plus"></span> <a class="dropdown-item" href="{% url 'event_create' %}"><span class="fas fa-plus"></span>
New Event</a> New Event</a>
<a class="dropdown-item" href="{% url 'subhire_create' %}"><span class="fas fa-truck"></span>
New Subhire</a>
{% endif %} {% endif %}
</div> </div>
</li> </li>
{% if perms.RIGS.view_riskassessment %} {% if perms.RIGS.view_riskassessment %}
<li class="nav-item dropdown"> <li class="nav-item"><a class="nav-link" href="{% url 'hs_list' %}">H&S</a></li>
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownHS" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
H&S
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownHS">
<a class="dropdown-item" href="{% url 'hs_list' %}"><span class="fas fa-eye"></span> Overview</a>
<a class="dropdown-item" href="{% url 'ra_list' %}"><span class="fas fa-file-medical"></span> Risk Assessments</a>
<a class="dropdown-item" href="{% url 'ec_list' %}"><span class="fas fa-tasks"></span> Event Checklists</a>
</div>
</li>
{% endif %} {% endif %}
{% if perms.RIGS.view_invoice %} {% if perms.RIGS.view_invoice %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">

View File

@@ -1,131 +1,197 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% load static %} {% load static %}
{% block js %} {% block title %}Calendar{% endblock %}
<script src="{% static 'js/moment.js' %}"></script>
<script>
$(document).ready(function() {
// set some button listeners
$('#today-button').click(function(){ calendar.today(); });
$('#go-to-date-input').change(function(){
if(moment($('#go-to-date-input').val()).isValid()){
document.getElementById('go-to-date-button').classList.remove('disabled');
document.getElementById('go-to-date-button').href = "?month=" + moment($('#go-to-date-input').val()).format("YYYY-MM");
} else{
document.getElementById('go-to-date-button').classList.add('disabled');
}
});
});
</script>
{% endblock %}
{% block css %} {% block css %}
<style> <link href="{% static 'css/main.css' %}" rel='stylesheet' />
.week { {% endblock %}
display:grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
grid-auto-flow: dense;
grid-gap: 2px 10px;
border: 1px solid black;
height: 8em;
align-content: start;
max-width: 100%;
}
.day { {% block js %}
display:contents; <script src="{% static 'js/moment.js' %}"></script>
} <script src="{% static 'js/main.js' %}"></script>
.day-label { <script>
grid-row-start: 1; viewToUrl = {
text-align: right; 'timeGridWeek':'week',
margin:0; 'timeGridDay':'day',
font-size: 1em !important; 'dayGridMonth':'month'
height: 1em; }
} viewFromUrl = {
'week':'timeGridWeek',
'day':'timeGridDay',
'month':'dayGridMonth'
}
var calendar; //Need to access it from jquery ready
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
.week-day, .day-label, .event { calendar = new FullCalendar.Calendar(calendarEl, {
padding: 4px 10px; themeSystem: 'bootstrap',
} aspectRatio: 1.5,
eventTimeFormat: {
'hour': '2-digit',
'minute': '2-digit',
'hour12': false
},
headerToolbar: false,
editable: false,
dayMaxEventRows: true, // allow "more" link when too many events
events: function(fetchInfo, successCallback, failureCallback) {
$.ajax({
url: '/api/event',
dataType: 'json',
data: {
start: moment(fetchInfo.startStr).format("YYYY-MM-DD[T]HH:mm:ss"),
end: moment(fetchInfo.endStr).format("YYYY-MM-DD[T]HH:mm:ss")
},
success: function(doc) {
var events = [];
colours = {
'Provisional': '#FFE89B',
'Confirmed': '#3AB54A' ,
'Booked': '#3AB54A' ,
'Cancelled': 'grey' ,
'non-rig': '#25AAE2'
};
$(doc).each(function() {
end = $(this).attr('latest')
allDay = false
if(end.indexOf("T") < 0){ //If latest does not contain a time
end = moment(end + " 23:59").format("YYYY-MM-DD[T]HH:mm:ss")
allDay = true
}
.event { thisEvent = {
background-color: #CCC; 'start': $(this).attr('earliest'),
font-size: 0.8em !important; 'end': end,
white-space: nowrap; 'className': 'modal-href',
overflow: hidden; 'title': $(this).attr('title'),
} 'url': $(this).attr('url'),
'allDay': allDay
}
.event-end { if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){
border-top-right-radius: 5px; thisEvent['color'] = colours[$(this).attr('status')];
border-bottom-right-radius: 5px; }else{
} thisEvent['color'] = colours['non-rig'];
}
events.push(thisEvent);
});
successCallback(events);
}
});
},
datesSet: function(info) {
var view = info.view;
// Set the title of the view
$('#calendar-header').text(view.title);
.event-start { // Enable/Disable "Today" button as required
border-top-left-radius: 5px; let $today = $('#today-button');
border-bottom-left-radius: 5px; if(moment().isBetween(view.currentStart, view.currentEnd)){
} //Today is within the current view
$today.prop('disabled', true);
}else{
$today.prop('disabled', false);
}
.week-day { // Set active view select button
font-size: 0.8em; let $month = $('#month-button');
} let $week = $('#week-button');
let $day = $('#day-button');
switch(view.type){
case 'dayGridMonth':
$month.addClass('active');
$week.removeClass('active');
$day.removeClass('active');
break;
@media (max-width: 767.98px) { case 'timeGridWeek':
.event { $month.removeClass('active');
padding: 2px; $week.addClass('active');
} $day.removeClass('active');
} break;
[data-span="1"] { grid-column-end: span 1; } case 'timeGridDay':
[data-span="2"] { grid-column-end: span 2; } $month.removeClass('active');
[data-span="3"] { grid-column-end: span 3; } $week.removeClass('active');
[data-span="4"] { grid-column-end: span 4; } $day.addClass('active');
[data-span="5"] { grid-column-end: span 5; } break;
[data-span="6"] { grid-column-end: span 6; } }
[data-span="7"] { grid-column-end: span 7; } history.replaceState(null,null,"{% url 'web_calendar' %}"+viewToUrl[view.type]+'/'+moment(view.currentStart).format('YYYY-MM-DD')+'/');
}
.day > a { });
color: inherit !important; calendar.render();
text-decoration: inherit !important; });
} $(document).ready(function() {
// set some button listeners
</style> $('#next-button').click(function(){ calendar.next(); });
$('#prev-button').click(function(){ calendar.prev(); });
$('#today-button').click(function(){ calendar.today(); });
$('#month-button').click(function(){ calendar.changeView('dayGridMonth'); });
$('#week-button').click(function(){ calendar.changeView('timeGridWeek'); });
$('#day-button').click(function(){ calendar.changeView('timeGridDay'); });
$('#go-to-date-input').change(function(){
if(moment($('#go-to-date-input').val()).isValid()){
$('#go-to-date-button').prop('disabled', false);
} else{
$('#go-to-date-button').prop('disabled', true);
}
});
$('#go-to-date-button').click(function(){
day = moment($('#go-to-date-input').val());
if(day.isValid()){
calendar.gotoDate(day.format("YYYY-MM-DD"));
} else{
alert('Invalid Date');
}
});
{% if view and date %}
// Go to the initial settings, if they're valid
view = viewFromUrl['{{view}}'];
calendar.changeView(view);
day = moment('{{date}}');
if(day.isValid()){
calendar.gotoDate(day.format("YYYY-MM-DD"));
} else{
console.log('Supplied date is invalid - using default')
}
{% endif %}
});
</script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="row justify-content-center mb-1"> <div class="row">
<a class="btn btn-info col-2" href="{% url 'web_calendar' %}?{{ prev_month }}"><span class="fas fa-chevron-left"></span> Previous Month</a> <div class="col-sm-12">
<div class="form-inline col-4"> <div class="pull-left">
<div class="input-group"> <span id="calendar-header" class="h2"></span>
<input type="date" id="go-to-date-input" placeholder="Go to date..." class="form-control"> </div>
<span class="input-group-append"> <div class="form-inline float-right btn-page my-3">
<a class="btn btn-success" id="go-to-date-button">Go!</a> <div class="input-group mx-2">
</span> <input type="date" class="form-control" id="go-to-date-input" placeholder="Go to date...">
<span class="input-group-append">
<button class="btn btn-success" id="go-to-date-button" type="button" disabled>Go!</button>
</span>
</div>
<div class="btn-group mx-2">
<button type="button" class="btn btn-primary" id="today-button">Today</button>
</div>
<div class="btn-group mx-2">
<button type="button" class="btn btn-secondary" id="prev-button"><span class="fas fa-chevron-left"></span></button>
<button type="button" class="btn btn-secondary" id="next-button"><span class="fas fa-chevron-right"></span></button>
</div>
<div class="btn-group ml-2">
<button type="button" class="btn btn-light" id="month-button">Month</button>
<button type="button" class="btn btn-light" id="week-button">Week</button>
<button type="button" class="btn btn-light" id="day-button">Day</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div id='calendar'></div>
</div> </div>
</div> </div>
<button type="button" class="btn btn-primary col-2" id="today-button">Today</button>
<a class="btn btn-info mx-2 col-2" href="{% url 'web_calendar' %}?{{ next_month }}"><span class="fas fa-chevron-right"></span> Next Month</a>
</div>
<div class="week" style="height: 2em;">
<div class="week-day">Monday</div>
<div class="week-day">Tuesday</div>
<div class="week-day">Wednesday</div>
<div class="week-day">Thursday</div>
<div class="week-day">Friday</div>
<div class="week-day">Saturday</div>
<div class="week-day">Sunday</div>
</div>
{% for week in weeks %}
<div class="week">
{% for day in week %}
{% if day.0 != 0 %}
<div class="day" id="{{day.0}}">
<h3 class="day-label text-muted">{{day.0}}</h3>
{{ day.2|safe }}
</div>
{% else %}
<div class="day"><span style="grid-row-start: 1;">&nbsp;<span></div>
{% endif %}
{% endfor %}
</div>
{% endfor %}
{% endblock %} {% endblock %}

View File

@@ -1,29 +0,0 @@
{% extends 'base_rigs.html' %}
{% load button from filters %}
{% block content %}
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">Upcoming Events</div>
<div class="card-body">{{ rig_count }}</div>
<div class="card-footer"><a href={% url 'rigboard' %}>View</a></div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">Upcoming Subhire</div>
<div class="card-body">{{ subhire_count }}</div>
<div class="card-footer"><a href={% url 'subhire_list' %}>View</a></div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">Active Dry Hires</div>
<div class="card-body">{{ hire_count }}</div>
<div class="card-footer"><a href={% url 'rigboard' %}>View</a></div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -15,7 +15,36 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include 'partials/archive_form.html' %} <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>
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
{% with object_list as events %} {% with object_list as events %}

View File

@@ -25,10 +25,12 @@
{% include 'partials/hs_details.html' %} {% include 'partials/hs_details.html' %}
</div> </div>
{% endif %} {% endif %}
{% if event.is_rig and event.internal and perms.RIGS.view_event %} {% if event.is_rig %}
<div class="col-md-8 py-3"> {% if event.is_rig and event.internal and perms.RIGS.view_event %}
{% include 'partials/auth_details.html' %} <div class="col-md-8 py-3">
</div> {% include 'partials/auth_details.html' %}
</div>
{% endif %}
{% endif %} {% endif %}
{% if not request.is_ajax and perms.RIGS.view_event %} {% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
@@ -45,15 +47,9 @@
<hr> <hr>
<p class="dont-break-out">{{ event.notes|markdown }}</p> <p class="dont-break-out">{{ event.notes|markdown }}</p>
{% endif %} {% endif %}
<h4>Event Items</h4> <br>
{% include 'partials/item_table.html' %}
</div> </div>
{% include 'partials/item_table.html' %}
{% if event.subhire_set.count > 0 %}
<div class="card-body"><h4>Associated Subhires</h4></div>
{% with event.subhire_set.all as events %}
{% include 'partials/event_table.html' %}
{%endwith%}
{% endif %}
</div> </div>
</div> </div>
{% if not request.is_ajax and perms.RIGS.view_event %} {% if not request.is_ajax and perms.RIGS.view_event %}

View File

@@ -106,10 +106,6 @@
title="Things that aren't service-based, like training, meetings and site visits."> title="Things that aren't service-based, like training, meetings and site visits.">
<button type="button" class="btn btn-info w-25" data-is_rig="0">Non-Rig</button> <button type="button" class="btn btn-info w-25" data-is_rig="0">Non-Rig</button>
</span> </span>
<span data-toggle="tooltip"
title="Record equipment hired in from other companies">
<a href="{% url 'subhire_create' %}" class="btn bg-warning w-25">Subhire</a>
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -228,123 +228,6 @@
</div> </div>
</div> </div>
</div> </div>
{% if event.riskassessment.event_size == 0 %}
<div class="row my-3" id="size-0">
<div class="col-12">
<div class="card border-success">
<div class="card-header">Electrical Checks <small>for Small TEC Events <6kVA (approx. 26A)</small></div>
<div class="card-body">
{% include 'partials/checklist_checkbox.html' with formitem=form.rcds %}
{% include 'partials/checklist_checkbox.html' with formitem=form.supply_test %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
</div>
</div>
</div>
</div>
{% else %}
<div class="row my-3" id="size-1">
<div class="col-12">
{% if event.riskassessment.event_size == 1 %}
<div class="card border-warning">
<div class="card-header">Electrical Checks <small>for Medium TEC Events </small></div>
<div class="card-body">
{% else %}
<div class="card border-danger">
<div class="card-header">Electrical Checks <small>for Large TEC Events</small></div>
<div class="card-body">
<div class="alert alert-danger"><strong>Here be dragons. Ensure you have appeased the Power Gods before continuing... (If you didn't check with a Supervisor, <em>you cannot continue your event!</em>)</strong></div>
{% endif %}
{% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %}
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
<hr>
<p>Tests at first distro</p>
<div class="table-responsive">
<table class="table table-bordered table-sm">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" colspan="3" class="text-center">Value</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small></th>
<th class="text-center">{{ form.fd_voltage_l1.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l2.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l3.help_text }}</th>
</tr>
<tr>
<td>{% render_field form.fd_voltage_l1 class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.fd_voltage_l2 class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.fd_voltage_l3 class+="form-control" style="min-width: 5rem;" %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_phase_rotation.help_text|safe}}</th>
<td colspan="3">{% include 'partials/checklist_checkbox.html' with formitem=form.fd_phase_rotation %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_earth_fault.help_text|safe}}</th>
<td colspan="3">{% render_field form.fd_earth_fault class+="form-control" %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_pssc.help_text|safe}}</th>
<td colspan="3">{% render_field form.fd_pssc class+="form-control" %}</td>
</tr>
</tbody>
</table>
</div>
<hr>
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" class="text-center">Point 1</th>
<th scope="col" class="text-center">Point 2</th>
<th scope="col" class="text-center">Point 3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">{{form.w1_description.help_text|safe}}</th>
<td>{% render_field form.w1_description class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.w2_description class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.w3_description class+="form-control" style="min-width: 5rem;" %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_polarity.help_text|safe}}</th>
<td>{% render_field form.w1_polarity %}</td>
<td>{% render_field form.w2_polarity %}</td>
<td>{% render_field form.w3_polarity %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_voltage.help_text|safe}}</th>
<td>{% render_field form.w1_voltage class+="form-control" %}</td>
<td>{% render_field form.w2_voltage class+="form-control" %}</td>
<td>{% render_field form.w3_voltage class+="form-control" %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_earth_fault.help_text|safe}}</th>
<td>{% render_field form.w1_earth_fault class+="form-control" %}</td>
<td>{% render_field form.w2_earth_fault class+="form-control" %}</td>
<td>{% render_field form.w3_earth_fault class+="form-control" %}</td>
</tr>
</tbody>
</table>
</div>
<hr/>
{% include 'partials/checklist_checkbox.html' with formitem=form.all_rcds_tested %}
{% include 'partials/checklist_checkbox.html' with formitem=form.public_sockets_tested %}
{% include 'partials/ec_power_info.html' %}
</div>
</div>
</div>
</div>
{% endif %}
<div class="row mt-3"> <div class="row mt-3">
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% button 'submit' %} {% button 'submit' %}

View File

@@ -12,6 +12,7 @@
<th scope="col">Dates</th> <th scope="col">Dates</th>
<th scope="col">RA</th> <th scope="col">RA</th>
<th scope="col">Checklists</th> <th scope="col">Checklists</th>
<th scope="col">Power Records</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -35,6 +36,14 @@
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span <a href="{% url 'event_ec' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="d-none d-sm-inline">Create</span></a> class="d-none d-sm-inline">Create</span></a>
</td> </td>
<td>
{% for record in event.power_tests.all %}
{% include 'partials/hs_status.html' with event=event object=record view='pt_detail' edit='pt_edit' create='event_pt' review='pt_review' perm=perms.RIGS.review_power %}
<br/>
{% endfor %}
<a href="{% url 'event_pt' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create</span></a>
</td>
</tr> </tr>
{% empty %} {% empty %}
<tr class="bg-warning text-dark"> <tr class="bg-warning text-dark">

View File

@@ -1,59 +0,0 @@
{% extends 'base_rigs.html' %}
{% load paginator from filters %}
{% load help_text from filters %}
{% load verbose_name from filters %}
{% load get_field from filters %}
{% block title %}{{ title }} List{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<h2>{{title}} List</h2>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="table-responsive">
<table class="table mb-0 table-sm">
<thead>
<tr>
<th scope="col">Event</th>
{# mmm hax #}
{% if object_list.0 != None %}
{% for field in object_list.0.fieldz %}
<th scope="col">{{ object_list.0|verbose_name:field|title }}</th>
{% endfor %}
{% endif %}
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr class="{% if object.reviewed_by %}table-success{%endif%}">
{# General #}
<th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a><br><small>{{ object.event.get_status_display }}</small></th>
{% for field in object_list.0.fieldz %}
<td>{{ object|get_field:field }}</td>
{% endfor %}
{# Buttons #}
<td>
{% include 'partials/hs_status.html' %}
</td>
</tr>
{% empty %}
<tr class="bg-warning">
<td colspan="6">Nothing found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% if is_paginated %}
<div class="row justify-content-center">
{% paginator %}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,167 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load help_text from filters %}
{% load profile_by_index from filters %}
{% load yesnoi from filters %}
{% load button from filters %}
{% block content %}
<div class="row">
<div class="col-12 text-right my-3">
{% button 'edit' url='pt_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %}
</div>
</div>
<div class="card mb-3">
<div class="card-header">{% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
<div class="card-body">
<dl class="row">
<dt class="col-6">Venue</dt>
<dd class="col-6">
{% if object.venue %}
<a href="{% url 'venue_detail' object.venue.pk %}" class="modal-href">
{{ object.venue }}
</a>
{% endif %}
</dd>
<dt class="col-6">Notes</dt>
<dd class="col-6">
{{ object.notes }}
</dd>
</dl>
{% if object.event.riskassessment.event_size == 0 %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
<dd class="col-2">
{{ object.rcds|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
<dd class="col-2">
{{ object.supply_test|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
<dd class="col-2">
{{ object.earthing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
<dd class="col-2">
{{ object.pat|yesnoi }}
</dd>
</dl>
{% else %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt>
<dd class="col-2">
{{ object.source_rcd|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'labelling'|safe }}</dt>
<dd class="col-2">
{{ object.labelling|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
<dd class="col-2">
{{ object.earthing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
<dd class="col-2">
{{ object.pat|yesnoi }}
</dd>
</dl>
<hr>
<p>Tests at first distro</p>
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" colspan="3" class="text-center">Value</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small></th>
<th>{{ object|help_text:'fd_voltage_l1' }}</th>
<th>{{ object|help_text:'fd_voltage_l2' }}</th>
<th>{{ object|help_text:'fd_voltage_l3' }}</th>
</tr>
<tr>
<td>{{ object.fd_voltage_l1 }}</td>
<td>{{ object.fd_voltage_l2 }}</td>
<td>{{ object.fd_voltage_l3 }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_phase_rotation'|safe }}</th>
<td colspan="3">{{ object.fd_phase_rotation|yesnoi }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_earth_fault'|safe}}</th>
<td colspan="3">{{ object.fd_earth_fault }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_pssc'}}</th>
<td colspan="3">{{ object.fd_pssc }}</td>
</tr>
</tbody>
</table>
<hr>
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" class="text-center">Point 1</th>
<th scope="col" class="text-center">Point 2</th>
<th scope="col" class="text-center">Point 3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">{{ object|help_text:'w1_description'|safe}}</th>
<td>{{ object.w1_description }}</td>
<td>{{ object.w2_description|default:'' }}</td>
<td>{{ object.w3_description|default:'' }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_polarity'|safe}}</th>
<td>{{ object.w1_polarity|yesnoi }}</td>
<td>{{ object.w2_polarity|default:''|yesnoi }}</td>
<td>{{ object.w3_polarity|default:''|yesnoi }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_voltage'|safe}}</th>
<td>{{ object.w1_voltage }}</td>
<td>{{ object.w2_voltage|default:'' }}</td>
<td>{{ object.w3_voltage|default:'' }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_earth_fault'|safe}}</th>
<td>{{ object.w1_earth_fault }}</td>
<td>{{ object.w2_earth_fault|default:'' }}</td>
<td>{{ object.w3_earth_fault|default:'' }}</td>
</tr>
</tbody>
</table>
<hr>
<dl class="row">
<dt class="col-10">{{ object|help_text:'all_rcds_tested'|safe }}</dt>
<dd class="col-2">
{{ object.all_rcds_tested|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'public_sockets_tested'|safe }}</dt>
<dd class="col-2">
{{ object.public_sockets_tested|yesnoi }}
</dd>
</dl>
<hr>
{% include 'partials/ec_power_info.html' %}
{% endif %}
</div>
</div>
<div class="col-12 text-right">
{% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div>
<div class="col-12 text-right">
{% include 'partials/last_edited.html' with target="eventchecklist_history" %}
</div>
{% endblock %}

View File

@@ -0,0 +1,191 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load help_text from filters %}
{% load profile_by_index from filters %}
{% load button from filters %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}"></script>
{% endblock %}
{% block js %}
{{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
{% endblock %}
{% block content %}
<div class="col-12">
{% include 'form_errors.html' %}
{% if edit %}
<form role="form" method="POST" action="{% url 'pt_edit' pk=object.pk %}">
{% else %}
<form role="form" method="POST" action="{% url 'event_pt' pk=event.pk %}">
{% endif %}
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
value="{{event.pk}}"/>
{% csrf_token %}
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">Event Information</div>
<div class="card-body">
<dl class="row">
<dt class="col-4">Event Date</dt>
<dd class="col-8">{{ event.start_date}}{%if event.end_date %}-{{ event.end_date}}{%endif%}</dd>
<dt class="col-4">Event Name</dt>
<dd class="col-8">{{ event.name }}</dd>
<dt class="col-4">Client</dt>
<dd class="col-8">{{ event.person }}</dd>
<dt class="col-4">Event Size</dt>
<dd class="col-8">{% include 'partials/event_size.html' with object=event.riskassessment %}</dd>
</dl>
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
<label for="{{ form.venue.id_for_label }}"
class="col-4 col-form-label">{{ form.venue.label }}</label>
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %}
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
{% elif event.venue %}
<option value="{{event.venue.pk}}" selected="selected">{{ event.venue.name }}</option>
{% endif %}
</select>
</div>
<label for="{{ form.notes.id_for_label }}">Notes</label>
{% render_field form.notes class+="form-control" %}
</div>
</div>
</div>
</div>
{% if event.riskassessment.event_size == 0 %}
<div class="row my-3" id="size-0">
<div class="col-12">
<div class="card border-success">
<div class="card-header">Electrical Checks <small>for Small TEC Events <6kVA (approx. 26A)</small></div>
<div class="card-body">
{% include 'partials/checklist_checkbox.html' with formitem=form.rcds %}
{% include 'partials/checklist_checkbox.html' with formitem=form.supply_test %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
</div>
</div>
</div>
</div>
{% else %}
<div class="row my-3" id="size-1">
<div class="col-12">
{% if event.riskassessment.event_size == 1 %}
<div class="card border-warning">
<div class="card-header">Electrical Checks <small>for Medium TEC Events </small></div>
<div class="card-body">
{% else %}
<div class="card border-danger">
<div class="card-header">Electrical Checks <small>for Large TEC Events</small></div>
<div class="card-body">
<div class="alert alert-danger"><strong>Here be dragons. Ensure you have appeased the Power Gods before continuing... (If you didn't check with a Supervisor, <em>you cannot continue your event!</em>)</strong></div>
{% endif %}
{% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %}
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
<hr>
<p>Tests at first distro</p>
<div class="table-responsive">
<table class="table table-bordered table-sm">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" colspan="3" class="text-center">Value</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small></th>
<th class="text-center">{{ form.fd_voltage_l1.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l2.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l3.help_text }}</th>
</tr>
<tr>
<td>{% render_field form.fd_voltage_l1 class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.fd_voltage_l2 class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.fd_voltage_l3 class+="form-control" style="min-width: 5rem;" %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_phase_rotation.help_text|safe}}</th>
<td colspan="3">{% include 'partials/checklist_checkbox.html' with formitem=form.fd_phase_rotation %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_earth_fault.help_text|safe}}</th>
<td colspan="3">{% render_field form.fd_earth_fault class+="form-control" %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_pssc.help_text|safe}}</th>
<td colspan="3">{% render_field form.fd_pssc class+="form-control" %}</td>
</tr>
</tbody>
</table>
</div>
<hr>
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" class="text-center">Point 1</th>
<th scope="col" class="text-center">Point 2</th>
<th scope="col" class="text-center">Point 3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">{{form.w1_description.help_text|safe}}</th>
<td>{% render_field form.w1_description class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.w2_description class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.w3_description class+="form-control" style="min-width: 5rem;" %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_polarity.help_text|safe}}</th>
<td>{% render_field form.w1_polarity %}</td>
<td>{% render_field form.w2_polarity %}</td>
<td>{% render_field form.w3_polarity %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_voltage.help_text|safe}}</th>
<td>{% render_field form.w1_voltage class+="form-control" %}</td>
<td>{% render_field form.w2_voltage class+="form-control" %}</td>
<td>{% render_field form.w3_voltage class+="form-control" %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_earth_fault.help_text|safe}}</th>
<td>{% render_field form.w1_earth_fault class+="form-control" %}</td>
<td>{% render_field form.w2_earth_fault class+="form-control" %}</td>
<td>{% render_field form.w3_earth_fault class+="form-control" %}</td>
</tr>
</tbody>
</table>
</div>
<hr/>
{% include 'partials/checklist_checkbox.html' with formitem=form.all_rcds_tested %}
{% include 'partials/checklist_checkbox.html' with formitem=form.public_sockets_tested %}
{% include 'partials/ec_power_info.html' %}
</div>
</div>
</div>
</div>
{% endif %}
<div class="row mt-3">
<div class="col-sm-12 text-right">
{% button 'submit' %}
</div>
</div>
</form>
</div>
{% endblock %}

View File

@@ -1,32 +0,0 @@
{% 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>

View File

@@ -47,6 +47,6 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p><strong>Voltage Drop on Circuit:</strong> 5% (approx. 12v)</p> <p><strong>Voltage Drop on Circuit:</strong> &#8804;5% (approx. 12v)</p>
</div> </div>
</div> </div>

View File

@@ -47,5 +47,7 @@
class="fas fa-pound-sign"></span> class="fas fa-pound-sign"></span>
<span class="d-none d-sm-inline">Invoice</span></a> <span class="d-none d-sm-inline">Invoice</span></a>
{% endif %} {% endif %}
<a href="https://docs.google.com/forms/d/e/1FAIpQLSf-TBOuJZCTYc2L8DWdAaC3_Werq0ulsUs8-6G85I6pA9WVsg/viewform" class="btn btn-danger"><span class="fas fa-file-invoice-dollar"></span> <span class="d-none d-sm-inline">Subhire Insurance Form</span></a>
{% endif %} {% endif %}
</div> </div>

View File

@@ -12,7 +12,21 @@
</thead> </thead>
<tbody> <tbody>
{% for event in events %} {% for event in events %}
<tr class="table-{{event.color}}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row"> <tr class="{% if event.cancelled %}
table-secondary
{% elif not event.is_rig %}
table-info
{% elif not event.mic %}
table-danger
{% elif event.confirmed and event.authorised %}
{% if event.dry_hire or event.riskassessment %}
table-success
{% else %}
table-warning
{% endif %}
{% else %}
table-warning
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<!---Number--> <!---Number-->
<th scope="row" id="event_number">{{ event.display_id }}</th> <th scope="row" id="event_number">{{ event.display_id }}</th>
<!--Dates & Times--> <!--Dates & Times-->
@@ -42,7 +56,7 @@
<!---Details--> <!---Details-->
<td id="event_details" class="w-100"> <td id="event_details" class="w-100">
<h4> <h4>
<a href="{{event.get_absolute_url}}"> <a href="{% url 'event_detail' event.pk %}">
{{ event.name }} {{ event.name }}
</a> </a>
{% if event.venue %} {% if event.venue %}

View File

@@ -10,10 +10,18 @@
<hr> <hr>
<h5>Event Checklists:</h5> <h5>Event Checklists:</h5>
{% for checklist in event.checklists.all %} {% for checklist in event.checklists.all %}
{% include 'partials/hs_status.html' with event=event object=checklist view='ec_detail' edit='ec_edit' create='event_ec' review='ec_review' perm=perms.RIGS.review_eventchecklist %} {% include 'partials/hs_status.html' with event=event object=checklist view='ec_detail' edit='ec_edit' create='event_ec' review='ec_review' perm=perms.RIGS.review_eventchecklist %}
<br/> <br/>
{% endfor %} {% endfor %}
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info mt-2"><span class="fas fa-paperclip"></span> <span <a href="{% url 'event_ec' event.pk %}" class="btn btn-info mt-2"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create</span></a> class="hidden-xs">Create</span></a>
<hr>
<h5>Power Test Records:</h5>
{% for record in event.power_tests.all %}
{% include 'partials/hs_status.html' with event=event object=record view='pt_detail' edit='pt_edit' create='event_pt' review='pt_review' perm=perms.RIGS.review_power %}
<br/>
{% endfor %}
<a href="{% url 'event_pt' event.pk %}" class="btn btn-info mt-2"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create</span></a>
</div> </div>
</div> </div>

View File

@@ -15,7 +15,7 @@
<button type="button" class="btn btn-success btn-sm item-add" <button type="button" class="btn btn-success btn-sm item-add"
data-toggle="modal" data-toggle="modal"
data-target="#itemModal"> data-target="#itemModal">
<span class="fas fa-plus"></span> Add Item <i class="fas fa-plus"></i> Add Item
</button> </button>
</th> </th>
{% endif %} {% endif %}

View File

@@ -1,84 +0,0 @@
{% 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>

View File

@@ -1,76 +0,0 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load markdown_tags %}
{% load button from filters %}
{% block content %}
<div class="row my-3 py-3">
<div class="col-sm-12 text-right mb-2">
{% button 'edit' 'subhire_update' object.pk %}
</div>
<div class="col-md-6">
{% include 'partials/contact_details.html' %}
</div>
<div class="col-md-6">
<div class="card card-default">
<div class="card-header">Hire Details</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6">Name</dt>
<dd class="col-sm-6">{{ object.name }}</dd>
<dt class="col-sm-6">Event Starts</dt>
<dd class="col-sm-6">{{ object.start_date|date:"D d M Y" }} {{ object.start_time|date:"H:i" }}</dd>
<dt class="col-sm-6">Event Ends</dt>
<dd class="col-sm-6">{{ object.end_date|date:"D d M Y" }} {{ object.end_time|date:"H:i" }}</dd>
<dt class="col-sm-6">Status</dt>
<dd class="col-sm-6">{{ object.get_status_display }}</dd>
<dt class="col-sm-6">PO</dt>
<dd class="col-sm-6">{{ object.po }}</dd>
</dl>
</div>
</div>
</div>
<div class="col-md-6 mt-2">
<div class="card card-default">
<div class="card-header">Equipment Information</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6">Description</dt>
<dd class="col-sm-6">{{ object.description }}</dd>
{% if perms.RIGS.subhire_finance %}
<dt class="col-sm-6">Insurance Value</dt>
<dd class="col-sm-6">£{{ object.insurance_value }}</dd>
{% endif %}
<dt class="col-sm-6">Quote</dt>
<dd class="col-sm-6"><a href="{{ object.quote }}">View</a></dd>
</dl>
</div>
</div>
</div>
<div class="col-md-12 mt-2">
<div class="card card-default">
<div class="card-header">Associated Event(s)</div>
{% with object.events.all as events %}
{% include 'partials/event_table.html' %}
{%endwith%}
</div>
</div>
{% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
{% include 'partials/last_edited.html' with target="event_history" %}
</div>
{% endif %}
</div>
{% endblock %}
{% if request.is_ajax %}
{% block footer %}
{% if perms.RIGS.view_event %}
{% include 'partials/last_edited.html' with target="event_history" %}
{% endif %}
<a href="{% url 'subhire_detail' object.pk %}" class="btn btn-primary">Open Event Page <span class="fas fa-eye"></span></a>
{% endblock %}
{% endif %}

View File

@@ -1,202 +0,0 @@
{% extends 'base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load multiply from filters %}
{% load button from filters %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/easymde.min.js' %}"></script>
{% endblock %}
{% block js %}
{{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
$(document).ready(function () {
setupMDE('#id_description');
});
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{% endblock %}
{% block content %}
<form class="row" role="form" method="POST">
{% csrf_token %}
<div class="col-12">
{% include 'form_errors.html' %}
</div>
{# Contact details #}
<div class="col-md-6 mb-2">
<div class="card">
<div class="card-header">Contact Details</div>
<div class="card-body">
<div class="form-group" data-toggle="tooltip">
<label for="{{ form.person.id_for_label }}">Primary Contact</label>
<div class="row">
<div class="col-9">
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}">
{% if person %}
<option value="{{form.person.value}}" selected="selected" data-update_url="{% url 'person_update' form.person.value %}">{{ person }}</option>
{% endif %}
</select>
</div>
<div class="col-3 align-right">
<div class="btn-group">
<a href="{% url 'person_create' %}" class="btn btn-success modal-href"
data-target="#{{ form.person.id_for_label }}">
<span class="fas fa-plus"></span>
</a>
<a {% if form.person.value %}href="{% url 'person_update' form.person.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.person.id_for_label }}-update" data-target="#{{ form.person.id_for_label }}">
<span class="fas fa-user-edit"></span>
</a>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="{{ form.organisation.id_for_label }}">Hire Company</label>
<div class="row">
<div class="col-9">
<select id="{{ form.organisation.id_for_label }}" name="{{ form.organisation.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}">
{% if organisation %}
<option value="{{form.organisation.value}}" selected="selected" data-update_url="{% url 'organisation_update' form.organisation.value %}">{{ organisation }}</option>
{% endif %}
</select>
</div>
<div class="col-3 align-right">
<div class="btn-group">
<a href="{% url 'organisation_create' %}" class="btn btn-success modal-href"
data-target="#{{ form.organisation.id_for_label }}">
<span class="fas fa-plus"></span>
</a>
<a {% if form.organisation.value %}href="{% url 'organisation_update' form.organisation.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.organisation.id_for_label }}-update" data-target="#{{ form.organisation.id_for_label }}">
<span class="fas fa-edit"></span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-2">
<div class="card">
<div class="card-header">Associated Event(s)</div>
<div class="card-body">
<div class="form-group">
<select multiple name="events" id="events_id" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='event' %}">
{% if object.events.count > 0 %}
{% for event in object.events.all %}
<option value="{{event.id}}" selected>{{ event }}</option>
{% endfor %}
{% endif %}
</select>
</div>
</div>
</div>
</div>
{# Event details #}
<div class="col-md-6 mb-2">
<div class="card card-default">
<div class="card-header">Hire Details</div>
<div class="card-body">
<div class="form-group" data-toggle="tooltip" title="Name of the event, displays on rigboard and on paperwork">
<label for="{{ form.name.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.name.label }}</label>
<div class="col-sm-8">
{% render_field form.name class+="form-control" %}
</div>
</div>
<div class="form-group">
<label for="{{ form.start_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
{% render_field form.start_date class+="form-control" %}
</div>
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="Start time of event, can be left blank">
{% render_field form.start_time class+="form-control" step="60" %}
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="{{ form.end_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
{% render_field form.end_date class+="form-control" %}
</div>
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="End time of event, leave blank if unknown">
{% render_field form.end_time class+="form-control" step="60" %}
</div>
</div>
</div>
</div>
<div class="form-group" data-toggle="tooltip" title="The current status of the event. Only mark as booked once paperwork is received">
<label for="{{ form.status.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.status.label }}</label>
<div class="col-sm-8">
{% render_field form.status class+="form-control" %}
</div>
</div>
<div class="form-group">
<label for="{{ form.purchase_order.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label>
<div class="col-sm-8">
{% render_field form.purchase_order class+="form-control" %}
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-2">
<div class="card">
<div class="card-header">Equipment Information</div>
<div class="card-body">
<div class="form-group">
<label for="{{ form.description.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.description.label }}</label>
<div class="col-sm-12">
{% render_field form.description class+="form-control" %}
</div>
</div>
<div class="form-group">
<label for="{{ form.insurance_value.id_for_label }}"
class="col-sm-6 col-form-label">{{ form.insurance_value.label }}</label>
<div class="col-sm-8 input-group">
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
{% render_field form.insurance_value class+="form-control" %}
</div>
<div class="border border-info p-2 rounded mt-1 font-weight-bold" style="border-width: thin thin thin thick !important;">
If this value is greater than £50,000 then please email productions@nottinghamtec.co.uk in addition to complete the additional insurance requirements
</div>
</div>
<div class="form-group">
<label for="{{ form.quote.id_for_label }}" class="col-sm-6 col-form-label">{{ form.quote.label }} (TEC SharePoint link)</label>
<div class="col-sm-12">{% render_field form.quote class+="form-control" %}</div>
</div>
</div>
</div>
</div>
<div class="col-sm-12 text-right my-3">
{% button 'submit' %}
</div>
</form>
{% endblock %}

View File

@@ -1,27 +0,0 @@
{% 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 %}

View File

@@ -175,9 +175,6 @@ def namewithnotes(obj, url, autoescape=True):
else: else:
return obj.name 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) @register.filter(needs_autoescape=True)
def linkornone(target, namespace=None, autoescape=True): def linkornone(target, namespace=None, autoescape=True):

View File

@@ -43,8 +43,12 @@ urlpatterns = [
# Rigboard # Rigboard
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'), path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
re_path(r'^rigboard/calendar/$', login_required()(views.WebCalendar.as_view()), path('rigboard/calendar/', login_required()(views.WebCalendar.as_view()),
name='web_calendar'), name='web_calendar'),
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/$',
login_required()(views.WebCalendar.as_view()), name='web_calendar'),
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$',
login_required()(views.WebCalendar.as_view()), name='web_calendar'),
path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')), path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
@@ -66,22 +70,6 @@ urlpatterns = [
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()), path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()),
name='event_duplicate'), name='event_duplicate'),
# Subhire
path('subhire/<int:pk>/', login_required(views.SubhireDetail.as_view()),
name='subhire_detail'),
path('subhire/create/', permission_required_with_403('RIGS.add_event')(views.SubhireCreate.as_view()),
name='subhire_create'),
path('subhire/<int:pk>/edit', permission_required_with_403('RIGS.change_event')(views.SubhireEdit.as_view()),
name='subhire_update'),
path('subhire/list/', login_required(views.SubhireList.as_view()),
name='subhire_list'),
# Dashboards
path('dashboard/productions/', views.ProductionsDashboard.as_view(),
name='productions_dashboard'),
# Event H&S # Event H&S
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'), path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'),
@@ -91,10 +79,8 @@ urlpatterns = [
name='ra_detail'), name='ra_detail'),
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()), path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
name='ra_edit'), name='ra_edit'),
path('event/ra/list', permission_required_with_403('RIGS.view_riskassessment')(views.EventRiskAssessmentList.as_view()), path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(views.MarkReviewed.as_view()),
name='ra_list'), name='ra_review', kwargs={'model': 'RiskAssessment'}),
path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(views.EventRiskAssessmentReview.as_view()),
name='ra_review'),
path('event/ra/<int:pk>/print/', permission_required_with_403('RIGS.view_riskassessment')(views.RAPrint.as_view()), name='ra_print'), path('event/ra/<int:pk>/print/', permission_required_with_403('RIGS.view_riskassessment')(views.RAPrint.as_view()), name='ra_print'),
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()), path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
@@ -103,10 +89,17 @@ urlpatterns = [
name='ec_detail'), name='ec_detail'),
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()), path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
name='ec_edit'), name='ec_edit'),
path('event/checklist/list', permission_required_with_403('RIGS.view_eventchecklist')(views.EventChecklistList.as_view()), path('event/checklist/<int:pk>/review/', permission_required_with_403('RIGS.review_eventchecklist')(views.MarkReviewed.as_view()),
name='ec_list'), name='ec_review', kwargs={'model': 'EventChecklist'}),
path('event/checklist/<int:pk>/review/', permission_required_with_403('RIGS.review_eventchecklist')(views.EventChecklistReview.as_view()),
name='ec_review'), path('event/<int:pk>/power/', permission_required_with_403('RIGS.add_powertestrecord')(views.PowerTestCreate.as_view()),
name='event_pt'),
path('event/power/<int:pk>/', login_required(views.PowerTestDetail.as_view()),
name='pt_detail'),
path('event/power/<int:pk>/edit/', permission_required_with_403('RIGS.change_powertestrecord')(views.PowerTestEdit.as_view()),
name='pt_edit'),
path('event/power/<int:pk>/review/', permission_required_with_403('RIGS.review_power')(views.MarkReviewed.as_view()),
name='pt_review', kwargs={'model': 'PowerTestRecord'}),
# Finance # Finance
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()), path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),

View File

@@ -1,56 +0,0 @@
from datetime import datetime, timedelta, date
import calendar
from calendar import HTMLCalendar
from RIGS.models import Event, Subhire
class Calendar(HTMLCalendar):
def __init__(self, year=None, month=None):
self.year = year
self.month = month
super(Calendar, self).__init__()
def get_html(self, day, event):
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).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 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)
for sh in subhires_per_day:
event_html += self.get_html(day, sh)
weeks_events.append((day[0], day[1], event_html))
data.append(weeks_events)
return data
def get_date(req_day):
if req_day:
year, month = (int(x) for x in req_day.split('-'))
return date(year, month, day=1)
return datetime.today()
def prev_month(d):
first = d.replace(day=1)
prev_month = first - timedelta(days=1)
month = f'month={str(prev_month.year)}-{str(prev_month.month)}'
return month
def next_month(d):
days_in_month = calendar.monthrange(d.year, d.month)[1]
last = d.replace(day=days_in_month)
next_month = last + timedelta(days=1)
month = f'month={str(next_month.year)}-{str(next_month.month)}'
return month

View File

@@ -1,10 +0,0 @@
from urllib.parse import urlparse
from django.core.exceptions import ValidationError
def validate_url(value):
if not value:
return # Required error is done the field
obj = urlparse(value)
if obj.hostname not in ('nottinghamtec.sharepoint.com'):
raise ValidationError('URL must point to a location on the TEC Sharepoint')

View File

@@ -3,5 +3,3 @@ from .finance import *
from .hs import * from .hs import *
from .ical import * from .ical import *
from .rigboard import * from .rigboard import *
from .subhire import *
from .dashboards import *

View File

@@ -1,14 +0,0 @@
from django.views import generic
from RIGS import models
class ProductionsDashboard(generic.TemplateView):
template_name = 'dashboards/productions.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = "Productions Dashboard"
context['rig_count'] = models.Event.objects.rig_count()
context['subhire_count'] = models.Subhire.objects.event_count()
context['hire_count'] = models.Event.objects.active_dry_hires().count()
return context

View File

@@ -1,3 +1,4 @@
from django.apps import apps
from django.contrib import messages from django.contrib import messages
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
@@ -10,7 +11,35 @@ from RIGS.views.rigboard import get_related
from PyRIGS.views import PrintView from PyRIGS.views import PrintView
class EventRiskAssessmentCreate(generic.CreateView): class HSCreateView(generic.CreateView):
def get_form(self, **kwargs):
form = super().get_form(**kwargs)
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
form.instance.event = event
return form
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
context['event'] = event
context['page_title'] = f'Create {self} for Event {event.display_id}'
return context
class MarkReviewed(generic.View):
def get(self, *args, **kwargs):
obj = apps.get_model('RIGS', kwargs.get('model')).objects.get(pk=kwargs.get('pk'))
with reversion.create_revision():
reversion.set_user(self.request.user)
obj.reviewed_by = self.request.user
obj.reviewed_at = timezone.now()
obj.save()
return HttpResponseRedirect(reverse_lazy('hs_list'))
class EventRiskAssessmentCreate(HSCreateView):
model = models.RiskAssessment model = models.RiskAssessment
template_name = 'hs/risk_assessment_form.html' template_name = 'hs/risk_assessment_form.html'
form_class = forms.EventRiskAssessmentForm form_class = forms.EventRiskAssessmentForm
@@ -27,22 +56,6 @@ class EventRiskAssessmentCreate(generic.CreateView):
return super(EventRiskAssessmentCreate, self).get(self) return super(EventRiskAssessmentCreate, self).get(self)
def get_form(self, **kwargs):
form = super(EventRiskAssessmentCreate, self).get_form(**kwargs)
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
form.instance.event = event
return form
def get_context_data(self, **kwargs):
context = super(EventRiskAssessmentCreate, self).get_context_data(**kwargs)
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
context['event'] = event
context['page_title'] = f'Create Risk Assessment for Event {event.display_id}'
get_related(context['form'], context)
return context
def get_success_url(self): def get_success_url(self):
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk}) return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
@@ -80,36 +93,6 @@ class EventRiskAssessmentDetail(generic.DetailView):
return context return context
class EventRiskAssessmentList(generic.ListView):
paginate_by = 20
model = models.RiskAssessment
template_name = 'hs/hs_object_list.html'
def get_queryset(self):
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
def get_context_data(self, **kwargs):
context = super(EventRiskAssessmentList, self).get_context_data(**kwargs)
context['title'] = 'Risk Assessment'
context['view'] = 'ra_detail'
context['edit'] = 'ra_edit'
context['review'] = 'ra_review'
context['perm'] = 'perms.RIGS.review_riskassessment'
return context
class EventRiskAssessmentReview(generic.View):
def get(self, *args, **kwargs):
rpk = kwargs.get('pk')
ra = models.RiskAssessment.objects.get(pk=rpk)
with reversion.create_revision():
reversion.set_user(self.request.user)
ra.reviewed_by = self.request.user
ra.reviewed_at = timezone.now()
ra.save()
return HttpResponseRedirect(reverse_lazy('ra_list'))
class EventChecklistDetail(generic.DetailView): class EventChecklistDetail(generic.DetailView):
model = models.EventChecklist model = models.EventChecklist
template_name = 'hs/event_checklist_detail.html' template_name = 'hs/event_checklist_detail.html'
@@ -143,7 +126,7 @@ class EventChecklistEdit(generic.UpdateView):
return context return context
class EventChecklistCreate(generic.CreateView): class EventChecklistCreate(HSCreateView):
model = models.EventChecklist model = models.EventChecklist
template_name = 'hs/event_checklist_form.html' template_name = 'hs/event_checklist_form.html'
form_class = forms.EventChecklistForm form_class = forms.EventChecklistForm
@@ -162,53 +145,63 @@ class EventChecklistCreate(generic.CreateView):
return super(EventChecklistCreate, self).get(self) return super(EventChecklistCreate, self).get(self)
def get_form(self, **kwargs):
form = super(EventChecklistCreate, self).get_form(**kwargs)
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
form.instance.event = event
return form
def get_context_data(self, **kwargs):
context = super(EventChecklistCreate, self).get_context_data(**kwargs)
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
context['event'] = event
context['page_title'] = f'Create Event Checklist for Event {event.display_id}'
return context
def get_success_url(self): def get_success_url(self):
return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk}) return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
class EventChecklistList(generic.ListView): class PowerTestDetail(generic.DetailView):
paginate_by = 20 model = models.PowerTestRecord
model = models.EventChecklist template_name = 'hs/power_detail.html'
template_name = 'hs/hs_object_list.html'
def get_queryset(self):
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventChecklistList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['title'] = 'Event Checklist' context['page_title'] = f"Power Test Record for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
context['view'] = 'ec_detail'
context['edit'] = 'ec_edit'
context['review'] = 'ec_review'
context['perm'] = 'perms.RIGS.review_eventchecklist'
return context return context
class EventChecklistReview(generic.View): class PowerTestEdit(generic.UpdateView):
model = models.PowerTestRecord
template_name = 'hs/power_form.html'
form_class = forms.PowerTestRecordForm
def get_success_url(self):
ec = self.get_object()
ec.reviewed_by = None
ec.reviewed_at = None
ec.save()
return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pk = self.kwargs.get('pk')
ec = models.PowerTestRecord.objects.get(pk=pk)
context['event'] = ec.event
context['edit'] = True
context['page_title'] = f'Edit Power Test Record for Event {ec.event.display_id}'
# get_related(context['form'], context)
return context
class PowerTestCreate(HSCreateView):
model = models.PowerTestRecord
template_name = 'hs/power_form.html'
form_class = forms.PowerTestRecordForm
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
rpk = kwargs.get('pk') epk = kwargs.get('pk')
ec = models.EventChecklist.objects.get(pk=rpk) event = models.Event.objects.get(pk=epk)
with reversion.create_revision():
reversion.set_user(self.request.user) # Check if RA exists
ec.reviewed_by = self.request.user ra = models.RiskAssessment.objects.filter(event=event).first()
ec.reviewed_at = timezone.now()
ec.save() if ra is None:
return HttpResponseRedirect(reverse_lazy('ec_list')) messages.error(self.request, f'A Risk Assessment must exist prior to creating any Power Test Records for {event}! Please create one now.')
return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
return super().get(self)
def get_success_url(self):
return reverse_lazy('pt_detail', kwargs={'pk': self.object.pk})
class HSList(generic.ListView): class HSList(generic.ListView):

View File

@@ -1,14 +1,12 @@
import datetime import datetime
from django.utils import timezone import pytz
from django.conf import settings from django.conf import settings
from django.db.models import Q from django.db.models import Q
from django_ical.views import ICalFeed from django_ical.views import ICalFeed
from RIGS import models from RIGS import models
from itertools import chain
class CalendarICS(ICalFeed): class CalendarICS(ICalFeed):
""" """
@@ -33,7 +31,6 @@ class CalendarICS(ICalFeed):
params['dry-hire'] = request.GET.get('dry-hire', 'true') == 'true' params['dry-hire'] = request.GET.get('dry-hire', 'true') == 'true'
params['non-rig'] = request.GET.get('non-rig', 'true') == 'true' params['non-rig'] = request.GET.get('non-rig', 'true') == 'true'
params['rig'] = request.GET.get('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['cancelled'] = request.GET.get('cancelled', 'false') == 'true'
params['provisional'] = request.GET.get('provisional', 'true') == 'true' params['provisional'] = request.GET.get('provisional', 'true') == 'true'
@@ -43,46 +40,42 @@ class CalendarICS(ICalFeed):
def description(self, params): def description(self, params):
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + ( 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 '') + ('Subhires' if params['subhire'] else '') + '\n' 'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
desc += "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + ( desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '') 'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
return desc return desc
def items(self, params): def items(self, params):
# include events from up to 1 year ago # include events from up to 1 year ago
start = timezone.now() - datetime.timedelta(days=365) start = datetime.datetime.now() - datetime.timedelta(days=365)
filter = Q(start_date__gte=start) filter = Q(start_date__gte=start)
type_filters = Q(pk=None) # Need something that is false for every entry typeFilters = Q(pk=None) # Need something that is false for every entry
if params['dry-hire']: if params['dry-hire']:
type_filters = type_filters | Q(dry_hire=True, is_rig=True) typeFilters = typeFilters | Q(dry_hire=True, is_rig=True)
if params['non-rig']: if params['non-rig']:
type_filters = type_filters | Q(is_rig=False) typeFilters = typeFilters | Q(is_rig=False)
if params['rig']: if params['rig']:
type_filters = type_filters | Q(is_rig=True, dry_hire=False) typeFilters = typeFilters | Q(is_rig=True, dry_hire=False)
status_filters = Q(pk=None) # Need something that is false for every entry statusFilters = Q(pk=None) # Need something that is false for every entry
if params['cancelled']: if params['cancelled']:
status_filters = status_filters | Q(status=models.Event.CANCELLED) statusFilters = statusFilters | Q(status=models.Event.CANCELLED)
if params['provisional']: if params['provisional']:
status_filters = status_filters | Q(status=models.Event.PROVISIONAL) statusFilters = statusFilters | Q(status=models.Event.PROVISIONAL)
if params['confirmed']: if params['confirmed']:
status_filters = status_filters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED) statusFilters = statusFilters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
filter = filter & type_filters & status_filters filter = filter & typeFilters & statusFilters
events = models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation', return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
'venue', 'mic') '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): def item_title(self, item):
title = '' title = ''
@@ -113,32 +106,30 @@ class CalendarICS(ICalFeed):
return item.latest_time return item.latest_time
def item_location(self, item): def item_location(self, item):
if hasattr(item, 'venue'): return item.venue
return item.venue
return ""
def item_description(self, item): def item_description(self, item):
# Create a nice information-rich description # Create a nice information-rich description
# note: only making use of information available to "non-keyholders" # 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'Rig ID = {item.display_id}\n'
desc += f'Event = {item.name}\n' desc += f'Event = {item.name}\n'
if hasattr(item, 'venue'): desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
if item.is_rig and item.person: if item.is_rig and item.person:
desc += 'Client = ' + item.person.name + ( desc += 'Client = ' + item.person.name + (
(' for ' + item.organisation.name) if item.organisation else '') + '\n' (' for ' + item.organisation.name) if item.organisation else '') + '\n'
desc += f'Status = {item.get_status_display()}\n' desc += f'Status = {item.get_status_display()}\n'
if hasattr(item, 'mic'): desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
desc += '\n' desc += '\n'
if hasattr(item, 'meet_at') and item.meet_at: if item.meet_at:
desc += 'Crew Meet = ' + ( desc += 'Crew Meet = ' + (
timezone.make_aware(item.meet_at).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n' item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
if hasattr(item, 'access_at') and item.access_at: if item.access_at:
desc += 'Access At = ' + ( desc += 'Access At = ' + (
timezone.make_aware(item.access_at).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n' item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
if item.start_date: if item.start_date:
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + ( desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + (
(' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n' (' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
@@ -149,6 +140,8 @@ class CalendarICS(ICalFeed):
desc += '\n' desc += '\n'
if item.description: if item.description:
desc += f'Event Description:\n{item.description}\n\n' desc += f'Event Description:\n{item.description}\n\n'
# if item.notes: // Need to add proper keyholder checks before this gets put back
# desc += 'Notes:\n'+item.notes+'\n\n'
desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}' desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}'

View File

@@ -2,6 +2,7 @@ import copy
import datetime import datetime
import re import re
import premailer import premailer
import simplejson
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
@@ -11,7 +12,9 @@ from django.core.exceptions import SuspiciousOperation
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@@ -19,7 +22,7 @@ from django.views import generic
from PyRIGS import decorators from PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
from RIGS import models, forms, utils from RIGS import models, forms
__author__ = 'ghost' __author__ = 'ghost'
@@ -37,25 +40,14 @@ class RigboardIndex(generic.TemplateView):
return context return context
class WebCalendar(generic.ListView): class WebCalendar(generic.TemplateView):
model = models.Event
template_name = 'calendar.html' template_name = 'calendar.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# use today's date for the calendar context['view'] = kwargs.get('view', '')
d = utils.get_date(self.request.GET.get('month', None)) context['date'] = kwargs.get('date', '')
context['prev_month'] = utils.prev_month(d) # context['page_title'] = "Calendar"
context['next_month'] = utils.next_month(d)
# Instantiate our calendar class with today's year and date
cal = utils.Calendar(d.year, d.month)
# Call the formatmonth method, which returns our calendar as a table
html_cal = cal.formatmonth(withyear=True)
# context['calendar'] = mark_safe(html_cal)
context['weeks'] = html_cal
context['page_title'] = d.strftime("%B %Y")
return context return context
@@ -69,6 +61,10 @@ class EventDetail(generic.DetailView, ModalURLMixin):
if self.object.dry_hire: if self.object.dry_hire:
title += " <span class='badge badge-secondary'>Dry Hire</span>" title += " <span class='badge badge-secondary'>Dry Hire</span>"
context['page_title'] = title context['page_title'] = title
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'
return context return context
@@ -204,7 +200,27 @@ class EventArchive(generic.ListView):
"Muppet! Check the dates, it has been fixed for you.") "Muppet! Check the dates, it has been fixed for you.")
start, end = end, start # Stop the impending fail start, end = end, start # Stop the impending fail
qs = self.model.objects.event_search(self.request.GET.get('q', None), start, end, self.request.GET.get('status', "")) 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')
if not qs.exists(): if not qs.exists():
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.") messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")

View File

@@ -1,62 +0,0 @@
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):
template_name = 'subhire_detail.html'
model = models.Subhire
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"{self.object.display_id} | {self.object.name}"
return context
class SubhireCreate(generic.CreateView):
model = models.Subhire
form_class = forms.SubhireForm
template_name = 'subhire_form.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = "New Subhire"
context['edit'] = True
form = context['form']
get_related(form, context)
return context
def get_success_url(self):
return reverse_lazy('subhire_detail', kwargs={'pk': self.object.pk})
class SubhireEdit(generic.UpdateView):
model = models.Subhire
form_class = forms.SubhireForm
template_name = 'subhire_form.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Edit Subhire: {self.object.display_id} | {self.object.name}"
context['edit'] = True
form = context['form']
get_related(form, context)
return context
def get_success_url(self):
return reverse_lazy('subhire_detail', kwargs={'pk': self.object.pk})
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['total_value'] = self.get_queryset().aggregate(sum=Sum('insurance_value'))['sum']
context['page_title'] = "Subhire List"
return context

View File

@@ -24,6 +24,7 @@ function fonts(done) {
function styles(done) { function styles(done) {
const bs_select = ["bootstrap-select.css", "ajax-bootstrap-select.css"] const bs_select = ["bootstrap-select.css", "ajax-bootstrap-select.css"]
return gulp.src(['pipeline/source_assets/scss/**/*.scss', return gulp.src(['pipeline/source_assets/scss/**/*.scss',
'node_modules/fullcalendar/main.css',
'node_modules/bootstrap-select/dist/css/bootstrap-select.css', 'node_modules/bootstrap-select/dist/css/bootstrap-select.css',
'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css', 'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css',
'node_modules/easymde/dist/easymde.min.css' 'node_modules/easymde/dist/easymde.min.css'
@@ -58,6 +59,7 @@ function scripts() {
'node_modules/html5sortable/dist/html5sortable.min.js', 'node_modules/html5sortable/dist/html5sortable.min.js',
'node_modules/clipboard/dist/clipboard.min.js', 'node_modules/clipboard/dist/clipboard.min.js',
'node_modules/moment/moment.js', 'node_modules/moment/moment.js',
'node_modules/fullcalendar/main.js',
'node_modules/bootstrap-select/dist/js/bootstrap-select.js', 'node_modules/bootstrap-select/dist/js/bootstrap-select.js',
'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js', 'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js',
'node_modules/easymde/dist/easymde.min.js', 'node_modules/easymde/dist/easymde.min.js',

11
package-lock.json generated
View File

@@ -19,6 +19,7 @@
"clipboard": "^2.0.8", "clipboard": "^2.0.8",
"cssnano": "^5.0.13", "cssnano": "^5.0.13",
"easymde": "^2.16.1", "easymde": "^2.16.1",
"fullcalendar": "^5.10.1",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-flatten": "^0.4.0", "gulp-flatten": "^0.4.0",
@@ -3062,6 +3063,11 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/fullcalendar": {
"version": "5.11.3",
"resolved": "https://registry.npmjs.org/fullcalendar/-/fullcalendar-5.11.3.tgz",
"integrity": "sha512-SgqiMEA+lWLyEd2jEwtIxdfx41j2CZr4KK00D2Gepj1MnGOjaEi13athnU6xvqMQXXjgJNj+vmlUP69QiuGncQ=="
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -11603,6 +11609,11 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"fullcalendar": {
"version": "5.11.3",
"resolved": "https://registry.npmjs.org/fullcalendar/-/fullcalendar-5.11.3.tgz",
"integrity": "sha512-SgqiMEA+lWLyEd2jEwtIxdfx41j2CZr4KK00D2Gepj1MnGOjaEi13athnU6xvqMQXXjgJNj+vmlUP69QiuGncQ=="
},
"function-bind": { "function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",

View File

@@ -15,6 +15,7 @@
"clipboard": "^2.0.8", "clipboard": "^2.0.8",
"cssnano": "^5.0.13", "cssnano": "^5.0.13",
"easymde": "^2.16.1", "easymde": "^2.16.1",
"fullcalendar": "^5.10.1",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-flatten": "^0.4.0", "gulp-flatten": "^0.4.0",

View File

@@ -73,6 +73,7 @@ function initPicker(obj) {
return array; return array;
} }
}; };
console.log(obj.data);
if (!obj.data('noclear')) { if (!obj.data('noclear')) {
obj.prepend($("<option></option>") obj.prepend($("<option></option>")
.attr("value",'') .attr("value",'')

View File

@@ -4,16 +4,16 @@ Date.prototype.getISOString = function () {
var dd = this.getDate().toString(); var dd = this.getDate().toString();
return yyyy + '-' + (mm[1] ? mm : "0" + mm[0]) + '-' + (dd[1] ? dd : "0" + dd[0]); // padding return yyyy + '-' + (mm[1] ? mm : "0" + mm[0]) + '-' + (dd[1] ? dd : "0" + dd[0]); // padding
}; };
$(document).ready(function () { jQuery(document).ready(function () {
$(document).on('click', '.modal-href', function (e) { jQuery(document).on('click', '.modal-href', function (e) {
$link = $(this); $link = jQuery(this);
// Anti modal inception // Anti modal inception
if ($link.parents('#modal').length == 0) { if ($link.parents('#modal').length == 0) {
e.preventDefault(); e.preventDefault();
modaltarget = $link.data('target'); modaltarget = $link.data('target');
modalobject = ""; modalobject = "";
$('#modal').load($link.attr('href'), function (e) { jQuery('#modal').load($link.attr('href'), function (e) {
$('#modal').modal(); jQuery('#modal').modal();
}); });
} }
}); });
@@ -23,6 +23,7 @@ $(document).ready(function () {
s.type = 'text/javascript'; s.type = 'text/javascript';
document.body.appendChild(s); document.body.appendChild(s);
s.src = '{% static "js/asteroids.min.js"%}'; s.src = '{% static "js/asteroids.min.js"%}';
ga('send', 'event', 'easter_egg', 'activated');
} }
easter_egg.load(); easter_egg.load();
}); });

View File

@@ -281,7 +281,3 @@ html.embedded {
.bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light { .bootstrap-select, button.btn.dropdown-toggle.bs-placeholder.btn-light {
padding-right: 1rem !important; padding-right: 1rem !important;
} }
.badge-purple, .bg-purple {
background-color: #800080 !important;
}

View File

@@ -22,7 +22,7 @@
<br><small>{{ item.description }}</small> <br><small>{{ item.description }}</small>
{% if item.prerequisites.exists %} {% if item.prerequisites.exists %}
<div class="ml-3 font-italic"> <div class="ml-3 font-italic">
<p class="text-info mb-0">Passed Out Prerequisites:</p> <p class="text-info mb-0">Competency Assessment Prerequisites:</p>
<ul> <ul>
{% for p in item.prerequisites.all %} {% for p in item.prerequisites.all %}
<li>{{p}}</li> <li>{{p}}</li>

View File

@@ -11,7 +11,7 @@
<spacer length="4" /> <spacer length="4" />
<para>{{ item.description }}</para> <para>{{ item.description }}</para>
{% if item.prerequisites.exists %} {% if item.prerequisites.exists %}
<h4>Prerequisites:</h4> <h4>Competency Assessment Prerequisites:</h4>
<ul bulletFontSize="5"> <ul bulletFontSize="5">
{% for p in item.prerequisites.all %} {% for p in item.prerequisites.all %}
<li><para>{{p}}</para></li> <li><para>{{p}}</para></li>

View File

@@ -28,6 +28,9 @@ class ItemListExport(PrintListView):
model = models.TrainingItem model = models.TrainingItem
template_name = 'item_list.xml' template_name = 'item_list.xml'
def get_queryset(self):
return self.model.objects.filter(active=True)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['filename'] = "TrainingItemList.pdf" context['filename'] = "TrainingItemList.pdf"

View File

@@ -118,11 +118,6 @@
<input type="checkbox" value="dry-hire" data-default="true" checked> Dry-Hires <input type="checkbox" value="dry-hire" data-default="true" checked> Dry-Hires
</label> </label>
<label class="checkbox-inline mx-lg-2"> <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 <input type="checkbox" value="cancelled" data-default="false" > Cancelled
</label> </label>
<label class="checkbox-inline mx-lg-2"> <label class="checkbox-inline mx-lg-2">