mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-15 11:09:42 +00:00
Compare commits
28 Commits
d55ec47b18
...
subhire
| Author | SHA1 | Date | |
|---|---|---|---|
| a4f240e581 | |||
| 2e4b84c94e | |||
| 8863d86ed0 | |||
| 87f2de46a1 | |||
| 1615e27767 | |||
| 773f55ac84 | |||
| 63a2f6d47b | |||
| 8393e85b74 | |||
| 311c02d554 | |||
| e100f5a1d4 | |||
| eb07990f4c | |||
| 7b7c1b86de | |||
| 2b8945c513 | |||
| eb3638b93a | |||
|
1660f51e55
|
|||
|
9feea56211
|
|||
|
951227e68b
|
|||
|
6e8779c81b
|
|||
|
e0da6a3120
|
|||
|
0c80ef1b72
|
|||
|
0f127d8ca4
|
|||
|
04ec728972
|
|||
|
bede8b4176
|
|||
|
8cade512d1
|
|||
|
418219940b
|
|||
| 948a41f43a | |||
|
4449efcced
|
|||
|
8b0cd13159
|
17
Pipfile
17
Pipfile
@@ -21,8 +21,7 @@ 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.7.1"
|
django-ical = "~=1.8.3"
|
||||||
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"
|
||||||
@@ -76,7 +75,6 @@ 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"
|
||||||
@@ -91,14 +89,11 @@ 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.9"
|
python_version = "3.10"
|
||||||
|
|
||||||
[dev-packages.pytest-xdist]
|
[pipenv]
|
||||||
extras = [ "psutil",]
|
allow_prereleases = true
|
||||||
version = "*"
|
|
||||||
|
|
||||||
[dev-packages.PyPOM]
|
|
||||||
extras = [ "splinter",]
|
|
||||||
version = "*"
|
|
||||||
|
|||||||
602
Pipfile.lock
generated
602
Pipfile.lock
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "2e2fb4b609c10fc42db6bbd69ca73800629fbcaceec664e1fcc79d4b37bc0eb1"
|
"sha256": "96e25ba04c8709eba5d40776ff20cb82e3abecfd9916b8bd9dd4c594d46e2098"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
"python_version": "3.9"
|
"python_version": "3.10"
|
||||||
},
|
},
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
@@ -272,11 +272,11 @@
|
|||||||
},
|
},
|
||||||
"django-ical": {
|
"django-ical": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:6df4dc61eb4abc55816bd16a949e497bea99828c7de648438ace7f1f85eeb405",
|
"sha256:0d5595c5bc954e401b59b27a9a86962557f0d3b965e9f5860244cd6bc450e8ab",
|
||||||
"sha256:bd5c874d2eb81329f220174cc0dde7be385f4574ce6c8a2d1579d7fd564a94f3"
|
"sha256:d3f97d163c03ea795e0722d5031e7f3806037ac913c814b0cfee54464f06978e"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.7.3"
|
"version": "==1.8.3"
|
||||||
},
|
},
|
||||||
"django-mass-edit": {
|
"django-mass-edit": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -296,19 +296,19 @@
|
|||||||
},
|
},
|
||||||
"django-recurrence": {
|
"django-recurrence": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:715f681f6af029ff3a8d73c7b1460abd8cbc5d5a5001efcb127032e84d9cb963",
|
"sha256:0c65f30872599b5813a9bab6952dada23c55894f28674490a753ada559f14bc5",
|
||||||
"sha256:9053b44b78b7fbfe3530673edfdd6d2f562105f8a192bc6a4b906a3df4f95f59"
|
"sha256:9c89444e651a78c587f352c5f63eda48ab2f53996347b9fcdff2d248f4fcff70"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==1.10.3"
|
"version": "==1.11.1"
|
||||||
},
|
},
|
||||||
"django-registration-redux": {
|
"django-registration-redux": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5079dd36980cc0faddf91a6e991129680410611b1059d8154d064cc0146744b2",
|
"sha256:2213bbe8732be72724034f4146f0255a7bd666eb5a5e1b2d8d8aa633fe8af894",
|
||||||
"sha256:88eb98530d98a7e3451bf728c0a5f6fe7ea2f45c65ef18f619ef37b940c854f5"
|
"sha256:56fbc7b01a7f0f48812fe4d4e0729d2dac916e16f8aaed36b3f10129f2df9d0f"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.11"
|
"version": "==2.12"
|
||||||
},
|
},
|
||||||
"django-reversion": {
|
"django-reversion": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -367,11 +367,11 @@
|
|||||||
},
|
},
|
||||||
"importlib-metadata": {
|
"importlib-metadata": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b",
|
"sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad",
|
||||||
"sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313"
|
"sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==5.1.0"
|
"version": "==6.0.0"
|
||||||
},
|
},
|
||||||
"lxml": {
|
"lxml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -466,61 +466,71 @@
|
|||||||
},
|
},
|
||||||
"msgpack": {
|
"msgpack": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467",
|
"sha256:04366c754ac3bfecf589ea0578599f0c26a3b6558e44cc94d5078bedc67ebfb8",
|
||||||
"sha256:0a68d3ac0104e2d3510de90a1091720157c319ceeb90d74f7b5295a6bee51bae",
|
"sha256:0a8fed756d52f8e8e45e1cb1eac83d96349d563997eed417ffd80eaac426e49e",
|
||||||
"sha256:0df96d6eaf45ceca04b3f3b4b111b86b33785683d682c655063ef8057d61fd92",
|
"sha256:12a5f5e5279a37909ed41dab91b20cc41d6423ddf944141e2d2cf41517f3b119",
|
||||||
"sha256:0dfe3947db5fb9ce52aaea6ca28112a170db9eae75adf9339a1aec434dc954ef",
|
"sha256:13eb94148866fe4f6f93a5253bab1b12b3976c1c859b6b11f3ca7be581f20c12",
|
||||||
"sha256:0e3590f9fb9f7fbc36df366267870e77269c03172d086fa76bb4eba8b2b46624",
|
"sha256:1c19803007800ed7ff492b21dc84872ea2ef7577800c97939a50f1ecef099fb2",
|
||||||
"sha256:11184bc7e56fd74c00ead4f9cc9a3091d62ecb96e97653add7a879a14b003227",
|
"sha256:1e600cb89997f4cda23f93b29c9ad4ae09884573ec87476d46df264b86a92cc3",
|
||||||
"sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88",
|
"sha256:20a26548e6fbd0998846d51835d79e2c9a1542d11228872baec61baf87264e92",
|
||||||
"sha256:1276e8f34e139aeff1c77a3cefb295598b504ac5314d32c8c3d54d24fadb94c9",
|
"sha256:2371e14ff3b17f5774f50602fb139e1df39ee3ca44eb3ae82683ac9b1db5e4ed",
|
||||||
"sha256:1576bd97527a93c44fa856770197dec00d223b0b9f36ef03f65bac60197cedf8",
|
"sha256:290f9a656d34aa20cb672ee11ebd5c6647d08419c88614823562997ecb566c16",
|
||||||
"sha256:1e91d641d2bfe91ba4c52039adc5bccf27c335356055825c7f88742c8bb900dd",
|
"sha256:2cd4e24daff07eedf168f6e7db1b2c0831bed748d8b7254053d4b2334c206ed5",
|
||||||
"sha256:26b8feaca40a90cbe031b03d82b2898bf560027160d3eae1423f4a67654ec5d6",
|
"sha256:318956e96edd3c02a183e96af10f471c1fa18c29add5c317871de3532302609c",
|
||||||
"sha256:2999623886c5c02deefe156e8f869c3b0aaeba14bfc50aa2486a0415178fce55",
|
"sha256:31b4112b43af2a78d005c9192d2a5f0cec62c6a731ca93e77a0d3979da585d9b",
|
||||||
"sha256:2a2df1b55a78eb5f5b7d2a4bb221cd8363913830145fad05374a80bf0877cb1e",
|
"sha256:3729619996e9a0db56d5dc00de1d72e401aee6695d59cbfb62815a5605c66cdb",
|
||||||
"sha256:2bb8cdf50dd623392fa75525cce44a65a12a00c98e1e37bf0fb08ddce2ff60d2",
|
"sha256:42418455bb0aba4591f8f90ac4b783834e6cb0d880c0b92a71423bf59ccc38b9",
|
||||||
"sha256:2cc5ca2712ac0003bcb625c96368fd08a0f86bbc1a5578802512d87bc592fe44",
|
"sha256:44b913a7b9a4a7726bb004aed024670682669a15f77dc2ad8d87a179d9e26e94",
|
||||||
"sha256:35bc0faa494b0f1d851fd29129b2575b2e26d41d177caacd4206d81502d4c6a6",
|
"sha256:4655afa670c7f05bb560a00640d725629c3f2d4f36267c0d3b9645bdecee9b74",
|
||||||
"sha256:3c11a48cf5e59026ad7cb0dc29e29a01b5a66a3e333dc11c04f7e991fc5510a9",
|
"sha256:469c8f3d9458b0d4fc2fa691b914eced40465a95a623e87f75bc40a74e31dfea",
|
||||||
"sha256:449e57cc1ff18d3b444eb554e44613cffcccb32805d16726a5494038c3b93dab",
|
"sha256:47d9123a621b18b4c7a63739acbb56de4f89b92b3e493cb165593474cff3c60f",
|
||||||
"sha256:462497af5fd4e0edbb1559c352ad84f6c577ffbbb708566a0abaaa84acd9f3ae",
|
"sha256:4df078e1a38a26d9f8addabf0df24fcf0abc2161bb7b43b2cfdd178d8a127a12",
|
||||||
"sha256:4733359808c56d5d7756628736061c432ded018e7a1dff2d35a02439043321aa",
|
"sha256:4e4d1c09fe6a3104a001e6197e46e34237f1858ca470b97a87cb7d29fdc359fe",
|
||||||
"sha256:48f5d88c99f64c456413d74a975bd605a9b0526293218a3b77220a2c15458ba9",
|
"sha256:512df5ec1f97ae44c3307049be05cc901b255b297aae5c88508e3058a3874270",
|
||||||
"sha256:49565b0e3d7896d9ea71d9095df15b7f75a035c49be733051c34762ca95bbf7e",
|
"sha256:53cbf882e4b11aba6cdeec41abe576d4cc7dbf22e7a431f95d8127b32768709f",
|
||||||
"sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250",
|
"sha256:556c17b6bbfeb5e31e52baa3e39d04e863dabd98b459538f73aa958bc4bc4043",
|
||||||
"sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce",
|
"sha256:5629026acea9c4e2c2e684de7b313ef82e516e2e88049b3eefcc6316da43ce40",
|
||||||
"sha256:4dea20515f660aa6b7e964433b1808d098dcfcabbebeaaad240d11f909298075",
|
"sha256:5d73c893dd03129c67cb2bea65733bdf1c52cf78e51fb599b81146c1ae8a51f0",
|
||||||
"sha256:545e3cf0cf74f3e48b470f68ed19551ae6f9722814ea969305794645da091236",
|
"sha256:61b202019a014ad3e7e5953430fe5838125196ad4fb27c15e521b22724add939",
|
||||||
"sha256:63e29d6e8c9ca22b21846234913c3466b7e4ee6e422f205a2988083de3b08cae",
|
"sha256:631bdeacad61e2bdee929835622025131d9971bd9aed4cbad9e44a46caa42069",
|
||||||
"sha256:6916c78f33602ecf0509cc40379271ba0f9ab572b066bd4bdafd7434dee4bc6e",
|
"sha256:6322b441d0ddab56ca5e79904dd2f79494d33636fdf53be0d01a23ebb56d2613",
|
||||||
"sha256:6a4192b1ab40f8dca3f2877b70e63799d95c62c068c84dc028b40a6cb03ccd0f",
|
"sha256:669450ebc749e8ac27d07b750643e8e2ff8976ba95ebcc2e12eb00999f3cf500",
|
||||||
"sha256:6c9566f2c39ccced0a38d37c26cc3570983b97833c365a6044edef3574a00c08",
|
"sha256:68726d2404250b6b3b3e63df7e2c4243d46846c630d356a8d129f4aec72ced56",
|
||||||
"sha256:76ee788122de3a68a02ed6f3a16bbcd97bc7c2e39bd4d94be2f1821e7c4a64e6",
|
"sha256:6e733b50bbcedd04e82922c80e7f045530f8bd19ce004c006316eef511b623bb",
|
||||||
"sha256:7760f85956c415578c17edb39eed99f9181a48375b0d4a94076d84148cf67b2d",
|
"sha256:7d18a179e7e26da21f85e3b807f317316da28c62f4213e6864191fa9aabe482a",
|
||||||
"sha256:77ccd2af37f3db0ea59fb280fa2165bf1b096510ba9fe0cc2bf8fa92a22fdb43",
|
"sha256:90703d9c8eae435fcb2f84a545183a23670b5662e6e9e7ee6dfdcd8f69a373f5",
|
||||||
"sha256:81fc7ba725464651190b196f3cd848e8553d4d510114a954681fd0b9c479d7e1",
|
"sha256:969e6ee8f82b7ff0f831b1d3ceb84eafe9b58f5300cc024a96041c7a8c20d559",
|
||||||
"sha256:85f279d88d8e833ec015650fd15ae5eddce0791e1e8a59165318f371158efec6",
|
"sha256:9c57c6730e94801b341c87d56edbf923165dda6d000f2c1c1d5fb74f257cd802",
|
||||||
"sha256:9667bdfdf523c40d2511f0e98a6c9d3603be6b371ae9a238b7ef2dc4e7a427b0",
|
"sha256:a34b0dfb71eb8807cf082d59c0666715df51fc49e734c0f171df5bbb86e02570",
|
||||||
"sha256:a75dfb03f8b06f4ab093dafe3ddcc2d633259e6c3f74bb1b01996f5d8aa5868c",
|
"sha256:a43019ea96dc4632dc2626c76b5413e5a4e1294781e9f5241435076897140594",
|
||||||
"sha256:ac5bd7901487c4a1dd51a8c58f2632b15d838d07ceedaa5e4c080f7190925bff",
|
"sha256:aa9a797de3c755e9bb47a8c6f592b4c0dbb296cee584d3cd0e36b53be0c31e80",
|
||||||
"sha256:aca0f1644d6b5a73eb3e74d4d64d5d8c6c3d577e753a04c9e9c87d07692c58db",
|
"sha256:bbe299a9e7b7d24e688f1e4dac09eb5b01d8eb8eaca944aae5d8f8aef6c73c37",
|
||||||
"sha256:b17be2478b622939e39b816e0aa8242611cc8d3583d1cd8ec31b249f04623243",
|
"sha256:bea6b16a3537ad712bc9b7189970bdf28c56a0cec0a0b46a9f3db3ac0a853335",
|
||||||
"sha256:c1683841cd4fa45ac427c18854c3ec3cd9b681694caf5bff04edb9387602d661",
|
"sha256:c65fd6feb88efe81765b51ad1150b9db682794fb2ab6ddf0e77a6fb4750eca92",
|
||||||
"sha256:c23080fdeec4716aede32b4e0ef7e213c7b1093eede9ee010949f2a418ced6ba",
|
"sha256:c81463959da83fc74ff9bfba7d0a5c6d21b44e799f78c28fe57c75b300160f5d",
|
||||||
"sha256:d5b5b962221fa2c5d3a7f8133f9abffc114fe218eb4365e40f17732ade576c8e",
|
"sha256:cb4a0545afb15189601c1e4e7cf82765456ef45985dc293297c854c4045afe31",
|
||||||
"sha256:d603de2b8d2ea3f3bcb2efe286849aa7a81531abc52d8454da12f46235092bcb",
|
"sha256:cbd3af673fa93706c59e66519f6110d4a317892ddeae7a9718dde3e0e9a9a6df",
|
||||||
"sha256:e83f80a7fec1a62cf4e6c9a660e39c7f878f603737a0cdac8c13131d11d97f52",
|
"sha256:ceed735d624af7e1834db1995ad293389e66306025c7c791db2ac42e006dbd25",
|
||||||
"sha256:eb514ad14edf07a1dbe63761fd30f89ae79b42625731e1ccf5e1f1092950eaa6",
|
"sha256:cf7aec2bf2ff7bf7e8a07de04b593c1076f51941a28dd23d2af5b07c23f60ee9",
|
||||||
"sha256:eba96145051ccec0ec86611fe9cf693ce55f2a3ce89c06ed307de0e085730ec1",
|
"sha256:d1960d6c57e30f60c132e2649e5fefb0bd29b1b55c707c0c5ecfa7f08def82d1",
|
||||||
"sha256:ed6f7b854a823ea44cf94919ba3f727e230da29feb4a99711433f25800cf747f",
|
"sha256:d6788d652256e38b19f7578eb7dd4f96de10fe20546ebf5519bef22aa18c6109",
|
||||||
"sha256:f0029245c51fd9473dc1aede1160b0a29f4a912e6b1dd353fa6d317085b219da",
|
"sha256:d6a73d8f30e06562efc35f5f9699221eb240b18691807b32ef29bae7f66e0da1",
|
||||||
"sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f",
|
"sha256:d896df74ce25ff2e0b2d5bdd0344eff01e05814cd9b168f9321bd459f476981e",
|
||||||
"sha256:fb62ea4b62bfcb0b380d5680f9a4b3f9a2d166d9394e9bbd9666c0ee09a3645c",
|
"sha256:d98a89e53df1540f3f465a510b511e97d21e1b1777b9f5e030184e1cc68d1072",
|
||||||
"sha256:fcb8a47f43acc113e24e910399376f7277cf8508b27e5b88499f053de6b115a8"
|
"sha256:da5db8a4d8b532bbe1e4aa1fabfb21f49f30ee7db49d4885c448c7a9ea032138",
|
||||||
|
"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.4"
|
"version": "==1.0.5rc1"
|
||||||
},
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -540,45 +550,42 @@
|
|||||||
},
|
},
|
||||||
"pikepdf": {
|
"pikepdf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:08ca2566c3ddcb633bbfb90c9e2b7134f222ab48f642f056c266fd69a4c0844e",
|
"sha256:0a0914d482aa1b80584659c44aef1b2770b473a504cedc209fa6db3f24575ef6",
|
||||||
"sha256:15eff12d7b5dc4eeeb9fbe5148d5e93cdc9f632a352d7df5191409837cdada6f",
|
"sha256:1b8dfdc2184aca33e271b104e0ec468e52ac6591ab51bcd32c2e53bc8cdeeffe",
|
||||||
"sha256:24717a49cab8cf0ccb360ae38fefe4b09e23d394086ef6da740071165a2542d8",
|
"sha256:1faed2d2553aafb6f4969f0a970958d1847869631eb1c82f5a91ef636817b93a",
|
||||||
"sha256:2c1a3a1ae0d787a6819ffb9d456b0ccbf2b8d48ad8b0748f765a6a306b92184b",
|
"sha256:260efb3c6aa44c013da2278872593bc4712facd5b766de2b2d88c53c5f524449",
|
||||||
"sha256:2c653bc771cc6065642f5a734ecb771ff16b5412cf05a5c3435072ac01f84fbe",
|
"sha256:3af2fd5762e222bf5133acec4f7d56719dda4a9b7dd468eb1c37a10055ff64b2",
|
||||||
"sha256:2e9c9cc68bc123b88007b321a494ab054c3ce378ee2f77bba1b90c8bdaaad59e",
|
"sha256:3e1490beb13b2d1a509aeb98fb0669ab7dea4035abd1df0e12085393f556654a",
|
||||||
"sha256:32a7775f8b2117e72fa101427edf4526f0886e785146a7bfdf2a877d27f94dbf",
|
"sha256:422cb31ed4b489b9e18f4a803fab7c6ea10ef6916960a5d8b5e531c5af3bfbca",
|
||||||
"sha256:4675c743ac873d8a1ff9d06c1d5fab15e9ca66aa1abf73fcbca4b86c976b214e",
|
"sha256:4290059bdf8d05cf3a7ba185d64b5756c745f178fc102aec41bbcd4a057e02f8",
|
||||||
"sha256:4800fd878876d9a780e2d1125e971d680a0de466440bd6bcbc54ac7cfb925fd6",
|
"sha256:5aca06b88b2d53122baaf3009bcfaec291b3c408846e401cddf8b2e89a0e0fe6",
|
||||||
"sha256:5b1271b200ece3e16ffa97c76ff93c057d3676f2ecee89ae52efd5e4804874ab",
|
"sha256:7eef9fe4d06cb01482486561292b3c3675d7506328f990cb60b26994dd7ac1d8",
|
||||||
"sha256:5b1c975d02e75aa8a0e09679ed23f85b6eddf91ac7c85189dc18f1ef8599ce70",
|
"sha256:80d4eb3624980c1292d7e2db66c569f146012e86004b8739a3580346ee8f69a9",
|
||||||
"sha256:5b295d9579081ab46ce7bb463eccbc62e0c60cbe546f29c0a9135abd5cf76a24",
|
"sha256:81caa67a08fdf683c521497fafd48d9b7bdf02549625329d8a1bb8ce706ef362",
|
||||||
"sha256:5f040aa7d9327b6d9087848dc8e35d2b8515bf17a04e2bdb20f8c51931810213",
|
"sha256:910a45cc6506dd899032638c3775f708278d99ccc9c3681fb75a57b55051d262",
|
||||||
"sha256:62d724e37a2f004f63615513d3f877334e679dd43b6c55cdfe9b8af56ff52400",
|
"sha256:93cd0357140dfa79e16c1d9249775d11eebb392665fcdb1528684aae71966b4e",
|
||||||
"sha256:6452fa9d10b15bb60b2343b7ac08123e0c5b87f852250538d32bf556f09b8c3a",
|
"sha256:93d98f460eb209b89ce855a5defb059ca82326042ee52dcb692a05e1c1a24bef",
|
||||||
"sha256:66679ee6b364b3b9659a72697e053554e2f4c231371173745b59114f00a821e5",
|
"sha256:9411824a7aa477fcad209e6e01cf23f0ecbd6833805812eca2026e372c724096",
|
||||||
"sha256:6b21fde29bd006cb0889e05a0407f0825b5650ae22272ba70720fc68a2e44a0e",
|
"sha256:a32215111f6713c934b9ed9a6fd686940559953539e81f70bfb860efbddfe3c3",
|
||||||
"sha256:788df6789df19a381976f6644b78a65da92c5d4fd45714682a1cffb0cd3ff410",
|
"sha256:a59559a1e480f4a7229f600d6fce22b1d32729df8552099542e37a95c02a572f",
|
||||||
"sha256:7a6de8d04df3dc4f37ddbdfdbecdb4c3d20d479699260902ec06fa531cbfbae6",
|
"sha256:ab07529096da5ea410dea81add8d724ac91f093b982b86f503771bfe8864e48c",
|
||||||
"sha256:80a4546f59bb38121c20fc37305a388b8f9d561c6bbcc77a3415afdd8a7ab16d",
|
"sha256:b0f1ca24118517970cfc78f902ded681c2e399dfb21ff04745143b4156dd551e",
|
||||||
"sha256:856e527970fa2516858662906173004568a38f95b46ef95c8231c529f3e71581",
|
"sha256:b7711e2c0ff1ad265f5c0497f519584c029c9906d72ad78e89e260c57964a5c0",
|
||||||
"sha256:8874d4d38b2da71837352576c5ec10c03b2a7976124b95721564d8c456c9824b",
|
"sha256:b860c44503b6bb237b97c2ae06d47dc645140649d61d2f5dd5276438bc60e2e0",
|
||||||
"sha256:913437609c3e5b9109a2fe1100f52bdcead4e56e8c83ca8a9c3b520cd7720016",
|
"sha256:b90bb4e77f9ea8a21d94c5d25e1b416f08a668377ea48edf8808be49f09f906a",
|
||||||
"sha256:91fd023805733163927ae04b16d48374f04f03f204f2cfa45d442b052314de83",
|
"sha256:ba75e70c932830fb7253b343f43f0ce03317661971cd3df03fc27c7cdb06992e",
|
||||||
"sha256:9d9fe5e3fbd54a45fe7678c08d24c1e8347a1e48adcb4f32c6c25ef380f056ca",
|
"sha256:bf9129507b7258dda27845e1ee6c7c4121674a04c0a1a0ac1115c19e5b4a2edf",
|
||||||
"sha256:a98fcebb4303b8b2f5cde0d56f2406a53e74a9ad4bc1e1bc12bdc2b3dd4def1f",
|
"sha256:cf912ab70313fd0f23a32811f8c3ea815a716ecfd6ee84a564c833d0c06f5483",
|
||||||
"sha256:aa7d3831fa016634f2ead255717644ba2dddf462117b8558b783ac2ba94583bb",
|
"sha256:dec854f908973c5c3760d246539c58c03b7b701a2bed45173f9a4e4d766d3eab",
|
||||||
"sha256:ae0ba99841d80107becaec6ed067b63c455f51a9bb83b0e88efd3a098753735b",
|
"sha256:e5bebaca43757c9c357637954b8e49c9221c21a40260394ec4c4fbafada5ee87",
|
||||||
"sha256:b77e2e134315d25dd004b4ffbd2d5032352da164b21bc3bd92768379610fa2b5",
|
"sha256:e900d2314a1019c98c9d3d50445af475ce2f16e8a54e44875201869f8561a3b2",
|
||||||
"sha256:c5a2710aacb1fa25edd77feba51dcadf85a9b735f2190e8b45edf0e1e79b5d1d",
|
"sha256:eb310e903b9a172de352446458390ccce31a2bb19a387f63d37b135cb4cca3f1",
|
||||||
"sha256:c768e55caf96024218866a5b5f95fdded2ef001b2d5463d1c89e57d07bdab928",
|
"sha256:edea85799240a3b7534650b275bc7e87519ddf0f47ccf9bdad09b22381da5442",
|
||||||
"sha256:d55b1d8eb3b17e55682b91c042c48e2d0c67be4c1e30b7e539a82e03977eed29",
|
"sha256:f7f0bfa897ecabbee3f7dc2ac10a8dd954c42a6f858fbef13019d0cef1b60127",
|
||||||
"sha256:d57a35b3ed3f8ef98aa528d4981d1376ccc1ba64ab937f2ac2ed0aadee1d5911",
|
"sha256:fcfd391b3a255b460a9040ca8e47dd6f36c6ea43d2b61cd00b5ecb06000a6b8f"
|
||||||
"sha256:e35110f69406e5b9cf6330d16106877372f6aa955fdc1a920d53babcd6a5ae3d",
|
|
||||||
"sha256:e3ff858d504312ea07519121d3ecdf1b5bc40541f957d2b85d271a2ca6284b70",
|
|
||||||
"sha256:f721cd86d8ddf0624306fcd1fed3e086387fcb2e32baeafbd1ba92a7c40c5563"
|
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==6.2.5"
|
"version": "==7.1.1"
|
||||||
},
|
},
|
||||||
"pillow": {
|
"pillow": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -866,19 +873,19 @@
|
|||||||
},
|
},
|
||||||
"sentry-sdk": {
|
"sentry-sdk": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:675f6279b6bb1fea09fd61751061f9a90dca3b5929ef631dd50dc8b3aeb245e9",
|
"sha256:69ecbb2e1ff4db02a06c4f20f6f69cb5dfe3ebfbc06d023e40d77cf78e9c37e7",
|
||||||
"sha256:8b4ff696c0bdcceb3f70bbb87a57ba84fd3168b1332d493fcd16c137f709578c"
|
"sha256:7ad4d37dd093f4a7cb5ad804c6efe9e8fab8873f7ffc06042dc3f3fd700a93ec"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.11.1"
|
"version": "==1.15.0"
|
||||||
},
|
},
|
||||||
"setuptools": {
|
"setuptools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:95f00380ef2ffa41d9bba85d95b27689d923c93dfbafed4aecd7cf988a25e012",
|
"sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330",
|
||||||
"sha256:bb6d8e508de562768f2027902929f8523932fcd1fb784e6d573d2cafac995a48"
|
"sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==67.3.2"
|
"version": "==67.4.0"
|
||||||
},
|
},
|
||||||
"simplejson": {
|
"simplejson": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -957,11 +964,11 @@
|
|||||||
},
|
},
|
||||||
"soupsieve": {
|
"soupsieve": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759",
|
"sha256:49e5368c2cda80ee7e84da9dbe3e110b70a4575f196efb74e51b94549d921955",
|
||||||
"sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"
|
"sha256:e28dba9ca6c7c00173e34e4ba57448f0688bb681b7c5e8bf4971daafc093d69a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.3.2.post1"
|
"version": "==2.4"
|
||||||
},
|
},
|
||||||
"sqlparse": {
|
"sqlparse": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1020,11 +1027,11 @@
|
|||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc",
|
"sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72",
|
||||||
"sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"
|
"sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.26.13"
|
"version": "==1.26.14"
|
||||||
},
|
},
|
||||||
"webencodings": {
|
"webencodings": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1256,14 +1263,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -1280,154 +1279,70 @@
|
|||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2022.12.7"
|
"version": "==2022.12.7"
|
||||||
},
|
},
|
||||||
"charset-normalizer": {
|
"chardet": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b",
|
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||||
"sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42",
|
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||||
"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"
|
|
||||||
],
|
],
|
||||||
"version": "==3.0.1"
|
"index": "pypi",
|
||||||
|
"version": "==4.0.0"
|
||||||
},
|
},
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79",
|
"sha256:00858a6213ea829ab417b6e05ac0a4c22eac7d3aae67c0187de2935d0548786b",
|
||||||
"sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a",
|
"sha256:08fd9ad5dfc490b7403027b20eebb8ac470621ae1ce0b33a13cab9ec8d4aed0a",
|
||||||
"sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f",
|
"sha256:0c52c8f0243a2e4c0b81db2f6468a9084dd380e0b69e931253aa24529eb812f3",
|
||||||
"sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a",
|
"sha256:0e857ef99769a54595c8801086e310dabb8205a1e742d66f6702544aeddfb1ba",
|
||||||
"sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa",
|
"sha256:1b1cca186e74d258d983a1e1a134ffba0b991effbc8e46ee65c5fbf4009dfce1",
|
||||||
"sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398",
|
"sha256:1dcb5c17b361b35d2a339c6031417f8dcce915b09ea55e7214a398833ec9a63f",
|
||||||
"sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba",
|
"sha256:22cca1925841e2655ce35a4e17c21b42dd0de2b85c5d6fe9c5bf4a45f58950f3",
|
||||||
"sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d",
|
"sha256:25ab1d4ae4bce324d427732bf0f7967493405daa0c2675385016102b0a5e87bf",
|
||||||
"sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf",
|
"sha256:2ca9c7735da025b0f0ca00ab15c5290798b62a49feaf312cb895ab4c4bc1575e",
|
||||||
"sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b",
|
"sha256:3008ace59d566e110e9351c855c6bf2f2b4037f772caffdfaa977c485bf96e8e",
|
||||||
"sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518",
|
"sha256:3de7363b0f21ac6fc97767f78036b900006e06eadd3cc72f040d57494405f44c",
|
||||||
"sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d",
|
"sha256:409d14e37de692f94689578cbbb0a26408da9d9354f8ff658e148f1750940b2f",
|
||||||
"sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795",
|
"sha256:425ba4ae75be4e2c9ce336a523265e6e1214ad624e8d18fb638771475dec2ebf",
|
||||||
"sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2",
|
"sha256:42d7ee01583d4db8098510d08e7505db0f5dbb70edc88a7350cafc336ae81048",
|
||||||
"sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e",
|
"sha256:4e07fc0e0dc8bdeae4f23b8ff821c711dcb2537bbc782f61ff726ca07fcfdb9b",
|
||||||
"sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32",
|
"sha256:4f32113f131edcd26266b8bfc9e24698b6dee4d9ea63362b7dd3cf0a351231a6",
|
||||||
"sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745",
|
"sha256:539ca9a37aaab0ed31ccb535039e33170cf2144b8cd5006c48ad724ba2ab5797",
|
||||||
"sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b",
|
"sha256:572867facf73374a9c8686691bf1b43abe3425f31a2a9b48043d0de9f669ad0d",
|
||||||
"sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e",
|
"sha256:583f5b9a414fcabe1c14d82519b9d24dfd69c3505e9030415c3c6b692bff9062",
|
||||||
"sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d",
|
"sha256:590411083d46c182e852e879a533fa99988937a3af96f836cacbc16a1bcfd058",
|
||||||
"sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f",
|
"sha256:5a0bc8377854cc2f447093149bc9774b0628e9db218f85026d7982466840040e",
|
||||||
"sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660",
|
"sha256:5f3ee269b6d32913eccd78eefce6da7d5120d8fbef059c463c028267c1a0d1ce",
|
||||||
"sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62",
|
"sha256:639bc5e8cf323a50d17b52554269e72c21c2cb5ad14ca1b43e095ce60abbc2b3",
|
||||||
"sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6",
|
"sha256:644b9c4e7e951aa210a8150b09f9c02dcea8701d14bff1564a50e054ce0ae48d",
|
||||||
"sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04",
|
"sha256:6591db6f6bbd5120c9475fdb12305a3216355ae4797b0e44528040f6d0d8f73f",
|
||||||
"sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c",
|
"sha256:7538a24505abb5dc61ac3bbf58d5232a76ad6fd2be63cf797c2e1caa9c60077c",
|
||||||
"sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5",
|
"sha256:75598efc204f513cc4d5ca99a8f9103867993c091e5cd62d78c1020a0affc7be",
|
||||||
"sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef",
|
"sha256:7911833a156476096d209569cbe600faf22a057a46c5e8bb19fffc387abad101",
|
||||||
"sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc",
|
"sha256:7f96cd694673191583acaef50ed01c8db3b47f49602b7046a15775fa6f753e9f",
|
||||||
"sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae",
|
"sha256:89230ec0b1f3817237a8f98fc593dec061eebd753cea097772e7abcf5fb9c6bc",
|
||||||
"sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578",
|
"sha256:959d65e8c5f84878a741dcddcbf71ccc22270c6981e5dfe0806517d49be0c1f2",
|
||||||
"sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466",
|
"sha256:98220679df217b9635c3c6a7a490c408f4de169c33ac4f708a86f9e97b2d9b14",
|
||||||
"sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4",
|
"sha256:996c74a93f6fac2099c288e709e7d0bcc37f3c700d878d7d52accdcc2b6550bd",
|
||||||
"sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91",
|
"sha256:9ec68b342a82dc821d4384e7a5b266c2b78bc5ec3a59fcadf8e96445f4002366",
|
||||||
"sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0",
|
"sha256:b3b6582423aeece24478028b8c9127cd1392d584dfade6c925421c91710cdad5",
|
||||||
"sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4",
|
"sha256:b48273db5287a185017f2150eb49581245777ba30c6e749bfd5567afcab27c3f",
|
||||||
"sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b",
|
"sha256:c1b862d4718a103cd090b6b91155503574918c498a381a13970e22785c7ae5a3",
|
||||||
"sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe",
|
"sha256:c87885ca7357e85e9e1550d804c7b2c42d6e4e8260849af499fc2b0dfe58962c",
|
||||||
"sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b",
|
"sha256:c976faf3bed96d2b94ee8b005ff26a075cfbc00782b532342119cbd172481f81",
|
||||||
"sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75",
|
"sha256:ca922b6558e1fe09c2ffc772faaa411f94cb47845d366d7aa6a887d934a25200",
|
||||||
"sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b",
|
"sha256:e0101d0cb004db88891ffb87ddfccd93ee76abbe4c0bf784c4214f467d026dd2",
|
||||||
"sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c",
|
"sha256:e7a0f9ab01ccd873d21584ddcad488ad752944f6a9e5bdff1aefbe5289ffd823",
|
||||||
"sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72",
|
"sha256:e83b73d8edf255187388b8d14c0c0df580bbf8e7099060e590915e3fbcf39598",
|
||||||
"sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b",
|
"sha256:e8b80f94e15676dbaa7ce2cef6e5433cdb2427d3d81ea9fe4c3d788fae3bc4a2",
|
||||||
"sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f",
|
"sha256:e9c1a662c837cf9b4b815f977404475b555fafc0fb12ae92667d0cdbf0f3c9eb",
|
||||||
"sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e",
|
"sha256:eb54a60e3819d60de6828b5bd197996f9ecb2306d280bd532a4a4291f3285658",
|
||||||
"sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53",
|
"sha256:efb09e5004fd1e4d05cf433d7ad7a6784d090c0afd68b46e8ef785ae169a31ed",
|
||||||
"sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3",
|
"sha256:f0ae2fd15eedb5f749cd9b3da01087b7dba2f76cc783866459d8b3f3feb7c969",
|
||||||
"sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84",
|
"sha256:f0aefc3015ae4a188dc48f2ea934ddbdf158c8c4b0b3d5691acfdad684857702",
|
||||||
"sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"
|
"sha256:f47e5f3c5acbc3b843ae89b042faf64b366a4976099813487161ce3c50649db3",
|
||||||
|
"sha256:fd868a0eb8eb35a84847935fe36a5b285fed2e4b99c2b90cf44778fa0e9418e9"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.7.0'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==6.5.0"
|
"version": "==6.6.0b1"
|
||||||
},
|
},
|
||||||
"coveralls": {
|
"coveralls": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1467,14 +1382,6 @@
|
|||||||
"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",
|
||||||
@@ -1488,17 +1395,9 @@
|
|||||||
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
|
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
|
||||||
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
|
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.7.0'",
|
"markers": "python_version >= '3.7'",
|
||||||
"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",
|
||||||
@@ -1565,21 +1464,13 @@
|
|||||||
"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:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71",
|
"sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5",
|
||||||
"sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"
|
"sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==7.2.0"
|
"version": "==7.2.1"
|
||||||
},
|
},
|
||||||
"pytest-cov": {
|
"pytest-cov": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1618,11 +1509,11 @@
|
|||||||
"psutil"
|
"psutil"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:40fdb8f3544921c5dfcd486ac080ce22870e71d82ced6d2e78fa97c2addd480c",
|
"sha256:336098e3bbd8193276867cc87db8b22903c3927665dff9d1ac8684c02f597b68",
|
||||||
"sha256:70a76f191d8a1d2d6be69fc440cdf85f3e4c03c08b520fd5dc5d338d6cf07d89"
|
"sha256:fa10f95a2564cd91652f2d132725183c3b590d9fdcdec09d3677386ecf4c1ce9"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.1.0"
|
"version": "==3.2.0"
|
||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1642,26 +1533,11 @@
|
|||||||
},
|
},
|
||||||
"setuptools": {
|
"setuptools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:95f00380ef2ffa41d9bba85d95b27689d923c93dfbafed4aecd7cf988a25e012",
|
"sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330",
|
||||||
"sha256:bb6d8e508de562768f2027902929f8523932fcd1fb784e6d573d2cafac995a48"
|
"sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==67.3.2"
|
"version": "==67.4.0"
|
||||||
},
|
|
||||||
"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": [
|
||||||
@@ -1678,37 +1554,13 @@
|
|||||||
"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:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc",
|
"sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72",
|
||||||
"sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"
|
"sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.26.13"
|
"version": "==1.26.14"
|
||||||
},
|
|
||||||
"wsproto": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
|
|
||||||
"sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
|
|
||||||
],
|
|
||||||
"markers": "python_full_version >= '3.7.0'",
|
|
||||||
"version": "==1.2.0"
|
|
||||||
},
|
},
|
||||||
"zope.component": {
|
"zope.component": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1718,6 +1570,22 @@
|
|||||||
"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",
|
||||||
@@ -1829,6 +1697,52 @@
|
|||||||
],
|
],
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ 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')
|
||||||
@@ -35,6 +36,7 @@ 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:
|
||||||
|
|||||||
@@ -321,45 +321,60 @@ class OEmbedView(generic.View):
|
|||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_info_string(user):
|
||||||
|
user_str = f"by {user.name} " if user else ""
|
||||||
|
time = timezone.now().strftime('%d/%m/%Y %H:%I')
|
||||||
|
return f"[Paperwork generated {user_str}on {time}"
|
||||||
|
|
||||||
|
|
||||||
|
def render_pdf_response(template, context, append_terms):
|
||||||
|
merger = PdfFileMerger()
|
||||||
|
rml = template.render(context)
|
||||||
|
buffer = rml2pdf.parseString(rml)
|
||||||
|
merger.append(PdfFileReader(buffer))
|
||||||
|
buffer.close()
|
||||||
|
|
||||||
|
if append_terms:
|
||||||
|
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
|
||||||
|
merger.append(BytesIO(terms.read()))
|
||||||
|
|
||||||
|
merged = BytesIO()
|
||||||
|
merger.write(merged)
|
||||||
|
|
||||||
|
response = HttpResponse(content_type='application/pdf')
|
||||||
|
f = context['filename']
|
||||||
|
response['Content-Disposition'] = f'filename="{f}"'
|
||||||
|
response.write(merged.getvalue())
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class PrintView(generic.View):
|
class PrintView(generic.View):
|
||||||
append_terms = False
|
append_terms = False
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
obj = get_object_or_404(self.model, pk=self.kwargs['pk'])
|
obj = get_object_or_404(self.model, pk=self.kwargs['pk'])
|
||||||
user_str = f"by {self.request.user.name} " if self.request.user is not None else ""
|
|
||||||
time = timezone.now().strftime('%d/%m/%Y %H:%I')
|
|
||||||
object_name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', obj.name)
|
object_name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', obj.name)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'object': obj,
|
'object': obj,
|
||||||
'current_user': self.request.user,
|
'current_user': self.request.user,
|
||||||
'object_name': object_name,
|
'object_name': object_name,
|
||||||
'info_string': f"[Paperwork generated {user_str}on {time} - {obj.current_version_id}]",
|
'info_string': get_info_string(self.request.user) + f"- {obj.current_version_id}]",
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
template = get_template(self.template_name)
|
return render_pdf_response(get_template(self.template_name), self.get_context_data(), self.append_terms)
|
||||||
|
|
||||||
merger = PdfFileMerger()
|
|
||||||
|
|
||||||
context = self.get_context_data()
|
class PrintListView(generic.ListView):
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
context['current_user'] = self.request.user
|
||||||
|
context['info_string'] = get_info_string(self.request.user) + "]"
|
||||||
|
return context
|
||||||
|
|
||||||
rml = template.render(context)
|
def get(self, request):
|
||||||
buffer = rml2pdf.parseString(rml)
|
self.object_list = self.get_queryset()
|
||||||
merger.append(PdfFileReader(buffer))
|
return render_pdf_response(get_template(self.template_name), self.get_context_data(), False)
|
||||||
buffer.close()
|
|
||||||
|
|
||||||
if self.append_terms:
|
|
||||||
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
|
|
||||||
merger.append(BytesIO(terms.read()))
|
|
||||||
|
|
||||||
merged = BytesIO()
|
|
||||||
merger.write(merged)
|
|
||||||
|
|
||||||
response = HttpResponse(content_type='application/pdf')
|
|
||||||
f = context['filename']
|
|
||||||
response['Content-Disposition'] = f'filename="{f}"'
|
|
||||||
response.write(merged.getvalue())
|
|
||||||
return response
|
|
||||||
|
|||||||
@@ -124,6 +124,22 @@ 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")
|
||||||
|
|||||||
39
RIGS/migrations/0046_subhire.py
Normal file
39
RIGS/migrations/0046_subhire.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
922
RIGS/models.py
922
RIGS/models.py
@@ -1,922 +0,0 @@
|
|||||||
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')
|
|
||||||
|
|
||||||
|
|
||||||
@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})"
|
|
||||||
4
RIGS/models/__init__.py
Normal file
4
RIGS/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .models import *
|
||||||
|
from .finance import *
|
||||||
|
from .hs import *
|
||||||
|
from .events import *
|
||||||
467
RIGS/models/events.py
Normal file
467
RIGS/models/events.py
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from reversion import revisions as reversion
|
||||||
|
from versioning.versioning import RevisionMixin
|
||||||
|
|
||||||
|
from RIGS.validators import validate_url
|
||||||
|
from .utils import filter_by_pk
|
||||||
|
from .finance import VatRate
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEventManager(models.Manager):
|
||||||
|
def event_search(self, q, start, end, status):
|
||||||
|
filt = Q()
|
||||||
|
if end:
|
||||||
|
filt &= Q(start_date__lte=end)
|
||||||
|
if start:
|
||||||
|
filt &= Q(start_date__gte=start)
|
||||||
|
|
||||||
|
objects = self.all()
|
||||||
|
|
||||||
|
if q:
|
||||||
|
objects = self.search(q)
|
||||||
|
|
||||||
|
if len(status) > 0:
|
||||||
|
filt &= Q(status__in=status)
|
||||||
|
|
||||||
|
qs = objects.filter(filt).order_by('-start_date')
|
||||||
|
|
||||||
|
# Preselect related for efficiency
|
||||||
|
qs.select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
class EventManager(BaseEventManager):
|
||||||
|
def current_events(self):
|
||||||
|
events = self.filter(
|
||||||
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
||||||
|
status=Event.CANCELLED)) | # Starts after with no end
|
||||||
|
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
|
||||||
|
status=Event.CANCELLED)) | # Ends after
|
||||||
|
(models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(
|
||||||
|
status=Event.CANCELLED)) | # Active dry hire
|
||||||
|
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
|
||||||
|
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
|
||||||
|
models.Q(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started
|
||||||
|
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
def events_in_bounds(self, start, end):
|
||||||
|
events = self.filter(
|
||||||
|
(models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds
|
||||||
|
(models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds
|
||||||
|
(models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds
|
||||||
|
(models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds
|
||||||
|
|
||||||
|
(models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after
|
||||||
|
(models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after
|
||||||
|
(models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after
|
||||||
|
(models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after
|
||||||
|
(models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after
|
||||||
|
|
||||||
|
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
|
||||||
|
'organisation',
|
||||||
|
'venue', 'mic')
|
||||||
|
return events
|
||||||
|
|
||||||
|
def active_dry_hires(self):
|
||||||
|
return self.filter(dry_hire=True, start_date__gte=timezone.now(), is_rig=True)
|
||||||
|
|
||||||
|
def rig_count(self):
|
||||||
|
event_count = self.exclude(status=BaseEvent.CANCELLED).filter(
|
||||||
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
|
||||||
|
is_rig=True)) | # Starts after with no end
|
||||||
|
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True)) | # Ends after
|
||||||
|
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True)) # Active dry hire
|
||||||
|
).count()
|
||||||
|
return event_count
|
||||||
|
|
||||||
|
def waiting_invoices(self):
|
||||||
|
events = self.filter(
|
||||||
|
(
|
||||||
|
models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
||||||
|
models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before
|
||||||
|
) & models.Q(invoice__isnull=True) & # Has not already been invoiced
|
||||||
|
models.Q(is_rig=True) # Is a rig (not non-rig)
|
||||||
|
).order_by('start_date') \
|
||||||
|
.select_related('person', 'organisation', 'venue', 'mic') \
|
||||||
|
.prefetch_related('items')
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
def search(self, query=None):
|
||||||
|
qs = self.get_queryset()
|
||||||
|
if query is not None:
|
||||||
|
or_lookup = Q(name__icontains=query) | Q(description__icontains=query) | Q(notes__icontains=query)
|
||||||
|
|
||||||
|
or_lookup = filter_by_pk(or_lookup, query)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if query[0] == "N":
|
||||||
|
val = int(query[1:])
|
||||||
|
or_lookup = Q(pk=val) # If string is N###### then do a simple PK filter
|
||||||
|
except: # noqa
|
||||||
|
pass
|
||||||
|
|
||||||
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
def find_earliest_event_time(event, datetime_list):
|
||||||
|
# If there is no start time defined, pretend it's midnight
|
||||||
|
startTimeFaked = False
|
||||||
|
if event.has_start_time:
|
||||||
|
startDateTime = datetime.datetime.combine(event.start_date, event.start_time)
|
||||||
|
else:
|
||||||
|
startDateTime = datetime.datetime.combine(event.start_date, datetime.time(00, 00))
|
||||||
|
startTimeFaked = True
|
||||||
|
|
||||||
|
# timezoneIssues - apply the default timezone to the naiive datetime
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
startDateTime = tz.localize(startDateTime)
|
||||||
|
datetime_list.append(startDateTime) # then add it to the list
|
||||||
|
|
||||||
|
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
|
||||||
|
|
||||||
|
# if we faked it & it's the earliest, better own up
|
||||||
|
if startTimeFaked and earliest == startDateTime:
|
||||||
|
return event.start_date
|
||||||
|
return earliest
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEvent(models.Model, RevisionMixin):
|
||||||
|
# Done to make it much nicer on the database
|
||||||
|
PROVISIONAL = 0
|
||||||
|
CONFIRMED = 1
|
||||||
|
BOOKED = 2
|
||||||
|
CANCELLED = 3
|
||||||
|
EVENT_STATUS_CHOICES = (
|
||||||
|
(PROVISIONAL, 'Provisional'),
|
||||||
|
(CONFIRMED, 'Confirmed'),
|
||||||
|
(BOOKED, 'Booked'),
|
||||||
|
(CANCELLED, 'Cancelled'),
|
||||||
|
)
|
||||||
|
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
|
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
|
||||||
|
description = models.TextField(blank=True, default='')
|
||||||
|
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
||||||
|
|
||||||
|
# Timing
|
||||||
|
start_date = models.DateField()
|
||||||
|
start_time = models.TimeField(blank=True, null=True)
|
||||||
|
end_date = models.DateField(blank=True, null=True)
|
||||||
|
end_time = models.TimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cancelled(self):
|
||||||
|
return (self.status == self.CANCELLED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def confirmed(self):
|
||||||
|
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_start_time(self):
|
||||||
|
return self.start_time is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_end_time(self):
|
||||||
|
return self.end_time is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_time(self):
|
||||||
|
"""Returns the end of the event - this function could return either a tzaware datetime, or a naiive date object"""
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
endDate = self.end_date
|
||||||
|
if endDate is None:
|
||||||
|
endDate = self.start_date
|
||||||
|
|
||||||
|
if self.has_end_time:
|
||||||
|
endDateTime = datetime.datetime.combine(endDate, self.end_time)
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
endDateTime = tz.localize(endDateTime)
|
||||||
|
|
||||||
|
return endDateTime
|
||||||
|
|
||||||
|
else:
|
||||||
|
return endDate
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
start = self.earliest_time
|
||||||
|
if isinstance(self.earliest_time, datetime.datetime):
|
||||||
|
start = self.earliest_time.date()
|
||||||
|
end = self.latest_time
|
||||||
|
if isinstance(self.latest_time, datetime.datetime):
|
||||||
|
end = self.latest_time.date()
|
||||||
|
return (end - start).days + 1
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
errdict = {}
|
||||||
|
if self.end_date and self.start_date > self.end_date:
|
||||||
|
errdict['end_date'] = ["Unless you've invented time travel, the event can't finish before it has started."]
|
||||||
|
|
||||||
|
startEndSameDay = not self.end_date or self.end_date == self.start_date
|
||||||
|
hasStartAndEnd = self.has_start_time and self.has_end_time
|
||||||
|
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
|
||||||
|
errdict['end_time'] = ["Unless you've invented time travel, the event can't finish before it has started."]
|
||||||
|
return errdict
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.display_id}: {self.name}"
|
||||||
|
|
||||||
|
@reversion.register(follow=['items'])
|
||||||
|
class Event(BaseEvent):
|
||||||
|
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
|
||||||
|
verbose_name="MIC", on_delete=models.CASCADE)
|
||||||
|
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
dry_hire = models.BooleanField(default=False)
|
||||||
|
is_rig = models.BooleanField(default=True)
|
||||||
|
based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
|
access_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
meet_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
# Dry-hire only
|
||||||
|
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
# Monies
|
||||||
|
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
|
||||||
|
|
||||||
|
# Authorisation request details
|
||||||
|
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
|
auth_request_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
auth_request_to = models.EmailField(blank=True, default='')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_id(self):
|
||||||
|
if self.pk:
|
||||||
|
if self.is_rig:
|
||||||
|
return f"N{self.pk:05d}"
|
||||||
|
return self.pk
|
||||||
|
return "????"
|
||||||
|
|
||||||
|
# Calculated values
|
||||||
|
"""
|
||||||
|
EX Vat
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sum_total(self):
|
||||||
|
total = self.items.aggregate(
|
||||||
|
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
|
||||||
|
output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
||||||
|
)['sum_total']
|
||||||
|
if total:
|
||||||
|
return total
|
||||||
|
return Decimal("0.00")
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def vat_rate(self):
|
||||||
|
return VatRate.objects.find_rate(self.start_date)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vat(self):
|
||||||
|
# No VAT is owed on internal transfers
|
||||||
|
if self.internal:
|
||||||
|
return 0
|
||||||
|
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
|
||||||
|
|
||||||
|
"""
|
||||||
|
Inc VAT
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def total(self):
|
||||||
|
return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hs_done(self):
|
||||||
|
return self.riskassessment is not None and len(self.checklists.all()) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def earliest_time(self):
|
||||||
|
"""Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object"""
|
||||||
|
|
||||||
|
# Put all the datetimes in a list
|
||||||
|
datetime_list = []
|
||||||
|
|
||||||
|
if self.access_at:
|
||||||
|
datetime_list.append(self.access_at)
|
||||||
|
|
||||||
|
if self.meet_at:
|
||||||
|
datetime_list.append(self.meet_at)
|
||||||
|
|
||||||
|
earliest = find_earliest_event_time(self, datetime_list)
|
||||||
|
|
||||||
|
return earliest
|
||||||
|
|
||||||
|
@property
|
||||||
|
def internal(self):
|
||||||
|
return bool(self.organisation and self.organisation.union_account)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def authorised(self):
|
||||||
|
if self.internal and hasattr(self, 'authorisation'):
|
||||||
|
return self.authorisation.amount == self.total
|
||||||
|
else:
|
||||||
|
return bool(self.purchase_order)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self):
|
||||||
|
if self.cancelled:
|
||||||
|
return "secondary"
|
||||||
|
elif not self.is_rig:
|
||||||
|
return "info"
|
||||||
|
elif not self.mic:
|
||||||
|
return "danger"
|
||||||
|
elif self.confirmed and self.authorised:
|
||||||
|
if self.dry_hire or self.riskassessment:
|
||||||
|
return "success"
|
||||||
|
return "warning"
|
||||||
|
else:
|
||||||
|
return "warning"
|
||||||
|
|
||||||
|
objects = EventManager()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('event_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def get_edit_url(self):
|
||||||
|
return reverse('event_update', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
errdict = super().clean()
|
||||||
|
|
||||||
|
if self.access_at is not None:
|
||||||
|
if self.access_at.date() > self.start_date:
|
||||||
|
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
|
||||||
|
elif self.start_time is not None and self.start_date == self.access_at.date() and self.access_at.time() > self.start_time:
|
||||||
|
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
|
||||||
|
|
||||||
|
if errdict: # If there was an error when validation
|
||||||
|
raise ValidationError(errdict)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""Call :meth:`full_clean` before saving."""
|
||||||
|
self.full_clean()
|
||||||
|
super(Event, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class EventItem(models.Model, RevisionMixin):
|
||||||
|
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
description = models.TextField(blank=True, default='')
|
||||||
|
quantity = models.IntegerField()
|
||||||
|
cost = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
order = models.IntegerField()
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_cost(self):
|
||||||
|
return self.cost * self.quantity
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['order']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return f"item {self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class EventAuthorisation(models.Model, RevisionMixin):
|
||||||
|
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
|
||||||
|
email = models.EmailField()
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID")
|
||||||
|
account_code = models.CharField(max_length=50, default='', blank=True)
|
||||||
|
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
|
||||||
|
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('event_detail', kwargs={'pk': self.event_id})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
|
||||||
|
|
||||||
|
|
||||||
|
class SubhireManager(BaseEventManager):
|
||||||
|
def current_events(self):
|
||||||
|
events = self.exclude(status=BaseEvent.CANCELLED).filter(
|
||||||
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True)) | # Starts after with no end
|
||||||
|
(models.Q(end_date__gte=timezone.now().date())) # Ends after
|
||||||
|
).order_by('start_date', 'end_date', 'start_time', 'end_time').select_related('person', 'organisation')
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
def event_count(self):
|
||||||
|
event_count = self.exclude(status=BaseEvent.CANCELLED).filter(
|
||||||
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True)) | # Starts after with no end
|
||||||
|
(models.Q(end_date__gte=timezone.now()))
|
||||||
|
).count()
|
||||||
|
return event_count
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class Subhire(BaseEvent):
|
||||||
|
insurance_value = models.DecimalField(max_digits=10, decimal_places=2) # TODO Validate if this is over notifiable threshold
|
||||||
|
events = models.ManyToManyField(Event)
|
||||||
|
quote = models.URLField(default='', validators=[validate_url])
|
||||||
|
|
||||||
|
objects = SubhireManager()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_rig(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dry_hire(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_id(self):
|
||||||
|
return f"S{self.pk:05d}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self):
|
||||||
|
return "purple"
|
||||||
|
|
||||||
|
def get_edit_url(self):
|
||||||
|
return reverse('subhire_update', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('subhire_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def earliest_time(self):
|
||||||
|
return find_earliest_event_time(self, [])
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = [
|
||||||
|
('subhire_finance', 'Can see financial data for subhire - insurance values')
|
||||||
|
]
|
||||||
170
RIGS/models/finance.py
Normal file
170
RIGS/models/finance.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from reversion import revisions as reversion
|
||||||
|
from versioning.versioning import RevisionMixin
|
||||||
|
from .utils import filter_by_pk
|
||||||
|
|
||||||
|
|
||||||
|
class VatManager(models.Manager):
|
||||||
|
def current_rate(self):
|
||||||
|
return self.find_rate(timezone.now())
|
||||||
|
|
||||||
|
def find_rate(self, date):
|
||||||
|
try:
|
||||||
|
return self.filter(start_at__lte=date).latest()
|
||||||
|
except VatRate.DoesNotExist:
|
||||||
|
r = VatRate
|
||||||
|
r.rate = 0
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class VatRate(models.Model, RevisionMixin):
|
||||||
|
start_at = models.DateField()
|
||||||
|
rate = models.DecimalField(max_digits=6, decimal_places=6)
|
||||||
|
comment = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
objects = VatManager()
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def as_percent(self):
|
||||||
|
return self.rate * 100
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-start_at']
|
||||||
|
get_latest_by = 'start_at'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.comment} {self.start_at} @ {self.as_percent}%"
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceManager(models.Manager):
|
||||||
|
def outstanding_invoices(self):
|
||||||
|
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||||
|
sql = "SELECT * FROM " \
|
||||||
|
"(SELECT " \
|
||||||
|
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
||||||
|
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
||||||
|
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
||||||
|
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
||||||
|
"AS sub " \
|
||||||
|
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
||||||
|
"ORDER BY invoice_date"
|
||||||
|
|
||||||
|
query = self.raw(sql)
|
||||||
|
return query
|
||||||
|
|
||||||
|
def search(self, query=None):
|
||||||
|
qs = self.get_queryset()
|
||||||
|
if query is not None:
|
||||||
|
or_lookup = Q(event__name__icontains=query)
|
||||||
|
|
||||||
|
or_lookup = filter_by_pk(or_lookup, query)
|
||||||
|
|
||||||
|
# try and parse an int
|
||||||
|
try:
|
||||||
|
val = int(query)
|
||||||
|
or_lookup = or_lookup | Q(event__pk=val)
|
||||||
|
except: # noqa
|
||||||
|
# not an integer
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if query[0] == "N":
|
||||||
|
val = int(query[1:])
|
||||||
|
or_lookup = Q(event__pk=val) # If string is Nxxxxx then filter by event number
|
||||||
|
elif query[0] == "#":
|
||||||
|
val = int(query[1:])
|
||||||
|
or_lookup = Q(pk=val) # If string is #xxxxx then filter by invoice number
|
||||||
|
except: # noqa
|
||||||
|
pass
|
||||||
|
|
||||||
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register(follow=['payment_set'])
|
||||||
|
class Invoice(models.Model, RevisionMixin):
|
||||||
|
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
||||||
|
invoice_date = models.DateField(auto_now_add=True)
|
||||||
|
void = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
reversion_perm = 'RIGS.view_invoice'
|
||||||
|
|
||||||
|
objects = InvoiceManager()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sum_total(self):
|
||||||
|
return self.event.sum_total
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total(self):
|
||||||
|
return self.event.total
|
||||||
|
|
||||||
|
@property
|
||||||
|
def payment_total(self):
|
||||||
|
total = self.payment_set.aggregate(total=models.Sum('amount'))['total']
|
||||||
|
if total:
|
||||||
|
return total
|
||||||
|
return Decimal("0.00")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def balance(self):
|
||||||
|
return self.sum_total - self.payment_total
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
return self.balance == 0 or self.void
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('invoice_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return f"{self.display_id} for Event {self.event.display_id}"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.display_id}: {self.event} (£{self.balance:.2f})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_id(self):
|
||||||
|
return f"#{self.pk:05d}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-invoice_date']
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class Payment(models.Model, RevisionMixin):
|
||||||
|
CASH = 'C'
|
||||||
|
INTERNAL = 'I'
|
||||||
|
EXTERNAL = 'E'
|
||||||
|
SUCORE = 'SU'
|
||||||
|
ADJUSTMENT = 'T'
|
||||||
|
METHODS = (
|
||||||
|
(CASH, 'Cash'),
|
||||||
|
(INTERNAL, 'Internal'),
|
||||||
|
(EXTERNAL, 'External'),
|
||||||
|
(SUCORE, 'SU Core'),
|
||||||
|
(ADJUSTMENT, 'TEC Adjustment'),
|
||||||
|
)
|
||||||
|
|
||||||
|
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
|
||||||
|
date = models.DateField()
|
||||||
|
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
|
||||||
|
method = models.CharField(max_length=2, choices=METHODS, default='', blank=True)
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.get_method_display()}: {self.amount}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return f"payment of £{self.amount}"
|
||||||
243
RIGS/models/hs.py
Normal file
243
RIGS/models/hs.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from reversion import revisions as reversion
|
||||||
|
from versioning.versioning import RevisionMixin
|
||||||
|
|
||||||
|
from RIGS.validators import validate_url
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class RiskAssessment(models.Model, RevisionMixin):
|
||||||
|
SMALL = (0, 'Small')
|
||||||
|
MEDIUM = (1, 'Medium')
|
||||||
|
LARGE = (2, 'Large')
|
||||||
|
SIZES = (SMALL, MEDIUM, LARGE)
|
||||||
|
|
||||||
|
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
||||||
|
# General
|
||||||
|
nonstandard_equipment = models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by <a href='https://nottinghamtec.sharepoint.com/:f:/g/HealthAndSafety/Eo4xED_DrqFFsfYIjKzMZIIB6Gm_ZfR-a8l84RnzxtBjrA?e=Bf0Haw'>"
|
||||||
|
"TEC's standard risk assessments and method statements?</a>")
|
||||||
|
nonstandard_use = models.BooleanField(help_text="Are TEC using their equipment in a way that is abnormal?<br><small>i.e. Not covered by TECs standard health and safety documentation</small>")
|
||||||
|
contractors = models.BooleanField(help_text="Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>")
|
||||||
|
other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>")
|
||||||
|
crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?")
|
||||||
|
general_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||||
|
|
||||||
|
# Power
|
||||||
|
big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
|
||||||
|
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
|
||||||
|
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)")
|
||||||
|
outside = models.BooleanField(help_text="Is the event outdoors?")
|
||||||
|
generators = models.BooleanField(help_text="Will generators be used?")
|
||||||
|
other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?")
|
||||||
|
nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?")
|
||||||
|
multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?")
|
||||||
|
power_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||||
|
power_plan = models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
||||||
|
|
||||||
|
# Sound
|
||||||
|
noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?")
|
||||||
|
sound_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||||
|
|
||||||
|
# Site
|
||||||
|
known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
|
||||||
|
safe_loading = models.BooleanField(help_text="Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)")
|
||||||
|
safe_storage = models.BooleanField(help_text="Are there any problems with safe and secure equipment storage?")
|
||||||
|
area_outside_of_control = models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")
|
||||||
|
barrier_required = models.BooleanField(help_text="Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?")
|
||||||
|
nonstandard_emergency_procedure = models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")
|
||||||
|
|
||||||
|
# Structures
|
||||||
|
special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?")
|
||||||
|
suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")
|
||||||
|
persons_responsible_structures = models.TextField(blank=True, default='', help_text="Who are the persons on site responsible for their use?")
|
||||||
|
rigging_plan = models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
||||||
|
|
||||||
|
# Blimey that was a lot of options
|
||||||
|
|
||||||
|
reviewed_at = models.DateTimeField(null=True)
|
||||||
|
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||||
|
verbose_name="Reviewer", on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
supervisor_consulted = models.BooleanField(null=True)
|
||||||
|
|
||||||
|
expected_values = {
|
||||||
|
'nonstandard_equipment': False,
|
||||||
|
'nonstandard_use': False,
|
||||||
|
'contractors': False,
|
||||||
|
'other_companies': False,
|
||||||
|
'crew_fatigue': False,
|
||||||
|
# 'big_power': False Doesn't require checking with a super either way
|
||||||
|
'generators': False,
|
||||||
|
'other_companies_power': False,
|
||||||
|
'nonstandard_equipment_power': False,
|
||||||
|
'multiple_electrical_environments': False,
|
||||||
|
'noise_monitoring': False,
|
||||||
|
'known_venue': False,
|
||||||
|
'safe_loading': False,
|
||||||
|
'safe_storage': False,
|
||||||
|
'area_outside_of_control': False,
|
||||||
|
'barrier_required': False,
|
||||||
|
'nonstandard_emergency_procedure': False,
|
||||||
|
'special_structures': False,
|
||||||
|
'suspended_structures': False,
|
||||||
|
}
|
||||||
|
inverted_fields = {key: value for (key, value) in expected_values.items() if not value}.keys()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
# Check for idiots
|
||||||
|
if not self.outside and self.generators:
|
||||||
|
raise forms.ValidationError("Engage brain, please. <strong>No generators indoors!(!)</strong>")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['event']
|
||||||
|
permissions = [
|
||||||
|
('review_riskassessment', 'Can review Risk Assessments')
|
||||||
|
]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def fieldz(self):
|
||||||
|
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def event_size(self):
|
||||||
|
# Confirm event size. Check all except generators, since generators entails outside
|
||||||
|
if self.outside or self.other_companies_power or self.nonstandard_equipment_power or self.multiple_electrical_environments:
|
||||||
|
return self.LARGE[0]
|
||||||
|
elif self.big_power:
|
||||||
|
return self.MEDIUM[0]
|
||||||
|
else:
|
||||||
|
return self.SMALL[0]
|
||||||
|
|
||||||
|
def get_event_size_display(self):
|
||||||
|
return self.SIZES[self.event_size][1] + " Event"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return str(self.event)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('ra_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.pk} | {self.event}"
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register(follow=['vehicles', 'crew'])
|
||||||
|
class EventChecklist(models.Model, RevisionMixin):
|
||||||
|
event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
# General
|
||||||
|
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists',
|
||||||
|
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?")
|
||||||
|
venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
|
||||||
|
date = models.DateField()
|
||||||
|
|
||||||
|
# Safety Checks
|
||||||
|
safe_parking = models.BooleanField(blank=True, null=True, help_text="Vehicles parked safely?<br><small>(does not obstruct venue access)</small>")
|
||||||
|
safe_packing = models.BooleanField(blank=True, null=True, help_text="Equipment packed away safely?<br><small>(including flightcases)</small>")
|
||||||
|
exits = models.BooleanField(blank=True, null=True, help_text="Emergency exits clear?")
|
||||||
|
trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?")
|
||||||
|
warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>")
|
||||||
|
ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?")
|
||||||
|
hs_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of Safety Bag/Box")
|
||||||
|
extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers")
|
||||||
|
|
||||||
|
# Small Electrical Checks
|
||||||
|
rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
|
||||||
|
supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested?<br><small>(using socket tester)</small>")
|
||||||
|
|
||||||
|
# Shared electrical checks
|
||||||
|
earthing = models.BooleanField(blank=True, null=True, help_text="Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>")
|
||||||
|
pat = models.BooleanField(blank=True, null=True, help_text="All equipment in PAT period?")
|
||||||
|
|
||||||
|
# Medium Electrical Checks
|
||||||
|
source_rcd = models.BooleanField(blank=True, null=True, help_text="Source RCD protected?<br><small>(if cable is more than 3m long) </small>")
|
||||||
|
labelling = models.BooleanField(blank=True, null=True, help_text="Appropriate and clear labelling on distribution and cabling?")
|
||||||
|
# First Distro
|
||||||
|
fd_voltage_l1 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L1-N", help_text="L1 - N")
|
||||||
|
fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N")
|
||||||
|
fd_voltage_l3 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N")
|
||||||
|
fd_phase_rotation = models.BooleanField(blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation<br><small>(if required)</small>")
|
||||||
|
fd_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||||
|
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
|
||||||
|
# Worst case points
|
||||||
|
w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||||
|
w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||||
|
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||||
|
w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||||
|
w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||||
|
w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||||
|
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||||
|
w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||||
|
w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||||
|
w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||||
|
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||||
|
w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||||
|
|
||||||
|
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</small>")
|
||||||
|
public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
|
||||||
|
|
||||||
|
reviewed_at = models.DateTimeField(null=True)
|
||||||
|
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||||
|
verbose_name="Reviewer", on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
inverted_fields = []
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['event']
|
||||||
|
permissions = [
|
||||||
|
('review_eventchecklist', 'Can review Event Checklists')
|
||||||
|
]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def fieldz(self):
|
||||||
|
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return str(self.event)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('ec_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.pk} | {self.event}"
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class EventChecklistVehicle(models.Model, RevisionMixin):
|
||||||
|
checklist = models.ForeignKey('EventChecklist', related_name='vehicles', blank=True, on_delete=models.CASCADE)
|
||||||
|
vehicle = models.CharField(max_length=255)
|
||||||
|
driver = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='vehicles', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.vehicle} driven by {self.driver}"
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class EventChecklistCrew(models.Model, RevisionMixin):
|
||||||
|
checklist = models.ForeignKey('EventChecklist', related_name='crew', blank=True, on_delete=models.CASCADE)
|
||||||
|
crewmember = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='crewed', on_delete=models.CASCADE)
|
||||||
|
role = models.CharField(max_length=255)
|
||||||
|
start = models.DateTimeField()
|
||||||
|
end = models.DateTimeField()
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.start > self.end:
|
||||||
|
raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.crewmember} ({self.role})"
|
||||||
173
RIGS/models/models.py
Normal file
173
RIGS/models/models.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from versioning.versioning import RevisionMixin
|
||||||
|
from .events import Event
|
||||||
|
from .utils import filter_by_pk
|
||||||
|
|
||||||
|
|
||||||
|
class Profile(AbstractUser):
|
||||||
|
initials = models.CharField(max_length=5, null=True, blank=False)
|
||||||
|
phone = models.CharField(max_length=13, blank=True, default='')
|
||||||
|
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
||||||
|
is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.")
|
||||||
|
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
||||||
|
last_emailed = models.DateTimeField(blank=True, null=True)
|
||||||
|
dark_theme = models.BooleanField(default=False)
|
||||||
|
is_supervisor = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def make_api_key(cls):
|
||||||
|
size = 20
|
||||||
|
chars = string.ascii_letters + string.digits
|
||||||
|
new_api_key = ''.join(random.choice(chars) for x in range(size))
|
||||||
|
return new_api_key
|
||||||
|
|
||||||
|
@property
|
||||||
|
def profile_picture(self):
|
||||||
|
url = ""
|
||||||
|
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
|
||||||
|
url = "https://www.gravatar.com/avatar/" + hashlib.md5(
|
||||||
|
self.email.encode('utf-8')).hexdigest() + "?d=wavatar&s=500"
|
||||||
|
return url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
name = self.get_full_name()
|
||||||
|
if self.initials:
|
||||||
|
name += f' "{self.initials}"'
|
||||||
|
return name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_events(self):
|
||||||
|
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def admins(cls):
|
||||||
|
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def users_awaiting_approval_count(cls):
|
||||||
|
return Profile.objects.filter(models.Q(is_approved=False)).count()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class ContactableManager(models.Manager):
|
||||||
|
def search(self, query=None):
|
||||||
|
qs = self.get_queryset()
|
||||||
|
if query is not None:
|
||||||
|
or_lookup = Q(name__icontains=query) | Q(email__icontains=query) | Q(address__icontains=query) | Q(notes__icontains=query) | Q(
|
||||||
|
phone__startswith=query) | Q(phone__endswith=query)
|
||||||
|
|
||||||
|
or_lookup = filter_by_pk(or_lookup, query)
|
||||||
|
|
||||||
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class Person(models.Model, RevisionMixin):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
|
email = models.EmailField(blank=True, default='')
|
||||||
|
address = models.TextField(blank=True, default='')
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
|
objects = ContactableManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
string = self.name
|
||||||
|
if self.notes is not None:
|
||||||
|
if len(self.notes) > 0:
|
||||||
|
string += "*"
|
||||||
|
return string
|
||||||
|
|
||||||
|
@property
|
||||||
|
def organisations(self):
|
||||||
|
o = []
|
||||||
|
for e in Event.objects.filter(person=self).select_related('organisation'):
|
||||||
|
if e.organisation:
|
||||||
|
o.append(e.organisation)
|
||||||
|
|
||||||
|
# Count up occurances and put them in descending order
|
||||||
|
c = Counter(o)
|
||||||
|
stats = c.most_common()
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_events(self):
|
||||||
|
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('person_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class Organisation(models.Model, RevisionMixin):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
|
email = models.EmailField(blank=True, default='')
|
||||||
|
address = models.TextField(blank=True, default='')
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
union_account = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
objects = ContactableManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
string = self.name
|
||||||
|
if self.notes is not None:
|
||||||
|
if len(self.notes) > 0:
|
||||||
|
string += "*"
|
||||||
|
return string
|
||||||
|
|
||||||
|
@property
|
||||||
|
def persons(self):
|
||||||
|
p = []
|
||||||
|
for e in Event.objects.filter(organisation=self).select_related('person'):
|
||||||
|
if e.person:
|
||||||
|
p.append(e.person)
|
||||||
|
|
||||||
|
# Count up occurances and put them in descending order
|
||||||
|
c = Counter(p)
|
||||||
|
stats = c.most_common()
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_events(self):
|
||||||
|
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('organisation_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class Venue(models.Model, RevisionMixin):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
|
email = models.EmailField(blank=True, default='')
|
||||||
|
three_phase_available = models.BooleanField(default=False)
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
address = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
|
objects = ContactableManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
string = self.name
|
||||||
|
if self.notes and len(self.notes) > 0:
|
||||||
|
string += "*"
|
||||||
|
return string
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_events(self):
|
||||||
|
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('venue_detail', kwargs={'pk': self.pk})
|
||||||
9
RIGS/models/utils.py
Normal file
9
RIGS/models/utils.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
def filter_by_pk(filt, query):
|
||||||
|
# try and parse an int
|
||||||
|
try:
|
||||||
|
val = int(query)
|
||||||
|
filt = filt | Q(pk=val)
|
||||||
|
except: # noqa
|
||||||
|
# not an integer
|
||||||
|
pass
|
||||||
|
return filt
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 63 KiB |
@@ -11,6 +11,7 @@
|
|||||||
<initialize>
|
<initialize>
|
||||||
<color id="LightGray" RGB="#D3D3D3"/>
|
<color id="LightGray" RGB="#D3D3D3"/>
|
||||||
<color id="DarkGray" RGB="#707070"/>
|
<color id="DarkGray" RGB="#707070"/>
|
||||||
|
<color id="Brand" RGB="#3853a4"/>
|
||||||
</initialize>
|
</initialize>
|
||||||
|
|
||||||
<paraStyle name="style.para" fontName="OpenSans" />
|
<paraStyle name="style.para" fontName="OpenSans" />
|
||||||
@@ -27,6 +28,8 @@
|
|||||||
<paraStyle name="style.times" fontName="OpenSans" fontSize="10" />
|
<paraStyle name="style.times" fontName="OpenSans" fontSize="10" />
|
||||||
<paraStyle name="style.head_titles" fontName="OpenSans-Bold" fontSize="10" />
|
<paraStyle name="style.head_titles" fontName="OpenSans-Bold" fontSize="10" />
|
||||||
<paraStyle name="style.head_numbers" fontName="OpenSans" fontSize="10" />
|
<paraStyle name="style.head_numbers" fontName="OpenSans" fontSize="10" />
|
||||||
|
<paraStyle name="style.emheader" fontName="OpenSans" textColor="White" fontSize="12" backColor="Brand" leading="20" borderPadding="4"/>
|
||||||
|
<paraStyle name="style.breakbefore" parent="emheader" pageBreakBefore="1"/>
|
||||||
|
|
||||||
<blockTableStyle id="eventSpecifics">
|
<blockTableStyle id="eventSpecifics">
|
||||||
<blockValign value="top"/>
|
<blockValign value="top"/>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
{% 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>
|
||||||
|
|||||||
@@ -1,197 +1,131 @@
|
|||||||
{% extends 'base_rigs.html' %}
|
{% extends 'base_rigs.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Calendar{% endblock %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
<link href="{% static 'css/main.css' %}" rel='stylesheet' />
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{% static 'js/moment.js' %}"></script>
|
<script src="{% static 'js/moment.js' %}"></script>
|
||||||
<script src="{% static 'js/main.js' %}"></script>
|
<script>
|
||||||
<script>
|
$(document).ready(function() {
|
||||||
viewToUrl = {
|
|
||||||
'timeGridWeek':'week',
|
|
||||||
'timeGridDay':'day',
|
|
||||||
'dayGridMonth':'month'
|
|
||||||
}
|
|
||||||
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');
|
|
||||||
|
|
||||||
calendar = new FullCalendar.Calendar(calendarEl, {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
thisEvent = {
|
|
||||||
'start': $(this).attr('earliest'),
|
|
||||||
'end': end,
|
|
||||||
'className': 'modal-href',
|
|
||||||
'title': $(this).attr('title'),
|
|
||||||
'url': $(this).attr('url'),
|
|
||||||
'allDay': allDay
|
|
||||||
}
|
|
||||||
|
|
||||||
if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){
|
|
||||||
thisEvent['color'] = colours[$(this).attr('status')];
|
|
||||||
}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);
|
|
||||||
|
|
||||||
// Enable/Disable "Today" button as required
|
|
||||||
let $today = $('#today-button');
|
|
||||||
if(moment().isBetween(view.currentStart, view.currentEnd)){
|
|
||||||
//Today is within the current view
|
|
||||||
$today.prop('disabled', true);
|
|
||||||
}else{
|
|
||||||
$today.prop('disabled', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set active view select button
|
|
||||||
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;
|
|
||||||
|
|
||||||
case 'timeGridWeek':
|
|
||||||
$month.removeClass('active');
|
|
||||||
$week.addClass('active');
|
|
||||||
$day.removeClass('active');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'timeGridDay':
|
|
||||||
$month.removeClass('active');
|
|
||||||
$week.removeClass('active');
|
|
||||||
$day.addClass('active');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
history.replaceState(null,null,"{% url 'web_calendar' %}"+viewToUrl[view.type]+'/'+moment(view.currentStart).format('YYYY-MM-DD')+'/');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
calendar.render();
|
|
||||||
});
|
|
||||||
$(document).ready(function() {
|
|
||||||
// set some button listeners
|
// set some button listeners
|
||||||
$('#next-button').click(function(){ calendar.next(); });
|
|
||||||
$('#prev-button').click(function(){ calendar.prev(); });
|
|
||||||
$('#today-button').click(function(){ calendar.today(); });
|
$('#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(){
|
$('#go-to-date-input').change(function(){
|
||||||
if(moment($('#go-to-date-input').val()).isValid()){
|
if(moment($('#go-to-date-input').val()).isValid()){
|
||||||
$('#go-to-date-button').prop('disabled', false);
|
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{
|
} else{
|
||||||
$('#go-to-date-button').prop('disabled', true);
|
document.getElementById('go-to-date-button').classList.add('disabled');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$('#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>
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<style>
|
||||||
|
.week {
|
||||||
|
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 {
|
||||||
|
display:contents;
|
||||||
|
}
|
||||||
|
.day-label {
|
||||||
|
grid-row-start: 1;
|
||||||
|
text-align: right;
|
||||||
|
margin:0;
|
||||||
|
font-size: 1em !important;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-day, .day-label, .event {
|
||||||
|
padding: 4px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
background-color: #CCC;
|
||||||
|
font-size: 0.8em !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-end {
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-start {
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-day {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.event {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-span="1"] { grid-column-end: span 1; }
|
||||||
|
[data-span="2"] { grid-column-end: span 2; }
|
||||||
|
[data-span="3"] { grid-column-end: span 3; }
|
||||||
|
[data-span="4"] { grid-column-end: span 4; }
|
||||||
|
[data-span="5"] { grid-column-end: span 5; }
|
||||||
|
[data-span="6"] { grid-column-end: span 6; }
|
||||||
|
[data-span="7"] { grid-column-end: span 7; }
|
||||||
|
|
||||||
|
.day > a {
|
||||||
|
color: inherit !important;
|
||||||
|
text-decoration: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row justify-content-center mb-1">
|
||||||
<div class="col-sm-12">
|
<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="pull-left">
|
<div class="form-inline col-4">
|
||||||
<span id="calendar-header" class="h2"></span>
|
<div class="input-group">
|
||||||
</div>
|
<input type="date" id="go-to-date-input" placeholder="Go to date..." class="form-control">
|
||||||
<div class="form-inline float-right btn-page my-3">
|
<span class="input-group-append">
|
||||||
<div class="input-group mx-2">
|
<a class="btn btn-success" id="go-to-date-button">Go!</a>
|
||||||
<input type="date" class="form-control" id="go-to-date-input" placeholder="Go to date...">
|
</span>
|
||||||
<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;"> <span></div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
29
RIGS/templates/dashboards/productions.html
Normal file
29
RIGS/templates/dashboards/productions.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -15,36 +15,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
{% include 'partials/archive_form.html' %}
|
||||||
<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 %}
|
||||||
|
|||||||
@@ -25,12 +25,10 @@
|
|||||||
{% include 'partials/hs_details.html' %}
|
{% include 'partials/hs_details.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if event.is_rig %}
|
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
||||||
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
|
<div class="col-md-8 py-3">
|
||||||
<div class="col-md-8 py-3">
|
{% include 'partials/auth_details.html' %}
|
||||||
{% include 'partials/auth_details.html' %}
|
</div>
|
||||||
</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">
|
||||||
@@ -47,9 +45,15 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<p class="dont-break-out">{{ event.notes|markdown }}</p>
|
<p class="dont-break-out">{{ event.notes|markdown }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br>
|
<h4>Event Items</h4>
|
||||||
{% 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 %}
|
||||||
|
|||||||
@@ -106,6 +106,10 @@
|
|||||||
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>
|
||||||
|
|||||||
32
RIGS/templates/partials/archive_form.html
Normal file
32
RIGS/templates/partials/archive_form.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% load get_list from filters %}
|
||||||
|
{% load button from filters %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 py-2">
|
||||||
|
<form class="form-inline" method="GET">
|
||||||
|
<div class="input-group mx-2">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">Start</span>
|
||||||
|
</div>
|
||||||
|
<input type="date" name="start" id="start" value="{{ start|default_if_none:'' }}" placeholder="Start" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="input-group mx-2">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">End</span>
|
||||||
|
</div>
|
||||||
|
<input type="date" name="end" id="end" value="{{ end|default_if_none:'' }}" placeholder="End" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="input-group mx-2">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">Keyword</span>
|
||||||
|
</div>
|
||||||
|
<input type="search" name="q" placeholder="Keyword" value="{{ request.GET.q }}" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<select class="selectpicker pr-3" multiple data-actions-box="true" data-none-selected-text="Status" data-actions-box="true" id="status" name="status">
|
||||||
|
{% for status in statuses %}
|
||||||
|
<option value="{{status.0}}" {% if status.0|safe in request.GET|get_list:'status' %}selected=""{% endif %}>{{status.1}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% button 'search' %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -47,7 +47,5 @@
|
|||||||
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>
|
||||||
|
|||||||
@@ -12,21 +12,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for event in events %}
|
{% for event in events %}
|
||||||
<tr class="{% if event.cancelled %}
|
<tr class="table-{{event.color}}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
||||||
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-->
|
||||||
@@ -56,7 +42,7 @@
|
|||||||
<!---Details-->
|
<!---Details-->
|
||||||
<td id="event_details" class="w-100">
|
<td id="event_details" class="w-100">
|
||||||
<h4>
|
<h4>
|
||||||
<a href="{% url 'event_detail' event.pk %}">
|
<a href="{{event.get_absolute_url}}">
|
||||||
{{ event.name }}
|
{{ event.name }}
|
||||||
</a>
|
</a>
|
||||||
{% if event.venue %}
|
{% if event.venue %}
|
||||||
|
|||||||
@@ -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">
|
||||||
<i class="fas fa-plus"></i> Add Item
|
<span class="fas fa-plus"></span> Add Item
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
84
RIGS/templates/partials/subhire_table.html
Normal file
84
RIGS/templates/partials/subhire_table.html
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
{% load linked_name from filters %}
|
||||||
|
{% load markdown_tags %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table mb-0" id="event_table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">#</th>
|
||||||
|
<th scope="col">Dates & Times</th>
|
||||||
|
<th scope="col">Hire Details</th>
|
||||||
|
<th scope="col">Associated Event(s)</th>
|
||||||
|
{% if perms.RIGS.subhire_finance %}
|
||||||
|
<th scope="col">Insurance Value</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for event in events %}
|
||||||
|
<tr {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
|
||||||
|
<!---Number-->
|
||||||
|
<th scope="row" id="event_number">{{ event.display_id }}</th>
|
||||||
|
<!--Dates & Times-->
|
||||||
|
<td id="event_dates" style="text-align: justify;">
|
||||||
|
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}
|
||||||
|
{% if event.has_start_time %}
|
||||||
|
{{ event.start_time|date:"H:i" }}
|
||||||
|
{% endif %}</strong>
|
||||||
|
</span>
|
||||||
|
{% if event.end_date %}
|
||||||
|
<br>
|
||||||
|
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}{% endif %}
|
||||||
|
{% if event.has_end_time %}
|
||||||
|
{{ event.end_time|date:"H:i" }}
|
||||||
|
{% endif %}</strong>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<!---Details-->
|
||||||
|
<td id="event_details" class="w-100">
|
||||||
|
<h4>
|
||||||
|
<a href="{{event.get_absolute_url}}">
|
||||||
|
{{ event.name }}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<h5>
|
||||||
|
Primary Contact: {{ event.person|linked_name }}
|
||||||
|
{% if event.organisation %}
|
||||||
|
({{ event.organisation|linked_name }})
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
{% if not event.cancelled and event.description %}
|
||||||
|
<p>{{ event.description|markdown }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% include 'partials/event_status.html' %}
|
||||||
|
</td>
|
||||||
|
<td class="p-0 text-nowrap">
|
||||||
|
<ul class="list-group">
|
||||||
|
{% for event in event.events.all %}
|
||||||
|
<li class="list-group-item"><a href="{{event.get_absolute_url}}">{{ event }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
{% if perms.RIGS.subhire_finance %}
|
||||||
|
<td id="insurance_value" class="text-nowrap">
|
||||||
|
£{{ event.insurance_value }}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr class="bg-warning">
|
||||||
|
<td colspan="4">No events found</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td>Total Value:</td>
|
||||||
|
<td>£{{ total_value }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
76
RIGS/templates/subhire_detail.html
Normal file
76
RIGS/templates/subhire_detail.html
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{% 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 %}
|
||||||
202
RIGS/templates/subhire_form.html
Normal file
202
RIGS/templates/subhire_form.html
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
{% 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 %}
|
||||||
27
RIGS/templates/subhire_list.html
Normal file
27
RIGS/templates/subhire_list.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{% extends 'base_rigs.html' %}
|
||||||
|
{% load paginator from filters %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block preload_js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="{% static 'js/selects.js' %}" async></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'partials/archive_form.html' %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
{% with object_list as events %}
|
||||||
|
{% include 'partials/subhire_table.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% paginator %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -175,6 +175,9 @@ def namewithnotes(obj, url, autoescape=True):
|
|||||||
else:
|
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):
|
||||||
@@ -196,8 +199,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
|
|||||||
text = "Edit"
|
text = "Edit"
|
||||||
elif type == 'print':
|
elif type == 'print':
|
||||||
clazz += " btn-primary "
|
clazz += " btn-primary "
|
||||||
icon = "fa-print"
|
icon = "fa-download"
|
||||||
text = "Print"
|
text = "Export"
|
||||||
elif type == 'duplicate':
|
elif type == 'duplicate':
|
||||||
clazz += " btn-info "
|
clazz += " btn-info "
|
||||||
icon = "fa-copy"
|
icon = "fa-copy"
|
||||||
|
|||||||
22
RIGS/urls.py
22
RIGS/urls.py
@@ -43,12 +43,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Rigboard
|
# Rigboard
|
||||||
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
|
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
|
||||||
path('rigboard/calendar/', login_required()(views.WebCalendar.as_view()),
|
re_path(r'^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')),
|
||||||
|
|
||||||
|
|
||||||
@@ -70,6 +66,22 @@ 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'),
|
||||||
|
|
||||||
|
|||||||
56
RIGS/utils.py
Normal file
56
RIGS/utils.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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
|
||||||
10
RIGS/validators.py
Normal file
10
RIGS/validators.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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')
|
||||||
@@ -3,3 +3,5 @@ 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 *
|
||||||
14
RIGS/views/dashboards.py
Normal file
14
RIGS/views/dashboards.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pytz
|
from django.utils import timezone
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
@@ -31,6 +33,7 @@ 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'
|
||||||
@@ -40,42 +43,46 @@ 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 '') + '\n'
|
'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire, ' if params['dry-hire'] else '') + ('Subhires' if params['subhire'] else '') + '\n'
|
||||||
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
|
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 = datetime.datetime.now() - datetime.timedelta(days=365)
|
start = timezone.now() - datetime.timedelta(days=365)
|
||||||
filter = Q(start_date__gte=start)
|
filter = Q(start_date__gte=start)
|
||||||
|
|
||||||
typeFilters = Q(pk=None) # Need something that is false for every entry
|
type_filters = Q(pk=None) # Need something that is false for every entry
|
||||||
|
|
||||||
if params['dry-hire']:
|
if params['dry-hire']:
|
||||||
typeFilters = typeFilters | Q(dry_hire=True, is_rig=True)
|
type_filters = type_filters | Q(dry_hire=True, is_rig=True)
|
||||||
|
|
||||||
if params['non-rig']:
|
if params['non-rig']:
|
||||||
typeFilters = typeFilters | Q(is_rig=False)
|
type_filters = type_filters | Q(is_rig=False)
|
||||||
|
|
||||||
if params['rig']:
|
if params['rig']:
|
||||||
typeFilters = typeFilters | Q(is_rig=True, dry_hire=False)
|
type_filters = type_filters | Q(is_rig=True, dry_hire=False)
|
||||||
|
|
||||||
statusFilters = Q(pk=None) # Need something that is false for every entry
|
status_filters = Q(pk=None) # Need something that is false for every entry
|
||||||
|
|
||||||
if params['cancelled']:
|
if params['cancelled']:
|
||||||
statusFilters = statusFilters | Q(status=models.Event.CANCELLED)
|
status_filters = status_filters | Q(status=models.Event.CANCELLED)
|
||||||
if params['provisional']:
|
if params['provisional']:
|
||||||
statusFilters = statusFilters | Q(status=models.Event.PROVISIONAL)
|
status_filters = status_filters | Q(status=models.Event.PROVISIONAL)
|
||||||
if params['confirmed']:
|
if params['confirmed']:
|
||||||
statusFilters = statusFilters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
|
status_filters = status_filters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
|
||||||
|
|
||||||
filter = filter & typeFilters & statusFilters
|
filter = filter & type_filters & status_filters
|
||||||
|
|
||||||
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
events = models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
||||||
'venue', 'mic')
|
'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 = ''
|
||||||
|
|
||||||
@@ -106,30 +113,32 @@ class CalendarICS(ICalFeed):
|
|||||||
return item.latest_time
|
return item.latest_time
|
||||||
|
|
||||||
def item_location(self, item):
|
def item_location(self, item):
|
||||||
return item.venue
|
if hasattr(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'
|
||||||
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
|
if hasattr(item, 'venue'):
|
||||||
|
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
|
||||||
if item.is_rig and item.person:
|
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'
|
||||||
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
if hasattr(item, 'mic'):
|
||||||
|
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
||||||
|
|
||||||
desc += '\n'
|
desc += '\n'
|
||||||
if item.meet_at:
|
if hasattr(item, 'meet_at') and item.meet_at:
|
||||||
desc += 'Crew Meet = ' + (
|
desc += 'Crew Meet = ' + (
|
||||||
item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
timezone.make_aware(item.meet_at).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
||||||
if item.access_at:
|
if hasattr(item, 'access_at') and item.access_at:
|
||||||
desc += 'Access At = ' + (
|
desc += 'Access At = ' + (
|
||||||
item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
timezone.make_aware(item.access_at).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
||||||
if item.start_date:
|
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'
|
||||||
@@ -140,8 +149,6 @@ 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()}'
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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
|
||||||
@@ -12,9 +11,7 @@ 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
|
||||||
@@ -22,7 +19,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
|
from RIGS import models, forms, utils
|
||||||
|
|
||||||
__author__ = 'ghost'
|
__author__ = 'ghost'
|
||||||
|
|
||||||
@@ -40,14 +37,25 @@ class RigboardIndex(generic.TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class WebCalendar(generic.TemplateView):
|
class WebCalendar(generic.ListView):
|
||||||
|
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)
|
||||||
context['view'] = kwargs.get('view', '')
|
# use today's date for the calendar
|
||||||
context['date'] = kwargs.get('date', '')
|
d = utils.get_date(self.request.GET.get('month', None))
|
||||||
# context['page_title'] = "Calendar"
|
context['prev_month'] = utils.prev_month(d)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
@@ -61,10 +69,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -200,27 +204,7 @@ 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
|
||||||
|
|
||||||
filter = Q()
|
qs = self.model.objects.event_search(self.request.GET.get('q', None), start, end, self.request.GET.get('status', ""))
|
||||||
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.")
|
||||||
|
|||||||
62
RIGS/views/subhire.py
Normal file
62
RIGS/views/subhire.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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
|
||||||
@@ -105,8 +105,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
for i in range(100):
|
for i in range(100):
|
||||||
prefix = random.choice(asset_prefixes)
|
prefix = random.choice(asset_prefixes)
|
||||||
asset_id = str(get_available_asset_id(wanted_prefix=prefix))
|
asset_id = get_available_asset_id(wanted_prefix=prefix)
|
||||||
asset_id = prefix + asset_id
|
|
||||||
asset = models.Asset(
|
asset = models.Asset(
|
||||||
asset_id=asset_id,
|
asset_id=asset_id,
|
||||||
description=random.choice(asset_description),
|
description=random.choice(asset_description),
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ class AssetManager(models.Manager):
|
|||||||
|
|
||||||
def get_available_asset_id(wanted_prefix=""):
|
def get_available_asset_id(wanted_prefix=""):
|
||||||
last_asset = Asset.objects.filter(asset_id_prefix=wanted_prefix).last()
|
last_asset = Asset.objects.filter(asset_id_prefix=wanted_prefix).last()
|
||||||
return 9000 if last_asset is None else wanted_prefix + str(last_asset.asset_id_number + 1)
|
last_asset_id = last_asset.asset_id_number if last_asset else 0
|
||||||
|
return wanted_prefix + str(last_asset_id + 1)
|
||||||
|
|
||||||
|
|
||||||
def validate_positive(value):
|
def validate_positive(value):
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ class DuplicateMixin:
|
|||||||
class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
|
class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
|
||||||
def get_initial(self, *args, **kwargs):
|
def get_initial(self, *args, **kwargs):
|
||||||
initial = super().get_initial(*args, **kwargs)
|
initial = super().get_initial(*args, **kwargs)
|
||||||
initial["asset_id"] = models.get_available_asset_id(wanted_prefix=self.get_object().asset_id_prefix)
|
initial["asset_id"] = models.get_available_asset_id()
|
||||||
return initial
|
return initial
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ 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'
|
||||||
@@ -59,7 +58,6 @@ 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
11
package-lock.json
generated
@@ -19,7 +19,6 @@
|
|||||||
"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",
|
||||||
@@ -3063,11 +3062,6 @@
|
|||||||
"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",
|
||||||
@@ -11609,11 +11603,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ 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",'')
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
jQuery(document).ready(function () {
|
$(document).ready(function () {
|
||||||
jQuery(document).on('click', '.modal-href', function (e) {
|
$(document).on('click', '.modal-href', function (e) {
|
||||||
$link = jQuery(this);
|
$link = $(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 = "";
|
||||||
jQuery('#modal').load($link.attr('href'), function (e) {
|
$('#modal').load($link.attr('href'), function (e) {
|
||||||
jQuery('#modal').modal();
|
$('#modal').modal();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -23,7 +23,6 @@ jQuery(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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -281,3 +281,7 @@ 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -143,10 +143,14 @@ class Command(BaseCommand):
|
|||||||
"Bin Diving",
|
"Bin Diving",
|
||||||
"Wiki Editing"]
|
"Wiki Editing"]
|
||||||
|
|
||||||
descriptions = ["Physical training concentrates on mechanistic goals: training programs in this area develop specific motor skills, agility, strength or physical fitness, often with an intention of peaking at a particular time.",
|
descriptions = [
|
||||||
"In military use, training means gaining the physical ability to perform and survive in combat, and learn the many skills needed in a time of war. These include how to use a variety of weapons, outdoor survival skills, and how to survive being captured by the enemy, among many others. See military education and training.",
|
"Physical training concentrates on mechanistic goals: training programs in this area develop specific motor skills, agility, strength or physical fitness, often with an intention of peaking at a particular time.",
|
||||||
"For psychological or physiological reasons, people who believe it may be beneficial to them can choose to practice relaxation training, or autogenic training, in an attempt to increase their ability to relax or deal with stress. While some studies have indicated relaxation training is useful for some medical conditions, autogenic training has limited results or has been the result of few studies.",
|
"In military use, training means gaining the physical ability to perform and survive in combat, and learn the many skills needed in a time of war.",
|
||||||
"Some occupations are inherently hazardous, and require a minimum level of competence before the practitioners can perform the work at an acceptable level of safety to themselves or others in the vicinity. Occupational diving, rescue, firefighting and operation of certain types of machinery and vehicles may require assessment and certification of a minimum acceptable competence before the person is allowed to practice as a licensed instructor."]
|
"These include how to use a variety of weapons, outdoor survival skills, and how to survive being captured by the enemy, among many others. See military education and training.",
|
||||||
|
"While some studies have indicated relaxation training is useful for some medical conditions, autogenic training has limited results or has been the result of few studies.",
|
||||||
|
"Some occupations are inherently hazardous, and require a minimum level of competence before the practitioners can perform the work at an acceptable level of safety to themselves or others in the vicinity.",
|
||||||
|
"Occupational diving, rescue, firefighting and operation of certain types of machinery and vehicles may require assessment and certification of a minimum acceptable competence before the person is allowed to practice as a licensed instructor."
|
||||||
|
]
|
||||||
|
|
||||||
for i, name in enumerate(names):
|
for i, name in enumerate(names):
|
||||||
category = random.choice(self.categories)
|
category = random.choice(self.categories)
|
||||||
@@ -155,7 +159,7 @@ class Command(BaseCommand):
|
|||||||
number = previous_item.reference_number + 1
|
number = previous_item.reference_number + 1
|
||||||
else:
|
else:
|
||||||
number = 0
|
number = 0
|
||||||
item = models.TrainingItem.objects.create(category=category, reference_number=number, name=name, description=random.choice(descriptions))
|
item = models.TrainingItem.objects.create(category=category, reference_number=number, name=name, description=random.choice(descriptions) + random.choice(descriptions) + random.choice(descriptions))
|
||||||
self.items.append(item)
|
self.items.append(item)
|
||||||
|
|
||||||
def setup_levels(self):
|
def setup_levels(self):
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
{% extends 'base_training.html' %}
|
{% extends 'base_training.html' %}
|
||||||
|
|
||||||
|
{% load button from filters %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="col-12 text-right py-2 pr-0">
|
||||||
|
{% button 'print' 'item_list_export' %}
|
||||||
|
</div>
|
||||||
<div id="accordion">
|
<div id="accordion">
|
||||||
{% for category in categories %}
|
{% for category in categories %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
25
training/templates/item_list.xml
Normal file
25
training/templates/item_list.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends 'base_print.xml' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 style="page-head">TEC Training Item List</h1>
|
||||||
|
<spacer length="15" />
|
||||||
|
{% for category in categories %}
|
||||||
|
<h2 {% if not forloop.first %}style="breakbefore"{%else%}style="emheader"{%endif%}>{{category}}</h2>
|
||||||
|
<spacer length="10" />
|
||||||
|
{% for item in category.items.all %}
|
||||||
|
<h3>{{ item }}</h3>
|
||||||
|
<spacer length="4" />
|
||||||
|
<para>{{ item.description }}</para>
|
||||||
|
{% if item.prerequisites.exists %}
|
||||||
|
<h4>Prerequisites:</h4>
|
||||||
|
<ul bulletFontSize="5">
|
||||||
|
{% for p in item.prerequisites.all %}
|
||||||
|
<li><para>{{p}}</para></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
<spacer length="8" />
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
<namedString id="lastPage"><pageNumber/></namedString>
|
||||||
|
{% endblock %}
|
||||||
@@ -8,6 +8,7 @@ from versioning.views import VersionHistory
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('items/', login_required(views.ItemList.as_view()), name='item_list'),
|
path('items/', login_required(views.ItemList.as_view()), name='item_list'),
|
||||||
|
path('items/export/', login_required(views.ItemListExport.as_view()), name='item_list_export'),
|
||||||
path('item/<int:pk>/qualified_users/', login_required(views.ItemQualifications.as_view()), name='item_qualification'),
|
path('item/<int:pk>/qualified_users/', login_required(views.ItemQualifications.as_view()), name='item_qualification'),
|
||||||
|
|
||||||
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
|
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.db import transaction
|
|||||||
from django.db.models import Q, Count
|
from django.db.models import Q, Count
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
from PyRIGS.views import is_ajax, ModalURLMixin, get_related
|
from PyRIGS.views import is_ajax, ModalURLMixin, get_related, PrintListView
|
||||||
from training import models, forms
|
from training import models, forms
|
||||||
from users import views
|
from users import views
|
||||||
from reversion.views import RevisionMixin
|
from reversion.views import RevisionMixin
|
||||||
@@ -24,6 +24,17 @@ class ItemList(generic.ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ItemListExport(PrintListView):
|
||||||
|
model = models.TrainingItem
|
||||||
|
template_name = 'item_list.xml'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['filename'] = "TrainingItemList.pdf"
|
||||||
|
context["categories"] = models.TrainingCategory.objects.all()
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class TraineeDetail(views.ProfileDetail):
|
class TraineeDetail(views.ProfileDetail):
|
||||||
template_name = "trainee_detail.html"
|
template_name = "trainee_detail.html"
|
||||||
model = models.Trainee
|
model = models.Trainee
|
||||||
|
|||||||
@@ -118,6 +118,11 @@
|
|||||||
<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">
|
||||||
|
|||||||
Reference in New Issue
Block a user