mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-11 00:59:41 +00:00
Compare commits
42 Commits
a24e6d4495
...
subhire
| Author | SHA1 | Date | |
|---|---|---|---|
| a4f240e581 | |||
| 2e4b84c94e | |||
| 8863d86ed0 | |||
|
|
6550ed2318 | ||
|
|
c9759a6339 | ||
|
|
b637c4e452 | ||
|
|
8986b94b07 | ||
| 87f2de46a1 | |||
| 1615e27767 | |||
| 773f55ac84 | |||
| 63a2f6d47b | |||
| 8393e85b74 | |||
| 311c02d554 | |||
| e100f5a1d4 | |||
| eb07990f4c | |||
| 7b7c1b86de | |||
| 2b8945c513 | |||
| eb3638b93a | |||
| 86c033ba97 | |||
| 52fd662340 | |||
| 9818ed995f | |||
| 5178614d71 | |||
|
1660f51e55
|
|||
| a7bf990666 | |||
|
9feea56211
|
|||
|
951227e68b
|
|||
|
a4a28a6130
|
|||
|
e3d8cf8978
|
|||
|
6e8779c81b
|
|||
|
e0da6a3120
|
|||
|
0c80ef1b72
|
|||
|
0f127d8ca4
|
|||
|
626779ef25
|
|||
| fa1dc31639 | |||
| d69543e309 | |||
|
04ec728972
|
|||
|
bede8b4176
|
|||
|
8cade512d1
|
|||
|
418219940b
|
|||
| 948a41f43a | |||
|
4449efcced
|
|||
|
8b0cd13159
|
24
.github/workflows/django.yml
vendored
24
.github/workflows/django.yml
vendored
@@ -13,26 +13,20 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.9.1
|
python-version: 3.9
|
||||||
- uses: actions/cache@v2
|
cache: 'pipenv'
|
||||||
id: pcache
|
|
||||||
with:
|
|
||||||
path: ~/.local/share/virtualenvs
|
|
||||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pipenv-
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip pipenv
|
python3 -m pip install --upgrade pip pipenv
|
||||||
pipenv install -d
|
pipenv install -d
|
||||||
# if: steps.pcache.outputs.cache-hit != 'true'
|
# if: steps.pcache.outputs.cache-hit != 'true'
|
||||||
- name: Cache Static Files
|
- name: Cache Static Files
|
||||||
id: static-cache
|
id: static-cache
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: 'pipeline/built_assets'
|
path: 'pipeline/built_assets'
|
||||||
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
||||||
@@ -43,9 +37,9 @@ jobs:
|
|||||||
- name: Basic Checks
|
- name: Basic Checks
|
||||||
run: |
|
run: |
|
||||||
pipenv run pycodestyle . --exclude=migrations,node_modules
|
pipenv run pycodestyle . --exclude=migrations,node_modules
|
||||||
pipenv run python manage.py check
|
pipenv run python3 manage.py check
|
||||||
pipenv run python manage.py makemigrations --check --dry-run
|
pipenv run python3 manage.py makemigrations --check --dry-run
|
||||||
pipenv run python manage.py collectstatic --noinput
|
pipenv run python3 manage.py collectstatic --noinput
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: pipenv run pytest -n auto -vv --cov
|
run: pipenv run pytest -n auto -vv --cov
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
|
|||||||
19
Pipfile
19
Pipfile
@@ -21,13 +21,12 @@ 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"
|
||||||
django-htmlmin = "~=0.11.0"
|
django-htmlmin = "~=0.11.0"
|
||||||
envparse = "~=0.2.0"
|
envparse = "*"
|
||||||
gunicorn = "~=20.0.4"
|
gunicorn = "~=20.0.4"
|
||||||
icalendar = "~=4.0.7"
|
icalendar = "~=4.0.7"
|
||||||
idna = "~=2.10"
|
idna = "~=2.10"
|
||||||
@@ -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 = "*"
|
|
||||||
|
|||||||
757
Pipfile.lock
generated
757
Pipfile.lock
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "8b1245d65cdfb7ee133cb029a31ccd2331214d5ef97cbbad3e47c2e050fcb67f"
|
"sha256": "96e25ba04c8709eba5d40776ff20cb82e3abecfd9916b8bd9dd4c594d46e2098"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
"python_version": "3.9"
|
"python_version": "3.10"
|
||||||
},
|
},
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
@@ -155,11 +155,11 @@
|
|||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14",
|
"sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
|
||||||
"sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"
|
"sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2022.9.24"
|
"version": "==2022.12.7"
|
||||||
},
|
},
|
||||||
"chardet": {
|
"chardet": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -233,19 +233,19 @@
|
|||||||
},
|
},
|
||||||
"django": {
|
"django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:18ba8efa36b69cfcd4b670d0fa187c6fe7506596f0ababe580e16909bcdec121",
|
"sha256:08208dfe892eb64fff073ca743b3b952311104f939e7f6dae954fe72dcc533ba",
|
||||||
"sha256:3adc285124244724a394fa9b9839cc8cd116faf7d159554c43ecdaa8cdf0b94d"
|
"sha256:4d492d9024c7b3dfababf49f94511ab6a58e2c9c3c7207786f1ba4eb77750706"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.2.16"
|
"version": "==3.2.18"
|
||||||
},
|
},
|
||||||
"django-debug-toolbar": {
|
"django-debug-toolbar": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1e3acad24e3d351ba45c6fa2072e4164820307332a776b16c9f06d1f89503465",
|
"sha256:24ef1a7d44d25e60d7951e378454c6509bf536dce7e7d9d36e7c387db499bc27",
|
||||||
"sha256:80de23066b624d3970fd296cf02d61988e5d56c31aa0dc4a428970b46e2883a8"
|
"sha256:879f8a4672d41621c06a4d322dcffa630fc4df056cada6e417ed01db0e5e0478"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.7.0"
|
"version": "==3.8.1"
|
||||||
},
|
},
|
||||||
"django-filter": {
|
"django-filter": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -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,87 +367,94 @@
|
|||||||
},
|
},
|
||||||
"importlib-metadata": {
|
"importlib-metadata": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab",
|
"sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad",
|
||||||
"sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"
|
"sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==5.0.0"
|
"version": "==6.0.0"
|
||||||
},
|
},
|
||||||
"lxml": {
|
"lxml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318",
|
"sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7",
|
||||||
"sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c",
|
"sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726",
|
||||||
"sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b",
|
"sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03",
|
||||||
"sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000",
|
"sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140",
|
||||||
"sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73",
|
"sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a",
|
||||||
"sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d",
|
"sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05",
|
||||||
"sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb",
|
"sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03",
|
||||||
"sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8",
|
"sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419",
|
||||||
"sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2",
|
"sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4",
|
||||||
"sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345",
|
"sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e",
|
||||||
"sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94",
|
"sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67",
|
||||||
"sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e",
|
"sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50",
|
||||||
"sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b",
|
"sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894",
|
||||||
"sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc",
|
"sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf",
|
||||||
"sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a",
|
"sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947",
|
||||||
"sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9",
|
"sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1",
|
||||||
"sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc",
|
"sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd",
|
||||||
"sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387",
|
"sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3",
|
||||||
"sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb",
|
"sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92",
|
||||||
"sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7",
|
"sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3",
|
||||||
"sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4",
|
"sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457",
|
||||||
"sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97",
|
"sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74",
|
||||||
"sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67",
|
"sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf",
|
||||||
"sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627",
|
"sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1",
|
||||||
"sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7",
|
"sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4",
|
||||||
"sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd",
|
"sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975",
|
||||||
"sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3",
|
"sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5",
|
||||||
"sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7",
|
"sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe",
|
||||||
"sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130",
|
"sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7",
|
||||||
"sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b",
|
"sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1",
|
||||||
"sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036",
|
"sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2",
|
||||||
"sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785",
|
"sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409",
|
||||||
"sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca",
|
"sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f",
|
||||||
"sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91",
|
"sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f",
|
||||||
"sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc",
|
"sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5",
|
||||||
"sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536",
|
"sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24",
|
||||||
"sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391",
|
"sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e",
|
||||||
"sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3",
|
"sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4",
|
||||||
"sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d",
|
"sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a",
|
||||||
"sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21",
|
"sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c",
|
||||||
"sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3",
|
"sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de",
|
||||||
"sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d",
|
"sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f",
|
||||||
"sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29",
|
"sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b",
|
||||||
"sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715",
|
"sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5",
|
||||||
"sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed",
|
"sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7",
|
||||||
"sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25",
|
"sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a",
|
||||||
"sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c",
|
"sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c",
|
||||||
"sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785",
|
"sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9",
|
||||||
"sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837",
|
"sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e",
|
||||||
"sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4",
|
"sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab",
|
||||||
"sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b",
|
"sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941",
|
||||||
"sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2",
|
"sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5",
|
||||||
"sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067",
|
"sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45",
|
||||||
"sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448",
|
"sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7",
|
||||||
"sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d",
|
"sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892",
|
||||||
"sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2",
|
"sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746",
|
||||||
"sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc",
|
"sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c",
|
||||||
"sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c",
|
"sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53",
|
||||||
"sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5",
|
"sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe",
|
||||||
"sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84",
|
"sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184",
|
||||||
"sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8",
|
"sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38",
|
||||||
"sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf",
|
"sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df",
|
||||||
"sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7",
|
"sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9",
|
||||||
"sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e",
|
"sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b",
|
||||||
"sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb",
|
"sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2",
|
||||||
"sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b",
|
"sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0",
|
||||||
"sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3",
|
"sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda",
|
||||||
"sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad",
|
"sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b",
|
||||||
"sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8",
|
"sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5",
|
||||||
"sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"
|
"sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380",
|
||||||
|
"sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33",
|
||||||
|
"sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8",
|
||||||
|
"sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1",
|
||||||
|
"sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889",
|
||||||
|
"sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9",
|
||||||
|
"sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f",
|
||||||
|
"sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"
|
||||||
],
|
],
|
||||||
"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": "==4.9.1"
|
"version": "==4.9.2"
|
||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -459,69 +466,79 @@
|
|||||||
},
|
},
|
||||||
"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": [
|
||||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
"sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
|
||||||
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
"sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==21.3"
|
"version": "==23.0"
|
||||||
},
|
},
|
||||||
"pep517": {
|
"pep517": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -533,45 +550,42 @@
|
|||||||
},
|
},
|
||||||
"pikepdf": {
|
"pikepdf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0207442d9b943efec7eb07ea5b3f138d90cc61429a3c3243902ac909cb508fb2",
|
"sha256:0a0914d482aa1b80584659c44aef1b2770b473a504cedc209fa6db3f24575ef6",
|
||||||
"sha256:0709832cc49ef51f004975e6e9bdc6daee8a8d68de621d428f13c95a83952e7f",
|
"sha256:1b8dfdc2184aca33e271b104e0ec468e52ac6591ab51bcd32c2e53bc8cdeeffe",
|
||||||
"sha256:07448689cc4c1e249e26ae694a2060210948e61035356cca3a5b8baf3a6a147d",
|
"sha256:1faed2d2553aafb6f4969f0a970958d1847869631eb1c82f5a91ef636817b93a",
|
||||||
"sha256:0bbf45e702bb0556d705e1a4b5da391921e024cc1e6823675f2ea200acec1199",
|
"sha256:260efb3c6aa44c013da2278872593bc4712facd5b766de2b2d88c53c5f524449",
|
||||||
"sha256:1376f3f4b1c34ac089a644af2e499ca713e4e4ec17035362350c0ec78ca6286e",
|
"sha256:3af2fd5762e222bf5133acec4f7d56719dda4a9b7dd468eb1c37a10055ff64b2",
|
||||||
"sha256:15725f1bf572abb9a675f61874da66dc22144e74f374f7e8d023558e8c9c3f38",
|
"sha256:3e1490beb13b2d1a509aeb98fb0669ab7dea4035abd1df0e12085393f556654a",
|
||||||
"sha256:18383d8e6a620c52974b75034ad99c423f80468a434b52de456bb74d5ab51360",
|
"sha256:422cb31ed4b489b9e18f4a803fab7c6ea10ef6916960a5d8b5e531c5af3bfbca",
|
||||||
"sha256:1dedbb95bb2c67d6923c91cdcd5a92703d10e4c4825d85cd7b8b474039978741",
|
"sha256:4290059bdf8d05cf3a7ba185d64b5756c745f178fc102aec41bbcd4a057e02f8",
|
||||||
"sha256:2035b39d2e5c97b6d9ede632f514403888e3f47d2b1e8b69b98420766ccb898b",
|
"sha256:5aca06b88b2d53122baaf3009bcfaec291b3c408846e401cddf8b2e89a0e0fe6",
|
||||||
"sha256:2dd952e678dfc523f2c481c3d0a29b9823f07024f73dea7e9c03d2ac6592a61c",
|
"sha256:7eef9fe4d06cb01482486561292b3c3675d7506328f990cb60b26994dd7ac1d8",
|
||||||
"sha256:3d06dabf16592bb7975e1124000212c3c3bab1e97ed3f7c6534ea92efe9b621a",
|
"sha256:80d4eb3624980c1292d7e2db66c569f146012e86004b8739a3580346ee8f69a9",
|
||||||
"sha256:40999c3f48e5d0259662f6f708694d3ace43b01b4a2a197cfed5cf230557b116",
|
"sha256:81caa67a08fdf683c521497fafd48d9b7bdf02549625329d8a1bb8ce706ef362",
|
||||||
"sha256:46c7c9a7128d1751eedbe769dbc6c0a7983eccded74fd7d1e236d83a50c9cc58",
|
"sha256:910a45cc6506dd899032638c3775f708278d99ccc9c3681fb75a57b55051d262",
|
||||||
"sha256:529d4d099eecbdaa3e06490a032954ce96feae2596c1ea22f961dbf791444a3c",
|
"sha256:93cd0357140dfa79e16c1d9249775d11eebb392665fcdb1528684aae71966b4e",
|
||||||
"sha256:5887799a29510b53c7015b05d7276ee2e0f0ff1d782c75c3a3d1d9f68013665e",
|
"sha256:93d98f460eb209b89ce855a5defb059ca82326042ee52dcb692a05e1c1a24bef",
|
||||||
"sha256:5c2de883986ef25e2e9b8ded8e5c285cb390950742164ce1bf116158009cd9c9",
|
"sha256:9411824a7aa477fcad209e6e01cf23f0ecbd6833805812eca2026e372c724096",
|
||||||
"sha256:62a8c05876b9c7af4cad0ba9a8f22c77775bcceb118c35d682735955f5485297",
|
"sha256:a32215111f6713c934b9ed9a6fd686940559953539e81f70bfb860efbddfe3c3",
|
||||||
"sha256:6df4510606546c9c995afe3f799c506fe90798602b0628affffb7e1516fa1062",
|
"sha256:a59559a1e480f4a7229f600d6fce22b1d32729df8552099542e37a95c02a572f",
|
||||||
"sha256:746897cbfc0c200de6be428a4e92dee72d0e03e1ff00d56006ee94fb59be199d",
|
"sha256:ab07529096da5ea410dea81add8d724ac91f093b982b86f503771bfe8864e48c",
|
||||||
"sha256:801100d8b4b885a203e76bc7266296f909944d621e6a0ac480fa2a0a0e0b1bb4",
|
"sha256:b0f1ca24118517970cfc78f902ded681c2e399dfb21ff04745143b4156dd551e",
|
||||||
"sha256:823f8b1cbad1182709d81afa32c23ac37b9c8ed33bdbc2b41f674be9420dc108",
|
"sha256:b7711e2c0ff1ad265f5c0497f519584c029c9906d72ad78e89e260c57964a5c0",
|
||||||
"sha256:94057ca79525ba5eb5cc9c42337364f0e9e5f239887c0457dffb4ba3e6ac0187",
|
"sha256:b860c44503b6bb237b97c2ae06d47dc645140649d61d2f5dd5276438bc60e2e0",
|
||||||
"sha256:9b1ba16cc5eb243c5e684c220752358a8e1e28a4e02ecdf2c3d24646f29c623f",
|
"sha256:b90bb4e77f9ea8a21d94c5d25e1b416f08a668377ea48edf8808be49f09f906a",
|
||||||
"sha256:9c9d75bb77dfe9b6f8915bc5339cfb0db427c3cb7cd75aa419b1da3c82f122ed",
|
"sha256:ba75e70c932830fb7253b343f43f0ce03317661971cd3df03fc27c7cdb06992e",
|
||||||
"sha256:a0b78071d5fcd6b2288da469e89c030475095349dd57d82f5c40c37600d02e14",
|
"sha256:bf9129507b7258dda27845e1ee6c7c4121674a04c0a1a0ac1115c19e5b4a2edf",
|
||||||
"sha256:a1679c7d5b374895b6196784a75b8122ca0bb9248f5d97cd5ed77c569e264e88",
|
"sha256:cf912ab70313fd0f23a32811f8c3ea815a716ecfd6ee84a564c833d0c06f5483",
|
||||||
"sha256:ab8d610ca732a6369479605817cc55ee6f62d5b105ffe7e3749c3785c383631e",
|
"sha256:dec854f908973c5c3760d246539c58c03b7b701a2bed45173f9a4e4d766d3eab",
|
||||||
"sha256:aff2ce52f0ab4ea8a1fbe57b06982b9fa9f997dd6bbec4b141091a1e71145a63",
|
"sha256:e5bebaca43757c9c357637954b8e49c9221c21a40260394ec4c4fbafada5ee87",
|
||||||
"sha256:bc9b625f5ed454f445bf5012682b24d334adc9f853d41e44cfee7c52ddf92666",
|
"sha256:e900d2314a1019c98c9d3d50445af475ce2f16e8a54e44875201869f8561a3b2",
|
||||||
"sha256:e0e66d49f8a85a4e0f915d42471643a5020bcdbef02586e49328ed417c13326a",
|
"sha256:eb310e903b9a172de352446458390ccce31a2bb19a387f63d37b135cb4cca3f1",
|
||||||
"sha256:e3dbcecc145d46d37738a407e0ddcce7cfb76d3e116ab3ba9c80f4dd14e71a3d",
|
"sha256:edea85799240a3b7534650b275bc7e87519ddf0f47ccf9bdad09b22381da5442",
|
||||||
"sha256:e99a90279a8254fa149d56cd307f94908c7844b2b8b42b61d241259804e40643",
|
"sha256:f7f0bfa897ecabbee3f7dc2ac10a8dd954c42a6f858fbef13019d0cef1b60127",
|
||||||
"sha256:ed0c7b4dcb0fec1cdcb4d7a054dd96204fec42fe7c12414ab3235ab16fb4042b",
|
"sha256:fcfd391b3a255b460a9040ca8e47dd6f36c6ea43d2b61cd00b5ecb06000a6b8f"
|
||||||
"sha256:efc497cd01c55c5dbdd8a81766e317f44f728b3ceb65d7b6c6a064772c60e1c7",
|
|
||||||
"sha256:f35cecdab44cb01377e93a60a475bf4437854d98cb94379fcd65c6daa1c9a37e",
|
|
||||||
"sha256:f9174a555b94b55d5666264fb3d03858e3faebd8f80f0fa30f0c525266f5bc64"
|
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==6.2.4"
|
"version": "==7.1.1"
|
||||||
},
|
},
|
||||||
"pillow": {
|
"pillow": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -843,10 +857,11 @@
|
|||||||
},
|
},
|
||||||
"retrying": {
|
"retrying": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:08c039560a6da2fe4f2c426d0766e284d3b736e355f8dd24b37367b0bb41973b"
|
"sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e",
|
||||||
|
"sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.3.3"
|
"version": "==1.3.4"
|
||||||
},
|
},
|
||||||
"selenium": {
|
"selenium": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -858,19 +873,19 @@
|
|||||||
},
|
},
|
||||||
"sentry-sdk": {
|
"sentry-sdk": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:e7b78a1ddf97a5f715a50ab8c3f7a93f78b114c67307785ee828ef67a5d6f117",
|
"sha256:69ecbb2e1ff4db02a06c4f20f6f69cb5dfe3ebfbc06d023e40d77cf78e9c37e7",
|
||||||
"sha256:f467e6c7fac23d4d42bc83eb049c400f756cd2d65ab44f0cc1165d0c7c3d40bc"
|
"sha256:7ad4d37dd093f4a7cb5ad804c6efe9e8fab8873f7ffc06042dc3f3fd700a93ec"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.11.0"
|
"version": "==1.15.0"
|
||||||
},
|
},
|
||||||
"setuptools": {
|
"setuptools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840",
|
"sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330",
|
||||||
"sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d"
|
"sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==65.6.0"
|
"version": "==67.4.0"
|
||||||
},
|
},
|
||||||
"simplejson": {
|
"simplejson": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -949,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": [
|
||||||
@@ -1012,11 +1027,11 @@
|
|||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e",
|
"sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72",
|
||||||
"sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"
|
"sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.26.12"
|
"version": "==1.26.14"
|
||||||
},
|
},
|
||||||
"webencodings": {
|
"webencodings": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1248,96 +1263,86 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {
|
"develop": {
|
||||||
"async-generator": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
|
|
||||||
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
|
|
||||||
],
|
|
||||||
"markers": "python_version >= '3.5'",
|
|
||||||
"version": "==1.10"
|
|
||||||
},
|
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6",
|
"sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836",
|
||||||
"sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"
|
"sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.5'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==22.1.0"
|
"version": "==22.2.0"
|
||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14",
|
"sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
|
||||||
"sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"
|
"sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2022.9.24"
|
"version": "==2022.12.7"
|
||||||
},
|
},
|
||||||
"charset-normalizer": {
|
"chardet": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
|
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||||
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
|
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"index": "pypi",
|
||||||
"version": "==2.1.1"
|
"version": "==4.0.0"
|
||||||
},
|
},
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"extras": [
|
|
||||||
"toml"
|
|
||||||
],
|
|
||||||
"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_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==6.5.0"
|
"version": "==6.6.0b1"
|
||||||
},
|
},
|
||||||
"coveralls": {
|
"coveralls": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1349,11 +1354,11 @@
|
|||||||
},
|
},
|
||||||
"django-coverage-plugin": {
|
"django-coverage-plugin": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3b0aa1ed26b52c5844c88510995f4a4b60b4fb0679970d11f82654bb8a2bd16a",
|
"sha256:245ecd6e91e5be7a66e0f811fd57091c46b55c0eb85c7fe1a1e4aebca9842a5f",
|
||||||
"sha256:f662efe592bf98baf2e540312059c918daa8d8379244a2a6b6f984c4a1dda015"
|
"sha256:c063d8d49ba2da30fe95d91cf3f0f9f659b55c3f80d4a029d619b2b3144b1206"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.0.4"
|
"version": "==3.0.0"
|
||||||
},
|
},
|
||||||
"docopt": {
|
"docopt": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1363,11 +1368,11 @@
|
|||||||
},
|
},
|
||||||
"exceptiongroup": {
|
"exceptiongroup": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828",
|
"sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e",
|
||||||
"sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"
|
"sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"
|
||||||
],
|
],
|
||||||
"markers": "python_version < '3.11'",
|
"markers": "python_version < '3.11'",
|
||||||
"version": "==1.0.4"
|
"version": "==1.1.0"
|
||||||
},
|
},
|
||||||
"execnet": {
|
"execnet": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1377,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_version >= '3.7'",
|
|
||||||
"version": "==0.14.0"
|
|
||||||
},
|
|
||||||
"idna": {
|
"idna": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||||
@@ -1395,26 +1392,19 @@
|
|||||||
},
|
},
|
||||||
"iniconfig": {
|
"iniconfig": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
|
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
|
||||||
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
|
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
|
||||||
],
|
|
||||||
"version": "==1.1.1"
|
|
||||||
},
|
|
||||||
"outcome": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672",
|
|
||||||
"sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"
|
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==1.2.0"
|
"version": "==2.0.0"
|
||||||
},
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
"sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
|
||||||
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
"sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==21.3"
|
"version": "==23.0"
|
||||||
},
|
},
|
||||||
"pluggy": {
|
"pluggy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1466,14 +1456,6 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.9.1"
|
"version": "==2.9.1"
|
||||||
},
|
},
|
||||||
"pyparsing": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
|
||||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.4.7"
|
|
||||||
},
|
|
||||||
"pypom": {
|
"pypom": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5da52cf447e62f43a0cfa47dfe52eb822eff07b2fdad759f930d1d227c15220b",
|
"sha256:5da52cf447e62f43a0cfa47dfe52eb822eff07b2fdad759f930d1d227c15220b",
|
||||||
@@ -1482,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": [
|
||||||
@@ -1535,11 +1509,11 @@
|
|||||||
"psutil"
|
"psutil"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:688da9b814370e891ba5de650c9327d1a9d861721a524eb917e620eec3e90291",
|
"sha256:336098e3bbd8193276867cc87db8b22903c3927665dff9d1ac8684c02f597b68",
|
||||||
"sha256:9feb9a18e1790696ea23e1434fa73b325ed4998b0e9fcb221f16fd1945e6df1b"
|
"sha256:fa10f95a2564cd91652f2d132725183c3b590d9fdcdec09d3677386ecf4c1ce9"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.0.2"
|
"version": "==3.2.0"
|
||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1559,41 +1533,18 @@
|
|||||||
},
|
},
|
||||||
"setuptools": {
|
"setuptools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840",
|
"sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330",
|
||||||
"sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d"
|
"sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==65.6.0"
|
"version": "==67.4.0"
|
||||||
},
|
|
||||||
"six": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
|
||||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.15.0"
|
|
||||||
},
|
|
||||||
"sniffio": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
|
|
||||||
"sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
|
|
||||||
],
|
|
||||||
"markers": "python_version >= '3.7'",
|
|
||||||
"version": "==1.3.0"
|
|
||||||
},
|
|
||||||
"sortedcontainers": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
|
|
||||||
"sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
|
|
||||||
],
|
|
||||||
"version": "==2.4.0"
|
|
||||||
},
|
},
|
||||||
"splinter": {
|
"splinter": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4a14a9d1f9d1372c64b666627ef4e103d759379bc1a9bde0c487e00d70976b1e",
|
"sha256:1f072570c084f5f7e0c685b6b5d93b1a7959da06cb5da4c3b548dc1b3b0757a0",
|
||||||
"sha256:616da85a0c99bef00b59e75eb29e2e48162027c68ccb81a12d1dfe6d26209692"
|
"sha256:ba9603385deb91ffb92b2e0edeed3da58dce3bfa0e0db1a37143c5e64b83ceb2"
|
||||||
],
|
],
|
||||||
"version": "==0.18.1"
|
"version": "==0.19.0"
|
||||||
},
|
},
|
||||||
"tomli": {
|
"tomli": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1603,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_version >= '3.7'",
|
|
||||||
"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:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e",
|
"sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72",
|
||||||
"sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"
|
"sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.26.12"
|
"version": "==1.26.14"
|
||||||
},
|
|
||||||
"wsproto": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
|
|
||||||
"sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
|
|
||||||
],
|
|
||||||
"markers": "python_version >= '3.7'",
|
|
||||||
"version": "==1.2.0"
|
|
||||||
},
|
},
|
||||||
"zope.component": {
|
"zope.component": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1643,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",
|
||||||
@@ -1754,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")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Command(BaseCommand):
|
|||||||
for event in events:
|
for event in events:
|
||||||
earliest_time = event.earliest_time if isinstance(event.earliest_time, datetime.datetime) else timezone.make_aware(datetime.datetime.combine(event.earliest_time, datetime.time(00, 00)))
|
earliest_time = event.earliest_time if isinstance(event.earliest_time, datetime.datetime) else timezone.make_aware(datetime.datetime.combine(event.earliest_time, datetime.time(00, 00)))
|
||||||
# 48 hours = 172800 seconds
|
# 48 hours = 172800 seconds
|
||||||
if not event.cancelled and not event.dry_hire and (earliest_time - timezone.now()).total_seconds() <= 172800 and not hasattr(event, 'riskassessment'):
|
if event.is_rig and not event.cancelled and not event.dry_hire and (earliest_time - timezone.now()).total_seconds() <= 172800 and not hasattr(event, 'riskassessment'):
|
||||||
context = {
|
context = {
|
||||||
"event": event,
|
"event": event,
|
||||||
"url": "https://" + settings.DOMAIN + reverse('event_ra', kwargs={'pk': event.pk})
|
"url": "https://" + settings.DOMAIN + reverse('event_ra', kwargs={'pk': event.pk})
|
||||||
|
|||||||
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"
|
||||||
|
|||||||
26
RIGS/urls.py
26
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,12 +66,28 @@ 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'),
|
||||||
|
|
||||||
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()),
|
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()),
|
||||||
name='event_ra'),
|
name='event_ra'),
|
||||||
path('event/ra/<int:pk>/', permission_required_with_403('RIGS.view_riskassessment')(views.EventRiskAssessmentDetail.as_view()),
|
path('event/ra/<int:pk>/', login_required(views.EventRiskAssessmentDetail.as_view()),
|
||||||
name='ra_detail'),
|
name='ra_detail'),
|
||||||
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
|
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
|
||||||
name='ra_edit'),
|
name='ra_edit'),
|
||||||
@@ -87,7 +99,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
|
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
|
||||||
name='event_ec'),
|
name='event_ec'),
|
||||||
path('event/checklist/<int:pk>/', permission_required_with_403('RIGS.view_eventchecklist')(views.EventChecklistDetail.as_view()),
|
path('event/checklist/<int:pk>/', login_required(views.EventChecklistDetail.as_view()),
|
||||||
name='ec_detail'),
|
name='ec_detail'),
|
||||||
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
|
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
|
||||||
name='ec_edit'),
|
name='ec_edit'),
|
||||||
|
|||||||
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),
|
||||||
|
|||||||
18
assets/migrations/0027_asset_nickname.py
Normal file
18
assets/migrations/0027_asset_nickname.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.16 on 2022-12-11 00:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0026_auto_20220526_1623'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='asset',
|
||||||
|
name='nickname',
|
||||||
|
field=models.CharField(blank=True, max_length=120),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -95,14 +95,15 @@ class AssetManager(models.Manager):
|
|||||||
def search(self, query=None):
|
def search(self, query=None):
|
||||||
qs = self.get_queryset()
|
qs = self.get_queryset()
|
||||||
if query is not None:
|
if query is not None:
|
||||||
or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query))
|
or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query) | Q(nickname__icontains=query))
|
||||||
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
@@ -125,6 +126,7 @@ class Asset(models.Model, RevisionMixin):
|
|||||||
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
||||||
replacement_cost = models.DecimalField(null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
replacement_cost = models.DecimalField(null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
|
nickname = models.CharField(max_length=120, blank=True)
|
||||||
|
|
||||||
# Audit
|
# Audit
|
||||||
last_audited_at = models.DateTimeField(blank=True, null=True)
|
last_audited_at = models.DateTimeField(blank=True, null=True)
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
<label for="{{ form.description.id_for_label }}">Description</label>
|
<label for="{{ form.description.id_for_label }}">Description</label>
|
||||||
{% render_field form.description|add_class:'form-control' value=object.description %}
|
{% render_field form.description|add_class:'form-control' value=object.description %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.nickname.id_for_label }}">Nickname</label>
|
||||||
|
{% render_field form.nickname|add_class:'form-control' value=object.nickname %}
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.category.id_for_label }}" >Category</label>
|
<label for="{{ form.category.id_for_label }}" >Category</label>
|
||||||
{% render_field form.category|add_class:'form-control'%}
|
{% render_field form.category|add_class:'form-control'%}
|
||||||
@@ -45,7 +49,10 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<dt>Asset ID</dt>
|
<dt>Asset ID</dt>
|
||||||
<dd>{{ object.asset_id }}</dd>
|
<dd>{{ object.asset_id }}</dd>
|
||||||
|
{% if object.nickname %}
|
||||||
|
<dt>Nickname</dt>
|
||||||
|
<dd>"{{ object.nickname }}"</dd>
|
||||||
|
{% endif %}
|
||||||
<dt>Description</dt>
|
<dt>Description</dt>
|
||||||
<dd>{{ object.description }}</dd>
|
<dd>{{ object.description }}</dd>
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
177
package-lock.json
generated
177
package-lock.json
generated
@@ -5,6 +5,7 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"name": "PyRIGS",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "Custom",
|
"license": "Custom",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -18,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",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"uglify-js": "^3.14.5"
|
"uglify-js": "^3.14.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browser-sync": "^2.27.10"
|
"browser-sync": "^2.27.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -1026,13 +1026,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browser-sync": {
|
"node_modules/browser-sync": {
|
||||||
"version": "2.27.10",
|
"version": "2.27.11",
|
||||||
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.10.tgz",
|
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.11.tgz",
|
||||||
"integrity": "sha512-xKm+6KJmJu6RuMWWbFkKwOCSqQOxYe3nOrFkKI5Tr/ZzjPxyU3pFShKK3tWnazBo/3lYQzN7fzjixG8fwJh1Xw==",
|
"integrity": "sha512-U5f9u97OYJH66T0MGWWzG9rOQTW6ZmDMj97vsmtqwNS03JAwdLVES8eel2lD3rvAqQCNAFqaJ74NMacBI57vJg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browser-sync-client": "^2.27.10",
|
"browser-sync-client": "^2.27.11",
|
||||||
"browser-sync-ui": "^2.27.10",
|
"browser-sync-ui": "^2.27.11",
|
||||||
"bs-recipes": "1.3.4",
|
"bs-recipes": "1.3.4",
|
||||||
"bs-snippet-injector": "^2.0.1",
|
"bs-snippet-injector": "^2.0.1",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
@@ -1050,7 +1050,7 @@
|
|||||||
"micromatch": "^4.0.2",
|
"micromatch": "^4.0.2",
|
||||||
"opn": "5.3.0",
|
"opn": "5.3.0",
|
||||||
"portscanner": "2.2.0",
|
"portscanner": "2.2.0",
|
||||||
"qs": "6.2.3",
|
"qs": "^6.11.0",
|
||||||
"raw-body": "^2.3.2",
|
"raw-body": "^2.3.2",
|
||||||
"resp-modifier": "6.0.2",
|
"resp-modifier": "6.0.2",
|
||||||
"rx": "4.1.0",
|
"rx": "4.1.0",
|
||||||
@@ -1070,9 +1070,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browser-sync-client": {
|
"node_modules/browser-sync-client": {
|
||||||
"version": "2.27.10",
|
"version": "2.27.11",
|
||||||
"resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.10.tgz",
|
"resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.11.tgz",
|
||||||
"integrity": "sha512-KCFKA1YDj6cNul0VsA28apohtBsdk5Wv8T82ClOZPZMZWxPj4Ny5AUbrj9UlAb/k6pdxE5HABrWDhP9+cjt4HQ==",
|
"integrity": "sha512-okMNfD2NasL/XD1/BclP3onXjhahisk3e/kTQ5HPDT/lLqdBqNDd6QFcjI5I1ak7na2hxKQSLjryql+7fp5gKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"etag": "1.8.1",
|
"etag": "1.8.1",
|
||||||
@@ -1086,9 +1086,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browser-sync-ui": {
|
"node_modules/browser-sync-ui": {
|
||||||
"version": "2.27.10",
|
"version": "2.27.11",
|
||||||
"resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.10.tgz",
|
"resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.11.tgz",
|
||||||
"integrity": "sha512-elbJILq4Uo6OQv6gsvS3Y9vRAJlWu+h8j0JDkF0X/ua+3S6SVbbiWnZc8sNOFlG7yvVGIwBED3eaYQ0iBo1Dtw==",
|
"integrity": "sha512-1T/Y8Pp1R68aUL7zVSFq0nxtr258xWd/nTasCAHX2M6EsGaswVOFtXsw3bKqsr35z+J+LfVfOdz1HFLYKxdgrA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async-each-series": "0.1.1",
|
"async-each-series": "0.1.1",
|
||||||
@@ -1931,9 +1931,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/decode-uri-component": {
|
"node_modules/decode-uri-component": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||||
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
|
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
@@ -3062,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",
|
||||||
@@ -4209,9 +4204,9 @@
|
|||||||
"integrity": "sha512-rmglSaNttGo4LY33PFW51mgeD1ItvHyfS9cRCD+Cj9Msj/xFaG/sZjLGVtPbtxYJmhY/c8jtw6G07yWhC2ifEw=="
|
"integrity": "sha512-rmglSaNttGo4LY33PFW51mgeD1ItvHyfS9cRCD+Cj9Msj/xFaG/sZjLGVtPbtxYJmhY/c8jtw6G07yWhC2ifEw=="
|
||||||
},
|
},
|
||||||
"node_modules/http-cache-semantics": {
|
"node_modules/http-cache-semantics": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||||
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
|
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
|
||||||
},
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -5957,6 +5952,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.12.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||||
|
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-keys": {
|
"node_modules/object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
@@ -6910,12 +6914,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.2.3",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||||
"integrity": "sha512-AY4g8t3LMboim0t6XWFdz6J5OuJ1ZNYu54SXihS/OMpgyCqYmcAJnWqkNSOjSjWmq3xxy+GF9uWQI2lI/7tKIA==",
|
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.0.4"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/quick-lru": {
|
"node_modules/quick-lru": {
|
||||||
@@ -7631,6 +7641,20 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.0",
|
||||||
|
"get-intrinsic": "^1.0.2",
|
||||||
|
"object-inspect": "^1.9.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
@@ -7841,15 +7865,15 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-client": {
|
"node_modules/socket.io-client": {
|
||||||
"version": "4.5.3",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.4.tgz",
|
||||||
"integrity": "sha512-I/hqDYpQ6JKwtJOf5ikM+Qz+YujZPMEl6qBLhxiP0nX+TfXKhW4KZZG8lamrD6Y5ngjmYHreESVasVCgi5Kl3A==",
|
"integrity": "sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
"debug": "~4.3.2",
|
"debug": "~4.3.2",
|
||||||
"engine.io-client": "~6.2.3",
|
"engine.io-client": "~6.2.3",
|
||||||
"socket.io-parser": "~4.2.0"
|
"socket.io-parser": "~4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
@@ -8667,9 +8691,9 @@
|
|||||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "4.9.3",
|
"version": "4.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -9991,13 +10015,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browser-sync": {
|
"browser-sync": {
|
||||||
"version": "2.27.10",
|
"version": "2.27.11",
|
||||||
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.10.tgz",
|
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.27.11.tgz",
|
||||||
"integrity": "sha512-xKm+6KJmJu6RuMWWbFkKwOCSqQOxYe3nOrFkKI5Tr/ZzjPxyU3pFShKK3tWnazBo/3lYQzN7fzjixG8fwJh1Xw==",
|
"integrity": "sha512-U5f9u97OYJH66T0MGWWzG9rOQTW6ZmDMj97vsmtqwNS03JAwdLVES8eel2lD3rvAqQCNAFqaJ74NMacBI57vJg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"browser-sync-client": "^2.27.10",
|
"browser-sync-client": "^2.27.11",
|
||||||
"browser-sync-ui": "^2.27.10",
|
"browser-sync-ui": "^2.27.11",
|
||||||
"bs-recipes": "1.3.4",
|
"bs-recipes": "1.3.4",
|
||||||
"bs-snippet-injector": "^2.0.1",
|
"bs-snippet-injector": "^2.0.1",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
@@ -10015,7 +10039,7 @@
|
|||||||
"micromatch": "^4.0.2",
|
"micromatch": "^4.0.2",
|
||||||
"opn": "5.3.0",
|
"opn": "5.3.0",
|
||||||
"portscanner": "2.2.0",
|
"portscanner": "2.2.0",
|
||||||
"qs": "6.2.3",
|
"qs": "^6.11.0",
|
||||||
"raw-body": "^2.3.2",
|
"raw-body": "^2.3.2",
|
||||||
"resp-modifier": "6.0.2",
|
"resp-modifier": "6.0.2",
|
||||||
"rx": "4.1.0",
|
"rx": "4.1.0",
|
||||||
@@ -10029,9 +10053,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browser-sync-client": {
|
"browser-sync-client": {
|
||||||
"version": "2.27.10",
|
"version": "2.27.11",
|
||||||
"resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.10.tgz",
|
"resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.27.11.tgz",
|
||||||
"integrity": "sha512-KCFKA1YDj6cNul0VsA28apohtBsdk5Wv8T82ClOZPZMZWxPj4Ny5AUbrj9UlAb/k6pdxE5HABrWDhP9+cjt4HQ==",
|
"integrity": "sha512-okMNfD2NasL/XD1/BclP3onXjhahisk3e/kTQ5HPDT/lLqdBqNDd6QFcjI5I1ak7na2hxKQSLjryql+7fp5gKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"etag": "1.8.1",
|
"etag": "1.8.1",
|
||||||
@@ -10042,9 +10066,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browser-sync-ui": {
|
"browser-sync-ui": {
|
||||||
"version": "2.27.10",
|
"version": "2.27.11",
|
||||||
"resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.10.tgz",
|
"resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.27.11.tgz",
|
||||||
"integrity": "sha512-elbJILq4Uo6OQv6gsvS3Y9vRAJlWu+h8j0JDkF0X/ua+3S6SVbbiWnZc8sNOFlG7yvVGIwBED3eaYQ0iBo1Dtw==",
|
"integrity": "sha512-1T/Y8Pp1R68aUL7zVSFq0nxtr258xWd/nTasCAHX2M6EsGaswVOFtXsw3bKqsr35z+J+LfVfOdz1HFLYKxdgrA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"async-each-series": "0.1.1",
|
"async-each-series": "0.1.1",
|
||||||
@@ -10687,9 +10711,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"decode-uri-component": {
|
"decode-uri-component": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||||
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og=="
|
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="
|
||||||
},
|
},
|
||||||
"default-compare": {
|
"default-compare": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -11579,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",
|
||||||
@@ -12500,9 +12519,9 @@
|
|||||||
"integrity": "sha512-rmglSaNttGo4LY33PFW51mgeD1ItvHyfS9cRCD+Cj9Msj/xFaG/sZjLGVtPbtxYJmhY/c8jtw6G07yWhC2ifEw=="
|
"integrity": "sha512-rmglSaNttGo4LY33PFW51mgeD1ItvHyfS9cRCD+Cj9Msj/xFaG/sZjLGVtPbtxYJmhY/c8jtw6G07yWhC2ifEw=="
|
||||||
},
|
},
|
||||||
"http-cache-semantics": {
|
"http-cache-semantics": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||||
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
|
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
|
||||||
},
|
},
|
||||||
"http-errors": {
|
"http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -13867,6 +13886,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"object-inspect": {
|
||||||
|
"version": "1.12.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||||
|
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"object-keys": {
|
"object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
@@ -14500,10 +14525,13 @@
|
|||||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||||
},
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.2.3",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||||
"integrity": "sha512-AY4g8t3LMboim0t6XWFdz6J5OuJ1ZNYu54SXihS/OMpgyCqYmcAJnWqkNSOjSjWmq3xxy+GF9uWQI2lI/7tKIA==",
|
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"side-channel": "^1.0.4"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"quick-lru": {
|
"quick-lru": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
@@ -15067,6 +15095,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
|
||||||
},
|
},
|
||||||
|
"side-channel": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"call-bind": "^1.0.0",
|
||||||
|
"get-intrinsic": "^1.0.2",
|
||||||
|
"object-inspect": "^1.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"signal-exit": {
|
"signal-exit": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
@@ -15254,15 +15293,15 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"socket.io-client": {
|
"socket.io-client": {
|
||||||
"version": "4.5.3",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.4.tgz",
|
||||||
"integrity": "sha512-I/hqDYpQ6JKwtJOf5ikM+Qz+YujZPMEl6qBLhxiP0nX+TfXKhW4KZZG8lamrD6Y5ngjmYHreESVasVCgi5Kl3A==",
|
"integrity": "sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
"debug": "~4.3.2",
|
"debug": "~4.3.2",
|
||||||
"engine.io-client": "~6.2.3",
|
"engine.io-client": "~6.2.3",
|
||||||
"socket.io-parser": "~4.2.0"
|
"socket.io-parser": "~4.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": {
|
"debug": {
|
||||||
@@ -15889,9 +15928,9 @@
|
|||||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "4.9.3",
|
"version": "4.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||||
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
|
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"typo-js": {
|
"typo-js": {
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -34,7 +33,7 @@
|
|||||||
"uglify-js": "^3.14.5"
|
"uglify-js": "^3.14.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browser-sync": "^2.27.10"
|
"browser-sync": "^2.27.11"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gulp": "gulp",
|
"gulp": "gulp",
|
||||||
|
|||||||
@@ -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,6 +143,15 @@ 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.",
|
||||||
|
"In military use, training means gaining the physical ability to perform and survive in combat, and learn the many skills needed in a time of war.",
|
||||||
|
"These include how to use a variety of weapons, outdoor survival skills, and how to survive being captured by the enemy, among many others. See military education and training.",
|
||||||
|
"While some studies have indicated relaxation training is useful for some medical conditions, autogenic training has limited results or has been the result of few studies.",
|
||||||
|
"Some occupations are inherently hazardous, and require a minimum level of competence before the practitioners can perform the work at an acceptable level of safety to themselves or others in the vicinity.",
|
||||||
|
"Occupational diving, rescue, firefighting and operation of certain types of machinery and vehicles may require assessment and certification of a minimum acceptable competence before the person is allowed to practice as a licensed instructor."
|
||||||
|
]
|
||||||
|
|
||||||
for i, name in enumerate(names):
|
for i, name in enumerate(names):
|
||||||
category = random.choice(self.categories)
|
category = random.choice(self.categories)
|
||||||
previous_item = models.TrainingItem.objects.filter(category=category).last()
|
previous_item = models.TrainingItem.objects.filter(category=category).last()
|
||||||
@@ -150,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, description=name)
|
item = models.TrainingItem.objects.create(category=category, reference_number=number, name=name, description=random.choice(descriptions) + random.choice(descriptions) + random.choice(descriptions))
|
||||||
self.items.append(item)
|
self.items.append(item)
|
||||||
|
|
||||||
def setup_levels(self):
|
def setup_levels(self):
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.18 on 2023-02-19 14:02
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('training', '0005_auto_20220223_1535'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='trainingitem',
|
||||||
|
old_name='description',
|
||||||
|
new_name='name',
|
||||||
|
),
|
||||||
|
]
|
||||||
18
training/migrations/0007_trainingitem_description.py
Normal file
18
training/migrations/0007_trainingitem_description.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.18 on 2023-02-19 14:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('training', '0006_rename_description_trainingitem_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='trainingitem',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -85,7 +85,7 @@ class TrainingItemManager(QueryablePropertiesManager):
|
|||||||
def search(self, query=None):
|
def search(self, query=None):
|
||||||
qs = self.get_queryset()
|
qs = self.get_queryset()
|
||||||
if query is not None:
|
if query is not None:
|
||||||
or_lookup = (Q(description__icontains=query) | Q(display_id=query))
|
or_lookup = (Q(name__icontains=query) | Q(description__icontains=query) | Q(display_id=query))
|
||||||
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -94,16 +94,13 @@ class TrainingItemManager(QueryablePropertiesManager):
|
|||||||
class TrainingItem(models.Model):
|
class TrainingItem(models.Model):
|
||||||
reference_number = models.IntegerField()
|
reference_number = models.IntegerField()
|
||||||
category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.CASCADE)
|
category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.CASCADE)
|
||||||
description = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
active = models.BooleanField(default=True)
|
active = models.BooleanField(default=True)
|
||||||
prerequisites = models.ManyToManyField('self', symmetrical=False, blank=True)
|
prerequisites = models.ManyToManyField('self', symmetrical=False, blank=True)
|
||||||
|
|
||||||
objects = TrainingItemManager()
|
objects = TrainingItemManager()
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
@queryable_property
|
@queryable_property
|
||||||
def display_id(self):
|
def display_id(self):
|
||||||
return f"{self.category.reference_number}.{self.reference_number}"
|
return f"{self.category.reference_number}.{self.reference_number}"
|
||||||
@@ -121,7 +118,7 @@ class TrainingItem(models.Model):
|
|||||||
return models.Q()
|
return models.Q()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
name = f"{self.display_id} {self.description}"
|
name = f"{self.display_id} {self.name}"
|
||||||
if not self.active:
|
if not self.active:
|
||||||
name += " (inactive)"
|
name += " (inactive)"
|
||||||
return name
|
return name
|
||||||
@@ -149,7 +146,7 @@ class TrainingItemQualificationManager(QueryablePropertiesManager):
|
|||||||
def search(self, query=None):
|
def search(self, query=None):
|
||||||
qs = self.get_queryset().select_related('item', 'supervisor', 'item__category')
|
qs = self.get_queryset().select_related('item', 'supervisor', 'item__category')
|
||||||
if query is not None:
|
if query is not None:
|
||||||
or_lookup = (Q(item__description__icontains=query) | Q(supervisor__first_name__icontains=query) | Q(supervisor__last_name__icontains=query) | Q(item__category__name__icontains=query) | Q(item__display_id=query))
|
or_lookup = (Q(item__name__icontains=query) | Q(supervisor__first_name__icontains=query) | Q(supervisor__last_name__icontains=query) | Q(item__category__name__icontains=query) | Q(item__display_id=query))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
or_lookup = Q(item__category__reference_number=int(query)) | or_lookup
|
or_lookup = Q(item__category__reference_number=int(query)) | or_lookup
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -13,7 +18,8 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% for item in category.items.all %}
|
{% for item in category.items.all %}
|
||||||
<li class="list-group-item {% if not item.active%}text-warning{%endif%}">{{ item }}
|
<li class="list-group-item {% if not item.active%}text-warning{%endif%}">{{ item }} <a href="{% url 'item_qualification' item.pk %}" class="btn btn-info float-right"><span class="fas fa-user"></span> Qualified Users</a>
|
||||||
|
<br><small>{{ item.description }}</small>
|
||||||
{% if item.prerequisites.exists %}
|
{% if item.prerequisites.exists %}
|
||||||
<div class="ml-3 font-italic">
|
<div class="ml-3 font-italic">
|
||||||
<p class="text-info mb-0">Passed Out Prerequisites:</p>
|
<p class="text-info mb-0">Passed Out Prerequisites:</p>
|
||||||
@@ -24,7 +30,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'item_qualification' item.pk %}" class="btn btn-info"><span class="fas fa-user"></span> Qualified Users</a>
|
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
25
training/templates/item_list.xml
Normal file
25
training/templates/item_list.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends 'base_print.xml' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 style="page-head">TEC Training Item List</h1>
|
||||||
|
<spacer length="15" />
|
||||||
|
{% for category in categories %}
|
||||||
|
<h2 {% if not forloop.first %}style="breakbefore"{%else%}style="emheader"{%endif%}>{{category}}</h2>
|
||||||
|
<spacer length="10" />
|
||||||
|
{% for item in category.items.all %}
|
||||||
|
<h3>{{ item }}</h3>
|
||||||
|
<spacer length="4" />
|
||||||
|
<para>{{ item.description }}</para>
|
||||||
|
{% if item.prerequisites.exists %}
|
||||||
|
<h4>Prerequisites:</h4>
|
||||||
|
<ul bulletFontSize="5">
|
||||||
|
{% for p in item.prerequisites.all %}
|
||||||
|
<li><para>{{p}}</para></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
<spacer length="8" />
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
<namedString id="lastPage"><pageNumber/></namedString>
|
||||||
|
{% endblock %}
|
||||||
@@ -2,20 +2,19 @@ from django.urls import path
|
|||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from training.decorators import is_supervisor
|
from training.decorators import is_supervisor
|
||||||
from PyRIGS.decorators import permission_required_with_403
|
|
||||||
|
|
||||||
from training import views, models
|
from training import views, models
|
||||||
from versioning.views import VersionHistory
|
from versioning.views import VersionHistory
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('items/', login_required(views.ItemList.as_view()), name='item_list'),
|
path('items/', login_required(views.ItemList.as_view()), name='item_list'),
|
||||||
|
path('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'),
|
||||||
path('trainee/<int:pk>/',
|
path('trainee/<int:pk>/', login_required(views.TraineeDetail.as_view()),
|
||||||
permission_required_with_403('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
|
||||||
name='trainee_detail'),
|
name='trainee_detail'),
|
||||||
path('trainee/<int:pk>/history', permission_required_with_403('RIGS.view_profile')(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
|
path('trainee/<int:pk>/history', login_required(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
|
||||||
path('trainee/<int:pk>/add_qualification/', is_supervisor()(views.AddQualification.as_view()),
|
path('trainee/<int:pk>/add_qualification/', is_supervisor()(views.AddQualification.as_view()),
|
||||||
name='add_qualification'),
|
name='add_qualification'),
|
||||||
path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),
|
path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),
|
||||||
|
|||||||
@@ -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