Compare commits

..

41 Commits

Author SHA1 Message Date
5a54092771 Update power_detail.html - fix copy paste error 2023-08-30 18:41:45 +00:00
dependabot[bot]
e8a8f2bf0d Build(deps): Bump semver from 5.7.1 to 5.7.2 (#557)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 10:15:23 +01:00
dependabot[bot]
3984c17605 Build(deps): Bump certifi from 2023.5.7 to 2023.7.22 (#559)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 10:07:48 +01:00
dependabot[bot]
b2209eef4e Build(deps): Bump pygments from 2.7.4 to 2.15.0 (#558)
Bumps [pygments](https://github.com/pygments/pygments) from 2.7.4 to 2.15.0.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.7.4...2.15.0)

---
updated-dependencies:
- dependency-name: pygments
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 10:07:37 +01:00
e7c4f7f73d Move flaky test out of class so that it can properly xfail 2023-07-09 23:08:37 +01:00
769d983e3d Remove ability to delete level requirements from level detail
To return in future in a special 'edit mode' page, but easiest to just nuke them entirely for now as it's not something we often need to do
2023-07-09 22:37:36 +01:00
303005a32b Mark problematic test as expected to fail 2023-07-09 22:34:53 +01:00
c722773586 Add explanations to payment methods, remove 'SU Core' method as now obsolete 2023-07-09 22:24:36 +01:00
dependabot[bot]
0c2e677786 Build(deps): Bump tough-cookie and node-sass (#556)
Removes [tough-cookie](https://github.com/salesforce/tough-cookie). It's no longer used after updating ancestor dependency [node-sass](https://github.com/sass/node-sass). These dependencies need to be updated together.


Removes `tough-cookie`

Updates `node-sass` from 7.0.3 to 9.0.0
- [Release notes](https://github.com/sass/node-sass/releases)
- [Changelog](https://github.com/sass/node-sass/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sass/node-sass/compare/v7.0.3...v9.0.0)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
- dependency-name: node-sass
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-09 18:05:52 +01:00
dependabot[bot]
9719581977 Build(deps): Bump django from 3.2.19 to 3.2.20 (#554)
Bumps [django](https://github.com/django/django) from 3.2.19 to 3.2.20.
- [Commits](https://github.com/django/django/compare/3.2.19...3.2.20)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-07 15:52:05 +01:00
James Herbert
b838dde30b Clarify check-in banner (#553) 2023-07-01 10:32:28 +01:00
387fa56442 Exclude non-rigs from the 'now' checkin list 2023-06-29 12:32:48 +01:00
f35ce88acc Fix another copy paste error 2023-06-28 16:31:42 +01:00
ee5468fdd7 Fix indenting, because apparently it had been sufficient time since I last pushed a pep8 mistake 2023-06-28 13:39:54 +01:00
1ce6ec3284 Re-do fix for #537 without regression
Ignore versions for "None" models, such as where the model has subsequently been deleted (eventchecklistcrew and eventchecklistvehicle). Now it makes sense...
2023-06-28 13:39:10 +01:00
677f352524 Fix #549: 500 error when reviewing power tests
Another silly copy paste error from ECs. Also really makes me want 'repeatable fragments' in Django templating so that the top and bottom page buttons could actually be the same...
2023-06-28 13:06:03 +01:00
8b3102b136 Only consider new users when sending 'users for approval' email 2023-06-28 13:01:45 +01:00
e2b1dc1d05 Tweak things to allow easier location of new users for approval 2023-06-28 13:01:29 +01:00
c9ba228bd2 Filter inactive/unapproved users out of SecureAPI requests. Fixes #552 2023-06-28 12:55:42 +01:00
9f4cd41d23 FIX #537: recent changes 500 server error
Bloody *hell* that was hard to track down. Looks like there's somehow versioning data for eventchecklistcrew and eventchecklistvehicle hanging around which the included patch skips over for...reasons. I don't know why it works, why it ever worked before, or much of anything really. Send it.
2023-06-28 12:38:31 +01:00
2049d0f76d Revert "Another stab at error-tolerant versioning"
This reverts commit 29db3b5a0c.
2023-06-28 12:02:04 +01:00
29db3b5a0c Another stab at error-tolerant versioning 2023-06-28 11:53:15 +01:00
53b09e47b8 Open event thread drafts in new tab 2023-06-27 17:34:08 +01:00
097e7c2481 Another stab at resolving weird null error ref #537 2023-06-27 17:32:00 +01:00
16874073e9 "Create forum post" button on a rig page (#551)
* Add button for creating forum thread draft from event detail

TODO: Allow RIGS to ingest POST requests sent from the forum on new posts in RIG info to link up the forum thread

RE https://forum.nottinghamtec.co.uk/t/rigs-discourse-integration/15592/21

* Mockup webhook recieving view

* Correct method of CRSF exemption for webhook reciever

* Use f-strings correctly, not like a big dumb

* That was also dumb, fix that too

* Second shot at webhook reciever

* Oops

* >.<

* Third shot

* Try again at signing

* What if I gave it the right arguments. That might be a good start.

* More fiddling with auth

* Add debug print

* Okay, put that back where it was because I inavertently overloaded my import

Flashbacks to my java days...

* Different header access method

* Fix import, again

* Fix ommited json parsing wotsit

* Fix url

* Fix string index

* Correct template logic

* Allow manual adding/editing of URLs

* Filter by right flavour of event

* Amend event str conversion for consistency

* Oops

* Make migration

Will be squashed later

* Fix logic when creating events

* Squash migration

* Implement codedoctor suggestion
2023-06-27 13:00:51 +01:00
Leo Riley
d03a4e115f Correct URL on power test edit save (#550) 2023-06-22 18:36:23 +01:00
e1b87b412a Temporarily remove power MIC defaulting to MIC in RAs
Because it only works somtimes :p
2023-06-05 20:50:00 +01:00
54b44404ba Fix various minor issues (#545)
* Add absolute URL to power tests

* Update to target Python 3.10

* Return user to current page when clicking 'mark reviewed'

* Add units to power test record detail and form

I'm a bad scientist (coz I'm an engineer)

* Allow a higher value in PSSC fields

* Default venue to event venue in EC/PT

* Fix population of initial venue values for EC/PT

* Add link to create power test from EC detail

* Do not set power plan field to required on RA

"This might be a problem if the risk assessment is being done by one person and the power plan by another."

* Default power MIC to MIC

* Implement some suggestions from the Doctor

* Prevent checking in to cancelled events and dry hires

Will close #539

* Exclude dry hires from H&S overview list

* Add "ex VAT" tooltips to asset purchase price and replacement cost

* Automagically clear and focus ID field when audit modal closes

Closes #533

* Delete unused things

* Allow two decimal places in cable length, show training item IDs in selectpicker

Will close #540

* Fix #524 500 Error when viewing qualification list for items nobody is qualified in

* Update README.md

* Add a guard against nulls in recent changes

Maybe fixes #537 I'm unable to replicate locally

* Turn down verbosity of CI tests, fix tests, potential speedup

* Squash migration

* Add encoding to open

* Update to v3 upload-artifact

Resolves a deprecation warning
2023-05-29 11:50:04 +01:00
dependabot[bot]
26942b80dd Build(deps): Bump tornado from 6.3.1 to 6.3.2 (#544)
Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.3.1 to 6.3.2.
- [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst)
- [Commits](https://github.com/tornadoweb/tornado/compare/v6.3.1...v6.3.2)

---
updated-dependencies:
- dependency-name: tornado
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-28 17:31:56 +01:00
dependabot[bot]
888300490c Build(deps): Bump socket.io-parser from 4.2.2 to 4.2.3 (#542)
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 4.2.2 to 4.2.3.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/4.2.2...4.2.3)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-24 10:50:41 +01:00
9201f9d896 Update level_list.html 2023-05-24 10:32:22 +01:00
9fae129e26 Update level_list.html 2023-05-24 10:22:19 +01:00
dependabot[bot]
8d45e260dd Build(deps): Bump requests from 2.25.1 to 2.31.0 (#541)
Bumps [requests](https://github.com/psf/requests) from 2.25.1 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.25.1...v2.31.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 21:36:41 +01:00
7d8dddb952 Update 0047_auto_20230517_0944.py
Testing in production for the L.
2023-05-19 12:56:39 +00:00
1104f10c91 Update 0047_auto_20230517_0944.py 2023-05-19 12:20:25 +00:00
3d5efba0af sigh 2023-05-19 11:49:14 +00:00
9a44aaf557 Update 0047_auto_20230517_0944.py
sigh
2023-05-19 11:37:16 +00:00
550eff83ee Update 0046_create_powertests.py
Fine, I'll do it that way.
2023-05-19 11:25:18 +00:00
bf3da5ae25 Update 0046_create_powertests.py
Allow it to migrate all foreignkey fields
2023-05-19 11:13:17 +00:00
87aa87bc0f Update 0046_create_powertests.py
Don't try and find a nonexistent notes field on EventChecklist.
2023-05-19 10:47:50 +00:00
01a0b8f831 Add event checkin functionality, seperate power tests into a different form (#536)
* Split power related parts of event checklist into a seperate form

* Revamp H&S overview, remove individual lists.

They were not a good thing.

* Remove old 'vehicle/crew' stuff

* Very initial version of checkin form

* Further work on checkin, add role field etc

* Fix tests after form split

* Add ability to edit checkins, more validation

* Basic checkin/out logic complete

* Add homepage checkin for events happening now

* Minor improvement to homepage UI

* Checkin button turns into checkout button where applicable

* UI work

* Clicking check out does not redirect the user

* Register check in model with the admin site

* Add power record status chip, checklist status chip displays number of checklists

* Minor fixes

* Implement codedoctor suggestions

* pep8

* Add data migration for crew/vehicles

* Checkin only requires login (no perms) and block users from editing other checkins at Django level
2023-05-19 10:28:51 +00:00
56 changed files with 1269 additions and 1276 deletions

View File

@@ -12,6 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PYTHONDONTWRITEBYTECODE: 1
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Python - name: Set up Python
@@ -41,8 +42,8 @@ jobs:
pipenv run python3 manage.py makemigrations --check --dry-run pipenv run python3 manage.py makemigrations --check --dry-run
pipenv run python3 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 --cov
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v3
if: failure() if: failure()
with: with:
name: failure-screenshots ${{ matrix.test-group }} name: failure-screenshots ${{ matrix.test-group }}

View File

@@ -39,7 +39,7 @@ premailer = "~=3.7.0"
progress = "~=1.5" progress = "~=1.5"
psutil = "~=5.8.0" psutil = "~=5.8.0"
psycopg2 = "~=2.8.6" psycopg2 = "~=2.8.6"
Pygments = "~=2.7.4" Pygments = "~=2.15.0"
pyparsing = "~=2.4.7" pyparsing = "~=2.4.7"
PyPDF2 = "~=1.27.5" PyPDF2 = "~=1.27.5"
PyPOM = "~=2.2.4" PyPOM = "~=2.2.4"
@@ -47,7 +47,7 @@ python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21" pytoml = "~=0.1.21"
pytz = "~=2020.5" pytz = "~=2020.5"
reportlab = "*" reportlab = "*"
requests = "~=2.25.1" requests = "~=2.31.0"
retrying = "~=1.3.3" retrying = "~=1.3.3"
simplejson = "~=3.17.2" simplejson = "~=3.17.2"
six = "~=1.15.0" six = "~=1.15.0"
@@ -56,7 +56,7 @@ sqlparse = "~=0.4.2"
static3 = "~=0.7.0" static3 = "~=0.7.0"
svg2rlg = "~=0.3" svg2rlg = "~=0.3"
tini = "~=3.0.1" tini = "~=3.0.1"
tornado = "~=6.1" tornado = "~=6.3"
urllib3 = "~=1.26.5" urllib3 = "~=1.26.5"
whitenoise = "~=5.2.0" whitenoise = "~=5.2.0"
yolk = "~=0.4.3" yolk = "~=0.4.3"
@@ -93,7 +93,7 @@ pytest = "*"
pytest-reverse = "*" pytest-reverse = "*"
[requires] [requires]
python_version = "3.9" python_version = "3.10"
[dev-packages.pytest-xdist] [dev-packages.pytest-xdist]
extras = [ "psutil",] extras = [ "psutil",]

664
Pipfile.lock generated
View File

@@ -1,11 +1,11 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "fd57c06cb6fe93ee6b4af9d978b7185d76bc32c962a847a209539534dcd2fb2a" "sha256": "db73ee1e1b3aaf5abe57ca59314cf73ba81f0e8c96aa8aca99e4462571adf9d1"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
"python_version": "3.9" "python_version": "3.10"
}, },
"sources": [ "sources": [
{ {
@@ -32,18 +32,12 @@
"index": "pypi", "index": "pypi",
"version": "==3.3.4" "version": "==3.3.4"
}, },
"async-generator": {
"hashes": [
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
],
"version": "==1.10"
},
"attrs": { "attrs": {
"hashes": [ "hashes": [
"sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04",
"sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"
], ],
"markers": "python_version >= '3.7'",
"version": "==23.1.0" "version": "==23.1.0"
}, },
"backports.tempfile": { "backports.tempfile": {
@@ -169,10 +163,11 @@
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
"sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
], ],
"version": "==2023.5.7" "index": "pypi",
"version": "==2023.7.22"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@@ -182,6 +177,87 @@
"index": "pypi", "index": "pypi",
"version": "==4.0.0" "version": "==4.0.0"
}, },
"charset-normalizer": {
"hashes": [
"sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96",
"sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c",
"sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710",
"sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706",
"sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020",
"sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252",
"sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad",
"sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329",
"sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a",
"sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f",
"sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6",
"sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4",
"sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a",
"sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46",
"sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2",
"sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23",
"sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace",
"sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd",
"sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982",
"sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10",
"sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2",
"sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea",
"sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09",
"sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5",
"sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149",
"sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489",
"sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9",
"sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80",
"sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592",
"sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3",
"sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6",
"sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed",
"sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c",
"sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200",
"sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a",
"sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e",
"sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d",
"sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6",
"sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623",
"sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669",
"sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3",
"sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa",
"sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9",
"sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2",
"sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f",
"sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1",
"sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4",
"sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a",
"sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8",
"sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3",
"sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029",
"sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f",
"sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959",
"sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22",
"sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7",
"sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952",
"sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346",
"sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e",
"sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d",
"sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299",
"sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd",
"sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a",
"sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3",
"sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037",
"sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94",
"sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c",
"sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858",
"sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a",
"sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449",
"sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c",
"sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918",
"sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1",
"sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c",
"sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
"sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
],
"markers": "python_version >= '3.7'",
"version": "==3.2.0"
},
"configparser": { "configparser": {
"hashes": [ "hashes": [
"sha256:85d5de102cfe6d14a5172676f09d19c465ce63d6019cf0a4ef13385fc535e828", "sha256:85d5de102cfe6d14a5172676f09d19c465ce63d6019cf0a4ef13385fc535e828",
@@ -211,6 +287,7 @@
"sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a", "sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a",
"sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969" "sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969"
], ],
"markers": "python_version >= '3.7'",
"version": "==0.7.0" "version": "==0.7.0"
}, },
"cssutils": { "cssutils": {
@@ -253,11 +330,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:031365bae96814da19c10706218c44dff3b654cc4de20a98bd2d29b9bde469f0", "sha256:a477ab326ae7d8807dc25c186b951ab8c7648a3a23f9497763c37307a2b5ef87",
"sha256:21cc991466245d659ab79cb01204f9515690f8dae00e5eabde307f14d24d4d7d" "sha256:dec2a116787b8e14962014bf78e120bba454135108e1af9e9b91ade7b2964c40"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.2.19" "version": "==3.2.20"
}, },
"django-debug-toolbar": { "django-debug-toolbar": {
"hashes": [ "hashes": [
@@ -355,10 +432,11 @@
}, },
"exceptiongroup": { "exceptiongroup": {
"hashes": [ "hashes": [
"sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e", "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5",
"sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785" "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"
], ],
"version": "==1.1.1" "markers": "python_version < '3.11'",
"version": "==1.1.2"
}, },
"freetype-py": { "freetype-py": {
"hashes": [ "hashes": [
@@ -370,6 +448,7 @@
"sha256:ccdb1616794a8ad48beaa9e29d3494e6643d24d8e925cc39263de21c062ea5a7", "sha256:ccdb1616794a8ad48beaa9e29d3494e6643d24d8e925cc39263de21c062ea5a7",
"sha256:f9b64ce3272a5c358dcee824800a32d70997fb872a0965a557adca20fce7a5d0" "sha256:f9b64ce3272a5c358dcee824800a32d70997fb872a0965a557adca20fce7a5d0"
], ],
"markers": "python_version >= '3.7'",
"version": "==2.3.0" "version": "==2.3.0"
}, },
"gunicorn": { "gunicorn": {
@@ -385,6 +464,7 @@
"sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
"sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
], ],
"markers": "python_version >= '3.7'",
"version": "==0.14.0" "version": "==0.14.0"
}, },
"html5lib": { "html5lib": {
@@ -392,6 +472,7 @@
"sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d",
"sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f" "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.1" "version": "==1.1"
}, },
"icalendar": { "icalendar": {
@@ -416,90 +497,105 @@
"sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705" "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version < '3.10'",
"version": "==6.6.0" "version": "==6.6.0"
}, },
"lxml": { "lxml": {
"hashes": [ "hashes": [
"sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7", "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3",
"sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726", "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d",
"sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03", "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a",
"sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140", "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120",
"sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a", "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305",
"sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05", "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287",
"sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03", "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23",
"sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419", "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52",
"sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4", "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f",
"sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e", "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4",
"sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67", "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584",
"sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50", "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f",
"sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894", "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693",
"sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf", "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef",
"sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947", "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5",
"sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1", "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02",
"sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd", "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc",
"sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3", "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7",
"sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92", "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da",
"sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3", "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a",
"sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457", "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40",
"sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74", "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8",
"sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf", "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd",
"sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1", "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601",
"sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4", "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c",
"sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975", "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be",
"sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5", "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2",
"sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe", "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c",
"sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7", "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129",
"sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1", "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc",
"sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2", "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2",
"sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409", "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1",
"sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f", "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7",
"sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f", "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d",
"sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5", "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477",
"sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24", "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d",
"sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e", "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e",
"sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4", "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7",
"sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a", "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2",
"sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c", "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574",
"sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de", "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf",
"sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f", "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b",
"sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b", "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98",
"sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5", "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12",
"sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7", "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42",
"sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a", "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35",
"sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c", "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d",
"sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9", "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce",
"sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e", "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d",
"sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab", "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f",
"sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941", "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db",
"sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5", "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4",
"sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45", "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694",
"sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7", "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac",
"sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892", "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2",
"sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746", "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7",
"sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c", "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96",
"sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53", "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d",
"sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe", "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b",
"sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184", "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a",
"sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38", "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13",
"sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df", "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340",
"sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9", "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6",
"sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b", "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458",
"sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2", "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c",
"sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0", "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c",
"sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda", "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9",
"sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b", "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432",
"sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5", "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991",
"sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380", "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69",
"sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33", "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf",
"sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8", "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb",
"sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1", "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b",
"sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889", "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833",
"sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9", "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76",
"sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f", "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85",
"sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c" "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e",
"sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50",
"sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8",
"sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4",
"sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b",
"sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5",
"sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190",
"sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7",
"sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa",
"sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0",
"sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9",
"sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0",
"sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b",
"sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5",
"sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7",
"sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"
], ],
"version": "==4.9.2" "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.9.3"
}, },
"markdown": { "markdown": {
"hashes": [ "hashes": [
@@ -583,6 +679,7 @@
"sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672", "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672",
"sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5" "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"
], ],
"markers": "python_version >= '3.7'",
"version": "==1.2.0" "version": "==1.2.0"
}, },
"packaging": { "packaging": {
@@ -590,6 +687,7 @@
"sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
"sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
], ],
"markers": "python_version >= '3.7'",
"version": "==23.1" "version": "==23.1"
}, },
"pep517": { "pep517": {
@@ -708,10 +806,11 @@
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849",
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"
], ],
"version": "==1.0.0" "markers": "python_version >= '3.7'",
"version": "==1.2.0"
}, },
"premailer": { "premailer": {
"hashes": [ "hashes": [
@@ -785,27 +884,28 @@
}, },
"pycairo": { "pycairo": {
"hashes": [ "hashes": [
"sha256:1a6d8e0f353062ad92954784e33dbbaf66c880c9c30e947996c542ed9748aaaf", "sha256:031f5ef2c80792673f2c54ee285f2a31779a44d7521a27a7f82e4a5ecfafc26e",
"sha256:2dec5378133778961993fb59d66df16070e03f4d491b67eb695ca9ad7a696008", "sha256:1444d52f1bb4cc79a4a0c0fe2ccec4bd78ff885ab01ebe1c0f637d8392bcafb6",
"sha256:3a71f758e461180d241e62ef52e85499c843bd2660fd6d87cec99c9833792bfa", "sha256:220742f187d3940d695c1af1a0c1646e26dc2199d65b7bafaa527e15c3520fd3",
"sha256:564601e5f528531c6caec1c0177c3d0709081e1a2a5cccc13561f715080ae535", "sha256:26fe2c32ba24caae524d855f26f3ee1a0c1ea3291da2a19604264ed29f64d834",
"sha256:82e335774a17870bc038e0c2fb106c1e5e7ad0c764662023886dfcfce5bb5a52", "sha256:3199d6a0538d6482c71efb816bd330515e98bb06f182e23572c77d92be98f536",
"sha256:87efd62a7b7afad9a0a420f05b6008742a6cfc59077697be65afe8dc73ae15ad", "sha256:4631ed794a3376ec314ce47826c3e51940b54695f4ef7d5b3245b203037ae760",
"sha256:9b61ac818723adc04367301317eb2e814a83522f07bbd1f409af0dada463c44c", "sha256:6ad5c3425408ebb0dfaad2cca4a80e7524d7a309305acdb2569638b5cc988fe7",
"sha256:a4b1f525bbdf637c40f4d91378de36c01ec2b7f8ecc585b700a079b9ff83298e", "sha256:afccac552386ab628e8ae658185fa363e8d15a5afe96d1de43f97027dd78bdd6",
"sha256:d6bacff15d688ed135b4567965a4b664d9fb8de7417a7865bb138ad612043c9f", "sha256:c5f6efdd86fe13d36a6b004c64d8e97cde9854d599cf6cca962afb1966fe533d",
"sha256:e7cde633986435d87a86b6118b7b6109c384266fd719ef959883e2729f6eafae", "sha256:c7c79e748ec849811241d29553184c3ad93c857558dbe9954a49327680b8d356",
"sha256:ec305fc7f2f0299df78aadec0eaf6eb9accb90eda242b5d3492544d3f2b28027" "sha256:ed0622a0ccffb873ffe7fee1699d60779f1260fba143390e5366d55f1d1739f5"
], ],
"version": "==1.23.0" "markers": "python_version >= '3.8'",
"version": "==1.24.0"
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435", "sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094",
"sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337" "sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.7.4" "version": "==2.15.0"
}, },
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
@@ -881,11 +981,11 @@
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.25.1" "version": "==2.31.0"
}, },
"retrying": { "retrying": {
"hashes": [ "hashes": [
@@ -897,10 +997,10 @@
}, },
"rlpycairo": { "rlpycairo": {
"hashes": [ "hashes": [
"sha256:7cd1eac30fe69d98f75d67a54892f9c10534a047b9a959ef17bb3926a196e50a", "sha256:f6ec712a76914f78c1491351e1d79eeb1a40d072accf5d36075e399963c17b17"
"sha256:a88bce206c45d2180f944b8754c6e2e9245f80506c90fdfb94c7fbdd27805c25"
], ],
"version": "==0.2.0" "markers": "python_version >= '3.7'",
"version": "==0.3.0"
}, },
"selenium": { "selenium": {
"hashes": [ "hashes": [
@@ -912,11 +1012,19 @@
}, },
"sentry-sdk": { "sentry-sdk": {
"hashes": [ "hashes": [
"sha256:5932c092c6e6035584eb74d77064e4bce3b7935dfc4a331349719a40db265840", "sha256:0bbcecda9f51936904c1030e7fef0fe693e633888f02a14d1cb68646a50e83b3",
"sha256:cf89a5063ef84278d186aceaed6fb595bfe67d099298e537634a323664265669" "sha256:56d6d9d194c898d853a7c1dd99bed92ce82334ee1282292c15bcc967ff1a49b5"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.22.2" "version": "==1.24.0"
},
"setuptools": {
"hashes": [
"sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f",
"sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"
],
"markers": "python_version >= '3.7'",
"version": "==68.0.0"
}, },
"simplejson": { "simplejson": {
"hashes": [ "hashes": [
@@ -998,6 +1106,7 @@
"sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
"sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
], ],
"markers": "python_version >= '3.7'",
"version": "==1.3.0" "version": "==1.3.0"
}, },
"sortedcontainers": { "sortedcontainers": {
@@ -1013,7 +1122,6 @@
"sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea" "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.0'",
"version": "==2.4.1" "version": "==2.4.1"
}, },
"sqlparse": { "sqlparse": {
@@ -1042,6 +1150,7 @@
"hashes": [ "hashes": [
"sha256:3ae765d3a9409ee60c0fb4d24c2deb6a80617aa927054f5bcd7fc98f0695e587" "sha256:3ae765d3a9409ee60c0fb4d24c2deb6a80617aa927054f5bcd7fc98f0695e587"
], ],
"markers": "python_version >= '3.7'",
"version": "==1.5.1" "version": "==1.5.1"
}, },
"tini": { "tini": {
@@ -1057,6 +1166,7 @@
"sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847", "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847",
"sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627" "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"
], ],
"markers": "python_version >= '3.7'",
"version": "==1.2.1" "version": "==1.2.1"
}, },
"toml": { "toml": {
@@ -1064,46 +1174,49 @@
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2" "version": "==0.10.2"
}, },
"tornado": { "tornado": {
"hashes": [ "hashes": [
"sha256:1285f0691143f7ab97150831455d4db17a267b59649f7bd9700282cba3d5e771", "sha256:05615096845cf50a895026f749195bf0b10b8909f9be672f50b0fe69cba368e4",
"sha256:3455133b9ff262fd0a75630af0a8ee13564f25fb4fd3d9ce239b8a7d3d027bf8", "sha256:0c325e66c8123c606eea33084976c832aa4e766b7dff8aedd7587ea44a604cdf",
"sha256:5e2f49ad371595957c50e42dd7e5c14d64a6843a3cf27352b69c706d1b5918af", "sha256:29e71c847a35f6e10ca3b5c2990a52ce38b233019d8e858b755ea6ce4dcdd19d",
"sha256:81c17e0cc396908a5e25dc8e9c5e4936e6dfd544c9290be48bd054c79bcad51e", "sha256:4b927c4f19b71e627b13f3db2324e4ae660527143f9e1f2e2fb404f3a187e2ba",
"sha256:90f569a35a8ec19bde53aa596952071f445da678ec8596af763b9b9ce07605e6", "sha256:5b17b1cf5f8354efa3d37c6e28fdfd9c1c1e5122f2cb56dac121ac61baa47cbe",
"sha256:9661aa8bc0e9d83d757cd95b6f6d1ece8ca9fd1ccdd34db2de381e25bf818233", "sha256:6a0848f1aea0d196a7c4f6772197cbe2abc4266f836b0aac76947872cd29b411",
"sha256:a27a1cfa9997923f80bdd962b3aab048ac486ad8cfb2f237964f8ab7f7eb824b", "sha256:7efcbcc30b7c654eb6a8c9c9da787a851c18f8ccd4a5a3a95b05c7accfa068d2",
"sha256:b4e7b956f9b5e6f9feb643ea04f07e7c6b49301e03e0023eedb01fa8cf52f579", "sha256:834ae7540ad3a83199a8da8f9f2d383e3c3d5130a328889e4cc991acc81e87a0",
"sha256:d7117f3c7ba5d05813b17a1f04efc8e108a1b811ccfddd9134cc68553c414864", "sha256:b46a6ab20f5c7c1cb949c72c1994a4585d2eaa0be4853f50a03b5031e964fc7c",
"sha256:db181eb3df8738613ff0a26f49e1b394aade05034b01200a63e9662f347d4415", "sha256:c2de14066c4a38b4ecbbcd55c5cc4b5340eb04f1c5e81da7451ef555859c833f",
"sha256:ffdce65a281fd708da5a9def3bfb8f364766847fa7ed806821a69094c9629e8a" "sha256:c367ab6c0393d71171123ca5515c61ff62fe09024fa6bf299cd1339dc9456829"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.3.1" "version": "==6.3.2"
}, },
"trio": { "trio": {
"hashes": [ "hashes": [
"sha256:ce68f1c5400a47b137c5a4de72c7c901bd4e7a24fbdebfe9b41de8c6c04eaacf", "sha256:3887cf18c8bcc894433420305468388dac76932e9668afa1c49aa3806b6accb3",
"sha256:f1dd0780a89bfc880c7c7994519cb53f62aacb2c25ff487001c0052bd721cdf0" "sha256:f43da357620e5872b3d940a2e3589aa251fd3f881b65a608d742e00809b1ec38"
], ],
"version": "==0.22.0" "markers": "python_version >= '3.7'",
"version": "==0.22.2"
}, },
"trio-websocket": { "trio-websocket": {
"hashes": [ "hashes": [
"sha256:0908435e4eecc49d830ae1c4d6c47b978a75f00594a2be2104d58b61a04cdb53", "sha256:1a748604ad906a7dcab9a43c6eb5681e37de4793ba0847ef0bc9486933ed027b",
"sha256:af13e9393f9051111300287947ec595d601758ce3d165328e7d36325135a8d62" "sha256:a9937d48e8132ebf833019efde2a52ca82d223a30a7ea3e8d60a7d28f75a4e3a"
], ],
"version": "==0.10.2" "markers": "python_version >= '3.7'",
"version": "==0.10.3"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f",
"sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42" "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.26.15" "version": "==1.26.16"
}, },
"webencodings": { "webencodings": {
"hashes": [ "hashes": [
@@ -1125,6 +1238,7 @@
"sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
"sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736" "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
], ],
"markers": "python_version >= '3.7'",
"version": "==1.2.0" "version": "==1.2.0"
}, },
"yolk": { "yolk": {
@@ -1176,11 +1290,11 @@
}, },
"zope.event": { "zope.event": {
"hashes": [ "hashes": [
"sha256:4ab47faac13163ca3c5d6d8a5595212e14770322e95c338d955e3688ba19082a", "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42",
"sha256:8cca6074a2bbca140d32704694e74098d5621f2c75ea6e3a9458240a3b5382a9" "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.5.1" "version": "==4.5.0"
}, },
"zope.hookable": { "zope.hookable": {
"hashes": [ "hashes": [
@@ -1342,106 +1456,102 @@
} }
}, },
"develop": { "develop": {
"async-generator": {
"hashes": [
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
],
"version": "==1.10"
},
"attrs": { "attrs": {
"hashes": [ "hashes": [
"sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04",
"sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"
], ],
"markers": "python_version >= '3.7'",
"version": "==23.1.0" "version": "==23.1.0"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
"sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
], ],
"version": "==2023.5.7" "index": "pypi",
"version": "==2023.7.22"
}, },
"charset-normalizer": { "charset-normalizer": {
"hashes": [ "hashes": [
"sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96",
"sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c",
"sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710",
"sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706",
"sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020",
"sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252",
"sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad",
"sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329",
"sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a",
"sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f",
"sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6",
"sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4",
"sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a",
"sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46",
"sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2",
"sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23",
"sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace",
"sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd",
"sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982",
"sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10",
"sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2",
"sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea",
"sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09",
"sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5",
"sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149",
"sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489",
"sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9",
"sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80",
"sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592",
"sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3",
"sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6",
"sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed",
"sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c",
"sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200",
"sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a",
"sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e",
"sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d",
"sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6",
"sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623",
"sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669",
"sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3",
"sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa",
"sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9",
"sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2",
"sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f",
"sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1",
"sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4",
"sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a",
"sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8",
"sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3",
"sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029",
"sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f",
"sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959",
"sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22",
"sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7",
"sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952",
"sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346",
"sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e",
"sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d",
"sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299",
"sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd",
"sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a",
"sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3",
"sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037",
"sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94",
"sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c",
"sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858",
"sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a",
"sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449",
"sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c",
"sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918",
"sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1",
"sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c",
"sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
"sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
], ],
"version": "==3.1.0" "markers": "python_version >= '3.7'",
"version": "==3.2.0"
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
@@ -1496,6 +1606,7 @@
"sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84",
"sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"
], ],
"markers": "python_version >= '3.7'",
"version": "==6.5.0" "version": "==6.5.0"
}, },
"coveralls": { "coveralls": {
@@ -1522,23 +1633,26 @@
}, },
"exceptiongroup": { "exceptiongroup": {
"hashes": [ "hashes": [
"sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e", "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5",
"sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785" "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"
], ],
"version": "==1.1.1" "markers": "python_version < '3.11'",
"version": "==1.1.2"
}, },
"execnet": { "execnet": {
"hashes": [ "hashes": [
"sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5", "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41",
"sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142" "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"
], ],
"version": "==1.9.0" "markers": "python_version >= '3.7'",
"version": "==2.0.2"
}, },
"h11": { "h11": {
"hashes": [ "hashes": [
"sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
"sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
], ],
"markers": "python_version >= '3.7'",
"version": "==0.14.0" "version": "==0.14.0"
}, },
"idna": { "idna": {
@@ -1554,6 +1668,7 @@
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
], ],
"markers": "python_version >= '3.7'",
"version": "==2.0.0" "version": "==2.0.0"
}, },
"outcome": { "outcome": {
@@ -1561,6 +1676,7 @@
"sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672", "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672",
"sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5" "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"
], ],
"markers": "python_version >= '3.7'",
"version": "==1.2.0" "version": "==1.2.0"
}, },
"packaging": { "packaging": {
@@ -1568,14 +1684,16 @@
"sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
"sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
], ],
"markers": "python_version >= '3.7'",
"version": "==23.1" "version": "==23.1"
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849",
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"
], ],
"version": "==1.0.0" "markers": "python_version >= '3.7'",
"version": "==1.2.0"
}, },
"psutil": { "psutil": {
"hashes": [ "hashes": [
@@ -1645,11 +1763,11 @@
}, },
"pytest-cov": { "pytest-cov": {
"hashes": [ "hashes": [
"sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b", "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6",
"sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470" "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.0.0" "version": "==4.1.0"
}, },
"pytest-django": { "pytest-django": {
"hashes": [ "hashes": [
@@ -1676,20 +1794,23 @@
"version": "==3.3.2" "version": "==3.3.2"
}, },
"pytest-xdist": { "pytest-xdist": {
"extras": [
"psutil"
],
"hashes": [ "hashes": [
"sha256:1849bd98d8b242b948e472db7478e090bf3361912a8fed87992ed94085f54727", "sha256:d5ee0520eb1b7bcca50a60a518ab7a7707992812c578198f8b44fdfac78e8c93",
"sha256:37290d161638a20b672401deef1cba812d110ac27e35d213f091d15b8beb40c9" "sha256:ff9daa7793569e6a68544850fd3927cd257cc03a7ef76c95e86915355e82b5f2"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.2.1" "version": "==3.3.1"
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.25.1" "version": "==2.31.0"
}, },
"selenium": { "selenium": {
"hashes": [ "hashes": [
@@ -1699,11 +1820,20 @@
"index": "pypi", "index": "pypi",
"version": "==4.9.1" "version": "==4.9.1"
}, },
"setuptools": {
"hashes": [
"sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f",
"sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"
],
"markers": "python_version >= '3.7'",
"version": "==68.0.0"
},
"sniffio": { "sniffio": {
"hashes": [ "hashes": [
"sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
"sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
], ],
"markers": "python_version >= '3.7'",
"version": "==1.3.0" "version": "==1.3.0"
}, },
"sortedcontainers": { "sortedcontainers": {
@@ -1725,35 +1855,39 @@
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
], ],
"markers": "python_version < '3.11'",
"version": "==2.0.1" "version": "==2.0.1"
}, },
"trio": { "trio": {
"hashes": [ "hashes": [
"sha256:ce68f1c5400a47b137c5a4de72c7c901bd4e7a24fbdebfe9b41de8c6c04eaacf", "sha256:3887cf18c8bcc894433420305468388dac76932e9668afa1c49aa3806b6accb3",
"sha256:f1dd0780a89bfc880c7c7994519cb53f62aacb2c25ff487001c0052bd721cdf0" "sha256:f43da357620e5872b3d940a2e3589aa251fd3f881b65a608d742e00809b1ec38"
], ],
"version": "==0.22.0" "markers": "python_version >= '3.7'",
"version": "==0.22.2"
}, },
"trio-websocket": { "trio-websocket": {
"hashes": [ "hashes": [
"sha256:0908435e4eecc49d830ae1c4d6c47b978a75f00594a2be2104d58b61a04cdb53", "sha256:1a748604ad906a7dcab9a43c6eb5681e37de4793ba0847ef0bc9486933ed027b",
"sha256:af13e9393f9051111300287947ec595d601758ce3d165328e7d36325135a8d62" "sha256:a9937d48e8132ebf833019efde2a52ca82d223a30a7ea3e8d60a7d28f75a4e3a"
], ],
"version": "==0.10.2" "markers": "python_version >= '3.7'",
"version": "==0.10.3"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f",
"sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42" "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.26.15" "version": "==1.26.16"
}, },
"wsproto": { "wsproto": {
"hashes": [ "hashes": [
"sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
"sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736" "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
], ],
"markers": "python_version >= '3.7'",
"version": "==1.2.0" "version": "==1.2.0"
}, },
"zope.component": { "zope.component": {
@@ -1766,11 +1900,11 @@
}, },
"zope.event": { "zope.event": {
"hashes": [ "hashes": [
"sha256:4ab47faac13163ca3c5d6d8a5595212e14770322e95c338d955e3688ba19082a", "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42",
"sha256:8cca6074a2bbca140d32704694e74098d5621f2c75ea6e3a9458240a3b5382a9" "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.5.1" "version": "==4.5.0"
}, },
"zope.hookable": { "zope.hookable": {
"hashes": [ "hashes": [

View File

@@ -218,8 +218,6 @@ TIME_ZONE = 'Europe/London'
FORMAT_MODULE_PATH = 'PyRIGS.formats' FORMAT_MODULE_PATH = 'PyRIGS.formats'
USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
@@ -264,3 +262,10 @@ TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk' AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
SECURE_HSTS_SECONDS = 3600
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = env('SESSION_COOKIE_SECURE_ENABLED', True)
CSRF_COOKIE_SECURE = env('CSRF_COOKIE_SECURE_ENABLED', True)
SECURE_HSTS_PRELOAD = True

View File

@@ -63,7 +63,7 @@ def screenshot_failure(func):
if not pathlib.Path("screenshots").is_dir(): if not pathlib.Path("screenshots").is_dir():
os.mkdir("screenshots") os.mkdir("screenshots")
self.driver.save_screenshot(screenshot_file) self.driver.save_screenshot(screenshot_file)
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr) print(f"Error in test {screenshot_name} is at path {screenshot_file}", file=sys.stderr)
raise e raise e
return wrapper_func return wrapper_func

View File

@@ -59,8 +59,8 @@ class TestSampleDataGenerator(TestCase):
assert Asset.objects.all().count() > 50 assert Asset.objects.all().count() > 50
assert Event.objects.all().count() > 100 assert Event.objects.all().count() > 100
call_command('deleteSampleData') call_command('deleteSampleData')
assert Asset.objects.all().count() == 0 assert not Asset.objects.all().exists()
assert Event.objects.all().count() == 0 assert not Event.objects.all().exists()
@override_settings(DEBUG=True) @override_settings(DEBUG=True)
@@ -76,9 +76,9 @@ def test_unauthenticated(client): # Nothing should be available to the unauthen
assertTemplateUsed(response, 'login_redirect.html') assertTemplateUsed(response, 'login_redirect.html')
else: else:
if "embed" in str(url): if "embed" in str(url):
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url) expected_url = f"{reverse('login_embed')}?next={request_url}"
else: else:
expected_url = "{0}?next={1}".format(reverse('login'), request_url) expected_url = f"{reverse('login')}?next={request_url}"
assertRedirects(response, expected_url) assertRedirects(response, expected_url)
call_command('deleteSampleData') call_command('deleteSampleData')

View File

@@ -48,7 +48,7 @@ class Index(generic.TemplateView): # Displays the current rig count along with
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['rig_count'] = models.Event.objects.rig_count() context['rig_count'] = models.Event.objects.rig_count()
context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now()) context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now()).exclude(status=models.Event.CANCELLED).filter(is_rig=True, dry_hire=False)
return context return context
@@ -134,11 +134,15 @@ class SecureAPIRequest(generic.View):
results = [] results = []
query = reduce(operator.and_, queries) query = reduce(operator.and_, queries)
objects = self.models[model].objects.filter(query) objects = self.models[model].objects.filter(query)
# Returning unactivated or unapproved users when they are elsewhere filtered out of the default queryset leads to some *very* unexpected results
if model == "profile":
objects = objects.filter(is_active=True, is_approved=True)
for o in objects: for o in objects:
name = o.display_name if hasattr(o, 'display_name') else o.name
data = { data = {
'pk': o.pk, 'pk': o.pk,
'value': o.pk, 'value': o.pk,
'text': o.name, 'text': name,
} }
try: # See if there is a valid update URL try: # See if there is a valid update URL
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk}) data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
@@ -183,7 +187,7 @@ class ModalURLMixin:
url = reverse_lazy('closemodal') url = reverse_lazy('closemodal')
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk})) update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object])) messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'") messages.info(self.request, f"modalobject[0]['update_url']='{update_url}'")
else: else:
url = reverse_lazy(detail, kwargs={ url = reverse_lazy(detail, kwargs={
'pk': self.object.pk, 'pk': self.object.pk,

View File

@@ -11,8 +11,9 @@ For setup information and other such helpful stuff check the [Wiki](https://gith
- PyRIGS: Base app, stores 'global' information - PyRIGS: Base app, stores 'global' information
- RIGS: Rigboard stuff - event calendar etc - RIGS: Rigboard stuff - event calendar etc
- assets: Database of our kit, testing data etc - assets: Database of our kit, testing data etc
- training: Logs in-house training within various "departments" (sound, lighting etc).
- versioning: Our custom logic built on top of django-reversion. Semi-modular. - versioning: Our custom logic built on top of django-reversion. Semi-modular.
- users: Our custom logic for registration and profiles. Semi-modular. - users: Our custom logic for registration and profiles. Semi-modular.
- training: SoonTM
[![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com)

View File

@@ -154,8 +154,9 @@ class AssociateAdmin(VersionAdmin):
@admin.register(models.Profile) @admin.register(models.Profile)
class ProfileAdmin(UserAdmin, AssociateAdmin): class ProfileAdmin(UserAdmin, AssociateAdmin):
list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events') list_display = ('username', 'name', 'is_approved', 'is_superuser', 'is_supervisor', 'number_of_events', 'last_login')
list_display_links = ['username'] list_display_links = ['username']
list_filter = UserAdmin.list_filter + ('is_approved',)
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (None, {'fields': ('username', 'password')}),
(_('Personal info'), { (_('Personal info'), {

View File

@@ -121,7 +121,7 @@ class EventForm(forms.ModelForm):
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date', fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic', 'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status', 'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
'purchase_order', 'collector'] 'purchase_order', 'collector', 'forum_url']
class BaseClientEventAuthorisationForm(forms.ModelForm): class BaseClientEventAuthorisationForm(forms.ModelForm):
@@ -131,7 +131,7 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
def clean(self): def clean(self):
if self.cleaned_data.get('amount') != self.instance.event.total: if self.cleaned_data.get('amount') != self.instance.event.total:
self.add_error('amount', 'The amount authorised must equal the total for the event (inc VAT).') self.add_error('amount', 'The amount authorised must equal the total for the event (inc VAT).')
return super(BaseClientEventAuthorisationForm, self).clean() return super().clean()
class Meta: class Meta:
abstract = True abstract = True
@@ -179,7 +179,7 @@ class EventRiskAssessmentForm(forms.ModelForm):
unexpected_values.append(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>") unexpected_values.append(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>")
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'): if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
raise forms.ValidationError(f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.", code='unusual_answers') raise forms.ValidationError(f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.", code='unusual_answers')
return super(EventRiskAssessmentForm, self).clean() return super().clean()
class Meta: class Meta:
model = models.RiskAssessment model = models.RiskAssessment
@@ -195,8 +195,6 @@ class EventChecklistForm(forms.ModelForm):
if field.__class__ == forms.NullBooleanField: if field.__class__ == forms.NullBooleanField:
# Only display yes/no to user, the 'none' is only ever set in the background # Only display yes/no to user, the 'none' is only ever set in the background
field.widget = forms.CheckboxInput() field.widget = forms.CheckboxInput()
# Parsed from incoming form data by clean, then saved into models when the form is saved
items = {}
related_models = { related_models = {
'venue': models.Venue, 'venue': models.Venue,
@@ -216,6 +214,11 @@ class PowerTestRecordForm(forms.ModelForm):
# Only display yes/no to user, the 'none' is only ever set in the background # Only display yes/no to user, the 'none' is only ever set in the background
field.widget = forms.CheckboxInput() field.widget = forms.CheckboxInput()
related_models = {
'venue': models.Venue,
'power_mic': models.Profile,
}
class Meta: class Meta:
model = models.PowerTestRecord model = models.PowerTestRecord
fields = '__all__' fields = '__all__'

View File

@@ -10,7 +10,7 @@ def migrate_old_data(apps, schema_editor):
PowerTestRecord = apps.get_model('RIGS', 'PowerTestRecord') PowerTestRecord = apps.get_model('RIGS', 'PowerTestRecord')
for ec in EventChecklist.objects.all(): for ec in EventChecklist.objects.all():
# New highscore for the most pythonic BS I've ever written. # New highscore for the most pythonic BS I've ever written.
PowerTestRecord.objects.create(event=ec.event, **{i.name:getattr(ec, i.attname) for i in PowerTestRecord._meta.get_fields() if not (i.is_relation or i.auto_created)}) PowerTestRecord.objects.create(event=ec.event, venue=ec.venue, reviewed_by=ec.reviewed_by, **{i.name:getattr(ec, i.attname) for i in PowerTestRecord._meta.get_fields() if not (i.is_relation or i.auto_created or i.name == "notes")})
def revert(apps, schema_editor): def revert(apps, schema_editor):

View File

@@ -3,6 +3,7 @@
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.core.exceptions import ObjectDoesNotExist
def migrate_old_data(apps, schema_editor): def migrate_old_data(apps, schema_editor):
@@ -10,8 +11,10 @@ def migrate_old_data(apps, schema_editor):
EventCheckIn = apps.get_model('RIGS', 'EventCheckIn') EventCheckIn = apps.get_model('RIGS', 'EventCheckIn')
for ec in EventChecklist.objects.all(): for ec in EventChecklist.objects.all():
for crew in ec.crew.all(): for crew in ec.crew.all():
vehicle = ec.vehicles.get(driver=crew.crewmember) or None try:
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end, vehicle=vehicle.vehicle) EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end, vehicle=ec.vehicles.get(driver=crew.crewmember).vehicle)
except ObjectDoesNotExist:
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end)
def revert(apps, schema_editor): def revert(apps, schema_editor):

View File

@@ -0,0 +1,53 @@
# Generated by Django 3.2.19 on 2023-05-29 10:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0048_auto_20230518_1256'),
]
operations = [
migrations.AlterField(
model_name='powertestrecord',
name='fd_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
name='fd_pssc',
field=models.IntegerField(blank=True, help_text='Prospective Short Circuit Current / A', null=True, verbose_name='PSCC'),
),
migrations.AlterField(
model_name='powertestrecord',
name='w1_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
name='w1_voltage',
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
),
migrations.AlterField(
model_name='powertestrecord',
name='w2_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
name='w2_voltage',
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
),
migrations.AlterField(
model_name='powertestrecord',
name='w3_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
name='w3_voltage',
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.19 on 2023-06-27 11:28
import RIGS.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0049_auto_20230529_1123'),
]
operations = [
migrations.AddField(
model_name='event',
name='forum_url',
field=models.URLField(blank=True, default='', validators=[RIGS.models.validate_forum_url]),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.19 on 2023-07-09 21:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0050_event_forum_url'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='method',
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('T', 'TEC Adjustment')], default='', max_length=2),
),
]

View File

@@ -76,7 +76,8 @@ class Profile(AbstractUser):
@classmethod @classmethod
def users_awaiting_approval_count(cls): def users_awaiting_approval_count(cls):
return Profile.objects.filter(models.Q(is_approved=False)).count() # last_login = None ensures we only pick up genuinely new users, not those that have been deactivated for inactivity
return Profile.objects.filter(is_approved=False, last_login=None).count()
def __str__(self): def __str__(self):
return self.name return self.name
@@ -308,6 +309,14 @@ class EventManager(models.Manager):
return qs return qs
def validate_forum_url(value):
if not value:
return # Required error is done the field
obj = urlparse(value)
if obj.hostname not in ('forum.nottinghamtec.co.uk'):
raise ValidationError('URL must point to a location on the TEC Forum')
@reversion.register(follow=['items']) @reversion.register(follow=['items'])
class Event(models.Model, RevisionMixin): class Event(models.Model, RevisionMixin):
# Done to make it much nicer on the database # Done to make it much nicer on the database
@@ -357,6 +366,8 @@ class Event(models.Model, RevisionMixin):
auth_request_at = models.DateTimeField(null=True, blank=True) auth_request_at = models.DateTimeField(null=True, blank=True)
auth_request_to = models.EmailField(blank=True, default='') auth_request_to = models.EmailField(blank=True, default='')
forum_url = models.URLField(default='', blank=True, validators=[validate_forum_url])
@property @property
def display_id(self): def display_id(self):
if self.pk: if self.pk:
@@ -497,7 +508,7 @@ class Event(models.Model, RevisionMixin):
earliest = datetime.datetime.combine(self.start_date, datetime.time(00, 00)) earliest = datetime.datetime.combine(self.start_date, datetime.time(00, 00))
tz = pytz.timezone(settings.TIME_ZONE) tz = pytz.timezone(settings.TIME_ZONE)
earliest = tz.localize(earliest) earliest = tz.localize(earliest)
return not self.dry_hire and earliest <= timezone.now() return not self.dry_hire and not self.status == Event.CANCELLED and earliest <= timezone.now()
objects = EventManager() objects = EventManager()
@@ -505,7 +516,7 @@ class Event(models.Model, RevisionMixin):
return reverse('event_detail', kwargs={'pk': self.pk}) return reverse('event_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return f"{self.display_id}: {self.name}" return f"{self.display_id} | {self.name}"
def clean(self): def clean(self):
errdict = {} errdict = {}
@@ -677,13 +688,11 @@ class Payment(models.Model, RevisionMixin):
CASH = 'C' CASH = 'C'
INTERNAL = 'I' INTERNAL = 'I'
EXTERNAL = 'E' EXTERNAL = 'E'
SUCORE = 'SU'
ADJUSTMENT = 'T' ADJUSTMENT = 'T'
METHODS = ( METHODS = (
(CASH, 'Cash'), (CASH, 'Cash'),
(INTERNAL, 'Internal'), (INTERNAL, 'Internal'),
(EXTERNAL, 'External'), (EXTERNAL, 'External'),
(SUCORE, 'SU Core'),
(ADJUSTMENT, 'TEC Adjustment'), (ADJUSTMENT, 'TEC Adjustment'),
) )
@@ -875,6 +884,9 @@ class EventChecklist(ReviewableModel, RevisionMixin):
@reversion.register @reversion.register
class PowerTestRecord(ReviewableModel, RevisionMixin): class PowerTestRecord(ReviewableModel, RevisionMixin):
earth_fault_text = "Earth Fault Loop Impedance (Z<small>S</small>) / Ω"
pssc_text = "Prospective Short Circuit Current / A"
event = models.ForeignKey('Event', related_name='power_tests', on_delete=models.CASCADE) event = models.ForeignKey('Event', related_name='power_tests', on_delete=models.CASCADE)
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists', 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?") verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?")
@@ -896,21 +908,21 @@ class PowerTestRecord(ReviewableModel, RevisionMixin):
fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - 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_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_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_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current") fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text=pssc_text)
# Worst case points # Worst case points
w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description") 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_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage") w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V")
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>)") w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description") 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_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage") w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V")
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>)") w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description") 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_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage") w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V")
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>)") w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</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>") public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
@@ -924,6 +936,9 @@ class PowerTestRecord(ReviewableModel, RevisionMixin):
def __str__(self): def __str__(self):
return f"{self.pk} - {self.event}" return f"{self.pk} - {self.event}"
def get_absolute_url(self):
return reverse('pt_detail', kwargs={'pk': self.pk})
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return str(self.event) return str(self.event)

View File

@@ -209,7 +209,7 @@
<div class="col-sm-9 col-md-7 col-lg-8"> <div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}"> <select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %} {% if venue %}
<option value="{{form.venue.value}}" selected="selected" data-update_url="{% url 'venue_update' form.venue.value %}">{{ venue }}</option> <option value="{{venue.id}}" selected="selected" data-update_url="{% url 'venue_update' venue.id %}">{{ venue }}</option>
{% endif %} {% endif %}
</select> </select>
</div> </div>
@@ -231,7 +231,7 @@
<label for="{{ form.start_date.id_for_label }}" <label for="{{ form.start_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label> class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
<div class="col-sm-8"> <div class="col-sm-10">
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required"> <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" %} {% render_field form.start_date class+="form-control" %}
@@ -246,7 +246,7 @@
<label for="{{ form.end_date.id_for_label }}" <label for="{{ form.end_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label> class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
<div class="col-sm-8"> <div class="col-sm-10">
<div class="row"> <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"> <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" %} {% render_field form.end_date class+="form-control" %}
@@ -334,12 +334,26 @@
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)"> <div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
<label for="{{ form.purchase_order.id_for_label }}" <label for="{{ form.purchase_order.id_for_label }}"
class="col-sm-4 col-fitem_tableorm-label">{{ form.purchase_order.label }}</label> class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label>
<div class="col-sm-8"> <div class="col-sm-8">
{% render_field form.purchase_order class+="form-control" %} {% render_field form.purchase_order class+="form-control" %}
</div> </div>
</div> </div>
<div class="form-group" data-toggle="tooltip" title="The thread for this event on the TEC Forum">
<label for="{{ form.forum_url.id_for_label }}"
class="col-sm-4 col-form-label">Forum Thread</label>
<div class="col-sm-12">
<p class="small mb-0">Paste URL</p>
{% render_field form.forum_url class+="form-control" %}
{% if object.pk %}
<p class="small mb-0">or</p>
<a href="{% url 'event_thread' object.pk %}" class="btn btn-primary" title="Create Forum Thread" target="_blank">
<span class="fas fa-plus"></span> <span class="hidden-xs">Create Forum Thread</span></a>
{% endif %}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -9,6 +9,8 @@
<div class="col-12 text-right my-3"> <div class="col-12 text-right my-3">
{% button 'edit' url='ec_edit' pk=object.pk %} {% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %} {% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
<a href="{% url 'event_pt' object.event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create Power Test</span></a>
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %} {% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div> </div>
</div> </div>
@@ -71,6 +73,8 @@
<div class="col-12 text-right"> <div class="col-12 text-right">
{% button 'edit' url='ec_edit' pk=object.pk %} {% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.pk text="Event" %} {% button 'view' url='event_detail' pk=object.pk text="Event" %}
<a href="{% url 'event_pt' object.event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create Power Test</span></a>
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %} {% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div> </div>
<div class="col-12 text-right"> <div class="col-12 text-right">

View File

@@ -59,8 +59,6 @@
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}"> <select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %} {% if venue %}
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option> <option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
{% elif event.venue %}
<option value="{{event.venue.pk}}" selected="selected">{{ event.venue.name }}</option>
{% endif %} {% endif %}
</select> </select>
</div> </div>

View File

@@ -86,7 +86,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small></th> <th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small> / V</th>
<th>{{ object|help_text:'fd_voltage_l1' }}</th> <th>{{ object|help_text:'fd_voltage_l1' }}</th>
<th>{{ object|help_text:'fd_voltage_l2' }}</th> <th>{{ object|help_text:'fd_voltage_l2' }}</th>
<th>{{ object|help_text:'fd_voltage_l3' }}</th> <th>{{ object|help_text:'fd_voltage_l3' }}</th>
@@ -165,11 +165,11 @@
</div> </div>
</div> </div>
<div class="col-12 text-right"> <div class="col-12 text-right">
{% button 'edit' url='ec_edit' pk=object.pk %} {% button 'edit' url='pt_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.pk text="Event" %} {% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %} {% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %}
</div> </div>
<div class="col-12 text-right"> <div class="col-12 text-right">
{% include 'partials/last_edited.html' with target="eventchecklist_history" %} {% include 'partials/last_edited.html' with target="powertestrecord_history" %}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -51,22 +51,18 @@
<div class="form-group form-row" id="{{ form.power_mic.id_for_label }}-group"> <div class="form-group form-row" id="{{ form.power_mic.id_for_label }}-group">
<label for="{{ form.power_mic.id_for_label }}" <label for="{{ form.power_mic.id_for_label }}"
class="col-4 col-form-label">{{ form.power_mic.help_text }}</label> class="col-4 col-form-label">{{ form.power_mic.help_text }}</label>
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true"> <select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true">
{% if power_mic %} {% if power_mic %}
<option value="{{power_mic.pk}}" selected="selected">{{ power_mic.name }}</option> <option value="{{power_mic.pk}}" selected="selected">{{ power_mic.name }}</option>
{% elif event.riskassessment.power_mic %}
<option value="{{event.riskassessment.power_mic.pk}}" selected="selected">{{ event.riskassessment.power_mic.name }}</option>
{% endif %} {% endif %}
</select> </select>
</div> </div>
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group"> <div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
<label for="{{ form.venue.id_for_label }}" <label for="{{ form.venue.id_for_label }}"
class="col-4 col-form-label">{{ form.venue.label }}</label> class="col-4 col-form-label">{{ form.venue.label }}</label>
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}"> <select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %} {% if venue %}
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option> <option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
{% elif event.venue %}
<option value="{{event.venue.pk}}" selected="selected">{{ event.venue.name }}</option>
{% endif %} {% endif %}
</select> </select>
</div> </div>
@@ -119,7 +115,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small></th> <th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small> / V</th>
<th class="text-center">{{ form.fd_voltage_l1.help_text }}</th> <th class="text-center">{{ form.fd_voltage_l1.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l2.help_text }}</th> <th class="text-center">{{ form.fd_voltage_l2.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l3.help_text }}</th> <th class="text-center">{{ form.fd_voltage_l3.help_text }}</th>

View File

@@ -25,7 +25,7 @@
}); });
$('input[type=radio][name=outside], input[type=radio][name=generators], input[type=radio][name=other_companies_power], input[type=radio][name=nonstandard_equipment_power], input[type=radio][name=multiple_electrical_environments]').change(function() { $('input[type=radio][name=outside], input[type=radio][name=generators], input[type=radio][name=other_companies_power], input[type=radio][name=nonstandard_equipment_power], input[type=radio][name=multiple_electrical_environments]').change(function() {
$('#{{ form.power_notes.id_for_label }}').prop('required', parseBool(this.value)); $('#{{ form.power_notes.id_for_label }}').prop('required', parseBool(this.value));
$('#{{ form.power_plan.id_for_label }}').prop('required', parseBool(this.value)); //$('#{{ form.power_plan.id_for_label }}').prop('required', parseBool(this.value));
}); });
$('input[type=radio][name=special_structures]').change(function() { $('input[type=radio][name=special_structures]').change(function() {
$('#{{ form.persons_responsible_structures.id_for_label }}').prop('hidden', !parseBool(this.value)).prop('required', parseBool(this.value)); $('#{{ form.persons_responsible_structures.id_for_label }}').prop('hidden', !parseBool(this.value)).prop('required', parseBool(this.value));

View File

@@ -77,6 +77,15 @@
<dt class="col-sm-6">PO</dt> <dt class="col-sm-6">PO</dt>
<dd class="col-sm-6">{{ object.purchase_order }}</dd> <dd class="col-sm-6">{{ object.purchase_order }}</dd>
{% endif %} {% endif %}
<dt class="col-6">Forum Thread</dt>
{% if object.forum_url %}
<dd class="col-6"><a href="{{object.forum_url}}">{{object.forum_url}}</a></dd>
{% else %}
<a href="{% url 'event_thread' object.pk %}" class="btn btn-primary" title="Create Forum Thread" target="_blank"><span
class="fas fa-plus"></span> <span
class="hidden-xs">Create Forum Thread</span></a>
{% endif %}
</dl> </dl>
</div> </div>
</div> </div>

View File

@@ -15,7 +15,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if not event.dry_hire %} {% if not event.dry_hire %}
{% if event.has_checklist %} {% if event.riskassessment %}
<a href="{{ event.riskassessment.get_absolute_url }}"><span class="badge badge-success">RA: <span class="fas fa-check{% if event.riskassessment.reviewed_by %}-double{%endif%}"></span></a> <a href="{{ event.riskassessment.get_absolute_url }}"><span class="badge badge-success">RA: <span class="fas fa-check{% if event.riskassessment.reviewed_by %}-double{%endif%}"></span></a>
{% else %} {% else %}
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span> <span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>

View File

@@ -29,7 +29,15 @@
</div> </div>
<div class="row pt-3"> <div class="row pt-3">
<label class="col-sm-4 col-form-label" <label class="col-sm-4 col-form-label"
for="{{ form.method.id_for_label }}">{{ form.method.label }}</label> for="{{ form.method.id_for_label }}">{{ form.method.label }}
<span class="fas fa-info-circle text-info" data-toggle="collapse" data-target="#collapse" aria-expanded="false" aria-controls="collapse"></span>
<ul class="collapse" id="collapse">
<li>Cash - Self Explanatory</li>
<li>Internal - Transfers within the Students' Union only</li>
<li>External - All other transfers (<em>including</em> the University)</li>
<li>TEC Adjustment - Manual corrections</li>
</ul>
</label>
<div class="col-sm-8"> <div class="col-sm-8">
{% render_field form.method class+="form-control" %} {% render_field form.method class+="form-control" %}
</div> </div>

View File

@@ -171,7 +171,7 @@ def title_spaced(string):
@register.filter(needs_autoescape=True) @register.filter(needs_autoescape=True)
def namewithnotes(obj, url, autoescape=True): def namewithnotes(obj, url, autoescape=True):
if hasattr(obj, 'notes') and obj.notes is not None and len(obj.notes) > 0: if hasattr(obj, 'notes') and obj.notes is not None and len(obj.notes) > 0:
return mark_safe(obj.name + " <a href='{}'><span class='fas fa-sticky-note'></span></a>".format(reverse(url, kwargs={'pk': obj.pk}))) return mark_safe(obj.name + f" <a href='{reverse(url, kwargs={'pk': obj.pk})}'><span class='fas fa-sticky-note'></span></a>")
else: else:
return obj.name return obj.name
@@ -183,7 +183,7 @@ def linkornone(target, namespace=None, autoescape=True):
link = namespace + "://" + target link = namespace + "://" + target
else: else:
link = target link = target
return mark_safe("<a href='{}' target='_blank'><span class='overflow-ellipsis'>{}</span></a>".format(link, str(target))) return mark_safe(f"<a href='{link}' target='_blank'><span class='overflow-ellipsis'>{target}</span></a>")
else: else:
return "None" return "None"

View File

@@ -114,7 +114,7 @@ class CreateEvent(FormPage):
} }
def select_event_type(self, type_name): def select_event_type(self, type_name):
self.find_element(By.XPATH, '//button[.="{}"]'.format(type_name)).click() self.find_element(By.XPATH, f'//button[.="{type_name}"]').click()
def item_row(self, ID): def item_row(self, ID):
return rigs_regions.ItemRow(self, self.find_element(By.ID, "item-" + ID)) return rigs_regions.ItemRow(self, self.find_element(By.ID, "item-" + ID))

View File

@@ -259,7 +259,7 @@ class TestPrintPaperwork(TestCase):
def test_login_redirect(client, django_user_model): def test_login_redirect(client, django_user_model):
request_url = reverse('event_embed', kwargs={'pk': 1}) request_url = reverse('event_embed', kwargs={'pk': 1})
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url) expected_url = f"{reverse('login_embed')}?next={request_url}"
# Request the page and check it redirects # Request the page and check it redirects
response = client.get(request_url, follow=True) response = client.get(request_url, follow=True)
@@ -372,7 +372,8 @@ def test_ra_redirect(admin_client, admin_user, ra):
class TestMarkdownTemplateTags(TestCase): class TestMarkdownTemplateTags(TestCase):
markdown = open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md")).read() with open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md"), encoding="utf-8") as f:
markdown = f.read()
def test_html_safe(self): def test_html_safe(self):
html = markdown_filter(self.markdown) html = markdown_filter(self.markdown)

View File

@@ -110,6 +110,9 @@ urlpatterns = [
path('event/<int:pk>/checkin/add/', login_required(views.EventCheckInOverride.as_view()), path('event/<int:pk>/checkin/add/', login_required(views.EventCheckInOverride.as_view()),
name='event_checkin_override'), name='event_checkin_override'),
path('event/<int:pk>/thread/', permission_required_with_403('RIGS.change_event')(views.CreateForumThread.as_view()), name='event_thread'),
path('event/webhook/', views.RecieveForumWebhook.as_view(), name='webhook_recieve'),
# Finance # Finance
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()), path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
name='invoice_list'), name='invoice_list'),

View File

@@ -13,31 +13,24 @@ from django.shortcuts import redirect
class HSCreateView(generic.CreateView): class HSCreateView(generic.CreateView):
def get_form(self, **kwargs):
form = super().get_form(**kwargs)
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
form.instance.event = event
return form
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
epk = self.kwargs.get('pk') event = models.Event.objects.get(pk=self.kwargs.get('pk'))
event = models.Event.objects.get(pk=epk)
context['event'] = event context['event'] = event
context['page_title'] = f'Create {self.model.__name__} for Event {event.display_id}' context['page_title'] = f'Create {self.model.__name__} for Event {event.display_id}'
get_related(context['form'], context)
return context return context
class MarkReviewed(generic.View): class MarkReviewed(generic.RedirectView):
def get(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
obj = apps.get_model('RIGS', kwargs.get('model')).objects.get(pk=kwargs.get('pk')) obj = apps.get_model('RIGS', kwargs.get('model')).objects.get(pk=kwargs.get('pk'))
with reversion.create_revision(): with reversion.create_revision():
reversion.set_user(self.request.user) reversion.set_user(self.request.user)
obj.reviewed_by = self.request.user obj.reviewed_by = self.request.user
obj.reviewed_at = timezone.now() obj.reviewed_at = timezone.now()
obj.save() obj.save()
return HttpResponseRedirect(reverse('hs_list')) return self.request.META.get('HTTP_REFERER', reverse('hs_list'))
class EventRiskAssessmentCreate(HSCreateView): class EventRiskAssessmentCreate(HSCreateView):
@@ -55,7 +48,7 @@ class EventRiskAssessmentCreate(HSCreateView):
if ra is not None: if ra is not None:
return HttpResponseRedirect(reverse('ra_edit', kwargs={'pk': ra.pk})) return HttpResponseRedirect(reverse('ra_edit', kwargs={'pk': ra.pk}))
return super(EventRiskAssessmentCreate, self).get(self) return super().get(self)
def get_success_url(self): def get_success_url(self):
return reverse('ra_detail', kwargs={'pk': self.object.pk}) return reverse('ra_detail', kwargs={'pk': self.object.pk})
@@ -74,7 +67,7 @@ class EventRiskAssessmentEdit(generic.UpdateView):
return reverse('ra_detail', kwargs={'pk': self.object.pk}) return reverse('ra_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventRiskAssessmentEdit, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
rpk = self.kwargs.get('pk') rpk = self.kwargs.get('pk')
ra = models.RiskAssessment.objects.get(pk=rpk) ra = models.RiskAssessment.objects.get(pk=rpk)
context['event'] = ra.event context['event'] = ra.event
@@ -89,7 +82,7 @@ class EventRiskAssessmentDetail(generic.DetailView):
template_name = 'hs/risk_assessment_detail.html' template_name = 'hs/risk_assessment_detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventRiskAssessmentDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = f"Risk Assessment for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>" context['page_title'] = f"Risk Assessment for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
return context return context
@@ -99,7 +92,7 @@ class EventChecklistDetail(generic.DetailView):
template_name = 'hs/event_checklist_detail.html' template_name = 'hs/event_checklist_detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventChecklistDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = f"Event Checklist for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>" context['page_title'] = f"Event Checklist for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
return context return context
@@ -117,7 +110,7 @@ class EventChecklistEdit(generic.UpdateView):
return reverse('ec_detail', kwargs={'pk': self.object.pk}) return reverse('ec_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventChecklistEdit, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
pk = self.kwargs.get('pk') pk = self.kwargs.get('pk')
ec = models.EventChecklist.objects.get(pk=pk) ec = models.EventChecklist.objects.get(pk=pk)
context['event'] = ec.event context['event'] = ec.event
@@ -136,19 +129,22 @@ class EventChecklistCreate(HSCreateView):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
epk = kwargs.get('pk') epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk) event = models.Event.objects.get(pk=epk)
# Check if RA exists # Check if RA exists
ra = models.RiskAssessment.objects.filter(event=event).first() ra = models.RiskAssessment.objects.filter(event=event).first()
if ra is None: if ra is None:
messages.error(self.request, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.') messages.error(self.request, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.')
return HttpResponseRedirect(reverse('event_ra', kwargs={'pk': epk})) return HttpResponseRedirect(reverse('event_ra', kwargs={'pk': epk}))
return super().get(self)
return super(EventChecklistCreate, self).get(self)
def get_success_url(self): def get_success_url(self):
return reverse('ec_detail', kwargs={'pk': self.object.pk}) return reverse('ec_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if context['event'].venue:
context['venue'] = context['event'].venue
return context
class PowerTestDetail(generic.DetailView): class PowerTestDetail(generic.DetailView):
model = models.PowerTestRecord model = models.PowerTestRecord
@@ -170,7 +166,7 @@ class PowerTestEdit(generic.UpdateView):
ec.reviewed_by = None ec.reviewed_by = None
ec.reviewed_at = None ec.reviewed_at = None
ec.save() ec.save()
return reverse('ec_detail', kwargs={'pk': self.object.pk}) return reverse('pt_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@@ -179,7 +175,7 @@ class PowerTestEdit(generic.UpdateView):
context['event'] = ec.event context['event'] = ec.event
context['edit'] = True context['edit'] = True
context['page_title'] = f'Edit Power Test Record for Event {ec.event.display_id}' context['page_title'] = f'Edit Power Test Record for Event {ec.event.display_id}'
# get_related(context['form'], context) get_related(context['form'], context)
return context return context
@@ -191,7 +187,6 @@ class PowerTestCreate(HSCreateView):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
epk = kwargs.get('pk') epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk) event = models.Event.objects.get(pk=epk)
# Check if RA exists # Check if RA exists
ra = models.RiskAssessment.objects.filter(event=event).first() ra = models.RiskAssessment.objects.filter(event=event).first()
@@ -204,6 +199,14 @@ class PowerTestCreate(HSCreateView):
def get_success_url(self): def get_success_url(self):
return reverse('pt_detail', kwargs={'pk': self.object.pk}) return reverse('pt_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if context['event'].venue:
context['venue'] = context['event'].venue
if context['event'].riskassessment.power_mic:
context['power_mic'] = context['event'].riskassessment.power_mic
return context
class HSList(generic.ListView): class HSList(generic.ListView):
paginate_by = 20 paginate_by = 20
@@ -211,10 +214,10 @@ class HSList(generic.ListView):
template_name = 'hs/hs_list.html' template_name = 'hs/hs_list.html'
def get_queryset(self): def get_queryset(self):
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists') return models.Event.objects.all().exclude(status=models.Event.CANCELLED).exclude(dry_hire=True).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(HSList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = 'H&S Overview' context['page_title'] = 'H&S Overview'
return context return context

View File

@@ -3,6 +3,12 @@ import datetime
import re import re
import premailer import premailer
import simplejson import simplejson
import urllib
import hmac
import hashlib
from envparse import env
from bs4 import BeautifulSoup
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
@@ -19,6 +25,7 @@ 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
from django.views import generic from django.views import generic
from django.views.decorators.csrf import csrf_exempt
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
@@ -377,3 +384,41 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
context['to_name'] = self.request.GET.get('to_name', None) context['to_name'] = self.request.GET.get('to_name', None)
context['target'] = 'event_authorise_form_preview' context['target'] = 'event_authorise_form_preview'
return context return context
class CreateForumThread(generic.base.RedirectView):
permanent = False
def get_redirect_url(self, *args, **kwargs):
event = get_object_or_404(models.Event, pk=kwargs['pk'])
if event.forum_url:
return event.forum_url
params = {
'title': str(event),
'body': f'https://rigs.nottinghamtec.co.uk/event/{event.pk}',
'category': 'rig-info'
}
return f'https://forum.nottinghamtec.co.uk/new-topic?{urllib.parse.urlencode(params)}'
class RecieveForumWebhook(generic.View):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
computed = f"sha256={hmac.new(env('FORUM_WEBHOOK_SECRET').encode(), request.body, hashlib.sha256).hexdigest()}"
if not hmac.compare_digest(request.headers.get('X-Discourse-Event-Signature'), computed):
return HttpResponseForbidden('Invalid signature header')
# Check if this is the right kind of event. The webhook filters by category on the forum side
if request.headers.get('X-Discourse-Event') == "topic_created":
body = simplejson.loads(request.body.decode('utf-8'))
event_id = int(body['topic']['title'][1:6]) # find the ID, force convert it to an int to eliminate leading zeros
event = models.Event.objects.filter(pk=event_id).first()
if event:
event.forum_url = f"https://forum.nottinghamtec.co.uk/t/{body['topic']['slug']}"
event.save()
return HttpResponse(status=202)
return HttpResponse(status=204)

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.19 on 2023-05-24 22:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0027_asset_nickname'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='length',
field=models.DecimalField(blank=True, decimal_places=2, help_text='m', max_digits=10, null=True),
),
]

View File

@@ -135,7 +135,7 @@ class Asset(models.Model, RevisionMixin):
# Cable assets # Cable assets
is_cable = models.BooleanField(default=False) is_cable = models.BooleanField(default=False)
cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL) cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL)
length = models.DecimalField(decimal_places=1, max_digits=10, length = models.DecimalField(decimal_places=2, max_digits=10,
blank=True, null=True, help_text='m') blank=True, null=True, help_text='m')
csa = models.DecimalField(decimal_places=2, max_digits=10, csa = models.DecimalField(decimal_places=2, max_digits=10,
blank=True, null=True, help_text='mm²') blank=True, null=True, help_text='mm²')
@@ -192,5 +192,5 @@ class Asset(models.Model, RevisionMixin):
return str(self.asset_id) return str(self.asset_id)
@property @property
def name(self): def display_name(self):
return f"{self.display_id} | {self.description}" return f"{self.display_id} | {self.description}"

View File

@@ -35,6 +35,11 @@
function onAuditClick(assetID) { function onAuditClick(assetID) {
$('#' + assetID).remove(); $('#' + assetID).remove();
} }
$('#modal').on('hidden.bs.modal', function (e) {
searchbar = document.getElementById('id_q');
searchbar.value = "";
setTimeout(searchbar.focus(), 2000);
})
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -12,7 +12,6 @@
{{ block.super }} {{ block.super }}
<script src="{% static 'js/selects.js' %}"></script> <script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/easymde.min.js' %}"></script> <script src="{% static 'js/easymde.min.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
@@ -35,9 +34,10 @@
$(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))}); $(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))});
}); });
</script> </script>
<script src="{% static "js/tooltip.js" %}"></script>
<script> <script>
$(document).ready(function () { $(function () {
setupMDE('#id_comments'); $('[data-toggle="tooltip"]').tooltip();
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -12,7 +12,7 @@
</thead> </thead>
<tbody id="asset_table_body"> <tbody id="asset_table_body">
{% for item in object_list %} {% for item in object_list %}
<tr class="table-{{ item.status.display_class|default:'' }} assetRow"> <tr class="table-{{ item.status.display_class|default:'' }} assetRow" id="{{ item.asset_id }}">
<th scope="row" class="align-middle"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></th> <th scope="row" class="align-middle"><a class="assetID" href="{% url 'asset_detail' item.asset_id %}">{{ item.asset_id }}</a></th>
<td class="assetDesc"><span class="text-truncate d-inline-block align-middle">{{ item.description }}</span></td> <td class="assetDesc"><span class="text-truncate d-inline-block align-middle">{{ item.description }}</span></td>
<td class="assetCategory align-middle">{{ item.category }}</td> <td class="assetCategory align-middle">{{ item.category }}</td>

View File

@@ -34,7 +34,7 @@
<label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label> <label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label>
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"><span class="input-group-text">£</span></div> <div class="input-group-prepend"><span class="input-group-text">£</span></div>
{% render_field form.purchase_price|add_class:'form-control' value=object.purchase_price %} {% render_field form.purchase_price|add_class:'form-control'|set_data:"toggle:tooltip" value=object.purchase_price title="Ex. VAT" %}
</div> </div>
</div> </div>
@@ -42,7 +42,7 @@
<label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label> <label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label>
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"><span class="input-group-text">£</span></div> <div class="input-group-prepend"><span class="input-group-text">£</span></div>
{% render_field form.replacement_cost|add_class:'form-control' value=object.replacement_cost %} {% render_field form.replacement_cost|add_class:'form-control'|set_data:"toggle:tooltip" value=object.replacement_cost title="Ex. VAT" %}
</div> </div>
</div> </div>

View File

@@ -38,3 +38,17 @@ def test_asset(db, category, status):
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100) asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100)
yield asset yield asset
asset.delete() asset.delete()
@pytest.fixture
def test_status_2(db):
status = models.AssetStatus.objects.create(name="Lost", should_show=False)
yield status
status.delete()
@pytest.fixture
def test_asset_2(db, category, test_status_2):
asset, created = models.Asset.objects.get_or_create(asset_id="10", description="Working Mic", status=test_status_2, category=category, date_acquired=datetime.date(2001, 10, 20), replacement_cost=1000)
yield asset
asset.delete()

View File

@@ -77,7 +77,7 @@ class AssetForm(FormPage):
'description': (regions.TextBox, (By.ID, 'id_description')), 'description': (regions.TextBox, (By.ID, 'id_description')),
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')), 'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')), 'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')), 'comments': (regions.TextBox, (By.ID, 'id_comments')),
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')), 'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')), 'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')), 'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),

View File

@@ -1,5 +1,6 @@
import time import time
import datetime import datetime
import pytest
from django.utils import timezone from django.utils import timezone
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
@@ -53,45 +54,45 @@ class TestAssetList(AutoLoginTest):
self.assertEqual("10", asset_ids[2]) self.assertEqual("10", asset_ids[2])
self.assertEqual("C1", asset_ids[3]) self.assertEqual("C1", asset_ids[3])
def test_search(self):
self.page.set_query("10")
self.page.search()
self.assertTrue(len(self.page.assets) == 1)
self.assertEqual("Working Mic", self.page.assets[0].description)
self.assertEqual("10", self.page.assets[0].id)
self.page.set_query("light") @pytest.mark.xfail(reason="Fails on CI for unknown reason", raises=AssertionError)
self.page.search() def test_search(logged_in_browser, admin_user, live_server, test_asset, test_asset_2, category, status, cable_type):
self.assertTrue(len(self.page.assets) == 1) page = pages.AssetList(logged_in_browser.driver, live_server.url).open()
self.assertEqual("A light", self.page.assets[0].description) page.set_query(test_asset.asset_id)
page.search()
assert len(page.assets) == 1
assert page.assets[0].description == test_asset.description
assert page.assets[0].id == test_asset.asset_id
self.page.set_query("Random string") page.set_query(test_asset.description)
self.page.search() page.search()
self.assertTrue(len(self.page.assets) == 0) assert len(page.assets) == 1
assert page.assets[0].description == test_asset.description
self.page.set_query("") page.set_query("Random string")
self.page.search() page.search()
# Only working stuff shown by default assert len(page.assets) == 0
self.assertTrue(len(self.page.assets) == 2)
self.page.status_selector.toggle() page.set_query("")
self.assertTrue(self.page.status_selector.is_open) page.search()
self.page.status_selector.select_all() # Only working stuff shown by default
self.page.status_selector.toggle() assert len(page.assets) == 1
self.assertFalse(self.page.status_selector.is_open)
self.page.filter()
self.assertTrue(len(self.page.assets) == 4)
self.page.category_selector.toggle() page.status_selector.toggle()
self.assertTrue(self.page.category_selector.is_open) assert page.status_selector.is_open
self.page.category_selector.set_option("Sound", True) page.status_selector.select_all()
self.page.category_selector.close() page.status_selector.toggle()
self.assertFalse(self.page.category_selector.is_open) assert not page.status_selector.is_open
self.page.filter() page.filter()
self.assertTrue(len(self.page.assets) == 2) assert len(page.assets) == 2
asset_ids = list(map(lambda x: x.id, self.page.assets))
self.assertEqual("1", asset_ids[0]) page.category_selector.toggle()
self.assertEqual("10", asset_ids[1]) assert page.category_selector.is_open
page.category_selector.set_option(category.name, True)
page.category_selector.close()
assert not page.category_selector.is_open
page.filter()
assert len(page.assets) == 2
def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type): def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type):

1295
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,7 @@
"jquery": "^3.6.0", "jquery": "^3.6.0",
"konami": "^1.6.3", "konami": "^1.6.3",
"moment": "^2.29.4", "moment": "^2.29.4",
"node-sass": "^7.0.3", "node-sass": "^9.0.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"postcss": "^8.4.5", "postcss": "^8.4.5",
"uglify-js": "^3.14.5" "uglify-js": "^3.14.5"

View File

@@ -6,11 +6,6 @@ function setupItemTable(items_json) {
newitem = -1; newitem = -1;
} }
function nl2br(str, is_xhtml) {
var breakTag = (is_xhtml || typeof is_xhtml === 'undefined') ? '<br />' : '<br>';
return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ breakTag +'$2');
}
function escapeHtml(str) { function escapeHtml(str) {
return $('<div/>').text(str).html(); return $('<div/>').text(str).html();
} }

View File

@@ -17,7 +17,6 @@
e.preventDefault(); e.preventDefault();
data = $(this).serialize(); data = $(this).serialize();
action = $(this).attr('action'); action = $(this).attr('action');
console.log(action)
$.post(action, data, function(resp) { $.post(action, data, function(resp) {
$('#modal').html(resp); $('#modal').html(resp);
}); });

View File

@@ -11,7 +11,7 @@
{% if now %} {% if now %}
<div class="col-sm-12 alert alert-primary rounded-0 mx-auto"> <div class="col-sm-12 alert alert-primary rounded-0 mx-auto">
{% for event in now %} {% for event in now %}
Event {{ event }} is happening now! <a href="{% url 'event_checkin' event.pk %}" class="btn btn-success btn-sm modal-href align-baseline {% if request.user.current_event %}disabled{%endif%}"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a><br/> Event {{ event }} is happening today! <a href="{% url 'event_checkin' event.pk %}" class="btn btn-success btn-sm modal-href align-baseline {% if request.user.current_event %}disabled{%endif%}"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a><br/>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -1,15 +1,15 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% load title_spaced from filters %} {% load title_spaced from filters %}
{% spaceless %} {% spaceless %}
<label for="{{ field.id_for_label }}" {% if col %}class="col-4 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label> {% if not nolabel %}<label for="{{ field.id_for_label }}" {% if col %}class="col-4 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>{%endif%}
{% if append or prepend %} {% if append or prepend %}
<div class="input-group {{col}}"> <div class="input-group {{col}} flex-nowrap">
{% if prepend %} {% if prepend %}
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text">{{ prepend }}</span> <span class="input-group-text">{{ prepend }}</span>
</div> </div>
{% endif %} {% endif %}
{% render_field field|add_class:'form-control' %} {% render_field field|add_class:'form-control' style=style %}
{% if append %} {% if append %}
<div class="input-group-append"> <div class="input-group-append">
<span class="input-group-text">{{ append }}</span> <span class="input-group-text">{{ append }}</span>
@@ -17,6 +17,6 @@
{% endif %} {% endif %}
</div> </div>
{% else %} {% else %}
{% render_field field|add_class:'form-control' class+=col %} {% render_field field|add_class:'form-control' class+=col style=style %}
{% endif %} {% endif %}
{% endspaceless %} {% endspaceless %}

View File

@@ -105,6 +105,10 @@ class TrainingItem(models.Model):
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}"
@property
def display_name(self):
return f"{self.display_id} | {self.name}"
@display_id.filter @display_id.filter
@classmethod @classmethod
def display_id(cls, lookup, value): def display_id(cls, lookup, value):
@@ -369,7 +373,7 @@ class TrainingLevelQualification(models.Model, RevisionMixin):
return str(self) return str(self)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('trainee_detail', kwargs={'pk': self.trainee.pk}) return reverse('trainee_detail', kwargs={'pk': self.trainee_id})
class Meta: class Meta:
unique_together = ["trainee", "level"] unique_together = ["trainee", "level"]

View File

@@ -78,11 +78,6 @@
</tr> </tr>
{% endfor %} {% endfor %}
<tr><th colspan="3" class="text-center">{{object}}</th></tr> <tr><th colspan="3" class="text-center">{{object}}</th></tr>
<tr>
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -8,10 +8,10 @@
<p>Please Note:</p> <p>Please Note:</p>
<ul> <ul>
<li>Technical Assistant status is automatically valid when the item requirements are met.</li> <li>Technical Assistant status is automatically valid when the item requirements are met.</li>
<li>Technician status is also automatic, but notification of status should be made at the next general meeting, at which point 'approval' should be granted on the system.</li> <li>Technician status is also automatic. Notification of completion should be made at the next general meeting.</li>
<li>Supervisor status is <em>not automatically valid</em> and until signed off at a general meeting, does not count.</li> <li>Supervisor status is <em>not automatically valid</em> and until signed off at a general meeting, does not count.</li>
</ul> </ul>
<sup>Correct as of 3rd September 2021, check the Training Policy.</sup> <sup>Correct as of 24th May 2023, check the Training Policy.</sup>
</div> </div>
{% endif %} {% endif %}
{% for level in object_list %} {% for level in object_list %}

View File

@@ -43,7 +43,7 @@ def confirm_button(user, trainee, level):
if level.user_has_requirements(trainee): if level.user_has_requirements(trainee):
string = "<span class='badge badge-warning p-2'>Awaiting Confirmation</span>" string = "<span class='badge badge-warning p-2'>Awaiting Confirmation</span>"
if models.Trainee.objects.get(pk=user.pk).is_supervisor or user.has_perm('training.add_traininglevelqualification'): if models.Trainee.objects.get(pk=user.pk).is_supervisor or user.has_perm('training.add_traininglevelqualification'):
string += "<a class='btn btn-info' href='{}'>Confirm</a>".format(reverse('confirm_level', kwargs={'pk': trainee.pk, 'level_pk': level.pk})) string += f"<a class='btn btn-info' href='{reverse('confirm_level', kwargs={'pk': trainee.pk, 'level_pk': level.pk})}'>Confirm</a>"
return mark_safe(string) return mark_safe(string)
else: else:
return "" return ""

View File

@@ -44,7 +44,7 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
page.submit() page.submit()
assert page.success assert page.success
qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item) qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item)
assert qualification.supervisor.pk == supervisor.pk assert qualification.supervisor_id == supervisor.pk
assert qualification.date == date assert qualification.date == date
assert qualification.notes == "A note" assert qualification.notes == "A note"
assert qualification.depth == models.TrainingItemQualification.STARTED assert qualification.depth == models.TrainingItemQualification.STARTED

View File

@@ -29,7 +29,7 @@ def test_add_qualification_reversion(admin_client, trainee, training_item, super
assert response.status_code == 302 assert response.status_code == 302
qual = models.TrainingItemQualification.objects.last() qual = models.TrainingItemQualification.objects.last()
assert qual is not None assert qual is not None
assert training_item.pk == qual.item.pk assert training_item.pk == qual.item_id
# Ensure only one revision has been created # Ensure only one revision has been created
assert Revision.objects.count() == 1 assert Revision.objects.count() == 1
response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1}) response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1})

View File

@@ -265,5 +265,5 @@ class ItemQualifications(generic.ListView):
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["page_title"] = f"People Qualified In {self.object_list[0].item}" context["page_title"] = f"People Qualified In {models.TrainingItem.objects.get(pk=self.kwargs['pk'])}"
return context return context

View File

@@ -30,15 +30,15 @@ for app in [apps.get_app_config(label) for label in ("RIGS", "assets", "training
modelname = model.__name__.lower() modelname = model.__name__.lower()
if appname == 'rigboard': if appname == 'rigboard':
urlpatterns += [ urlpatterns += [
path('{}/<str:pk>/history/'.format(modelname), path(f'{modelname}/<str:pk>/history/',
permission_required_with_403('{}.change_{}'.format(app.label, modelname))( permission_required_with_403(f'{app.label}.change_{modelname}')(
views.VersionHistory.as_view()), views.VersionHistory.as_view()),
name='{}_history'.format(modelname), kwargs={'model': model, 'app': appname, }), name=f'{modelname}_history', kwargs={'model': model, 'app': appname, }),
] ]
else: else:
urlpatterns += [ urlpatterns += [
path('{}/{}/<str:pk>/history/'.format(appname, modelname), path(f'{appname}/{modelname}/<str:pk>/history/',
permission_required_with_403('{}.change_{}'.format(app.label, modelname))( permission_required_with_403(f'{app.label}.change_{modelname}')(
views.VersionHistory.as_view()), views.VersionHistory.as_view()),
name='{}_history'.format(modelname), kwargs={'model': model, 'app': appname, }), name=f'{modelname}_history', kwargs={'model': model, 'app': appname, }),
] ]

View File

@@ -1,3 +1,4 @@
import logging
from diff_match_patch import diff_match_patch from diff_match_patch import diff_match_patch
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@@ -147,9 +148,9 @@ class ModelComparison:
@cached_property @cached_property
def item_changes(self): def item_changes(self):
from RIGS.models import EventAuthorisation
from training.models import TrainingLevelQualification, TrainingItemQualification
if self.follow and self.version.object is not None: if self.follow and self.version.object is not None:
from RIGS.models import EventAuthorisation
from training.models import TrainingLevelQualification, TrainingItemQualification
item_type = ContentType.objects.get_for_model(self.version.object) item_type = ContentType.objects.get_for_model(self.version.object)
old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(TrainingItemQualification)) \ old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(TrainingItemQualification)) \
.exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification)) .exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification))
@@ -160,10 +161,14 @@ class ModelComparison:
# Build some dicts of what we have # Build some dicts of what we have
item_dict = {} # build a list of items, key is the item_pk item_dict = {} # build a list of items, key is the item_pk
for version in old_item_versions: # put all the old versions in a list for version in old_item_versions: # put all the old versions in a list
if version._model is None:
continue
compare = ModelComparison(old=version._object_version.object, **comparisonParams) compare = ModelComparison(old=version._object_version.object, **comparisonParams)
item_dict[version.object_id] = compare item_dict[version.object_id] = compare
for version in new_item_versions: # go through the new versions for version in new_item_versions: # go through the new versions
if version._model is None:
continue
try: try:
compare = item_dict[version.object_id] # see if there's a matching old version compare = item_dict[version.object_id] # see if there's a matching old version
compare.new = version._object_version.object # then add the new version to the dictionary compare.new = version._object_version.object # then add the new version to the dictionary

View File

@@ -27,10 +27,10 @@ class VersionHistory(generic.ListView):
return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk']) return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk'])
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(VersionHistory, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['object'] = self.get_object() context['object'] = self.get_object()
if self.kwargs['app'] != 'rigboard': if self.kwargs['app'] != 'rigboard':
context['override'] = 'base_{}.html'.format(self.kwargs['app']) context['override'] = f'base_{self.kwargs["app"]}.html'
return context return context
@@ -59,10 +59,10 @@ class ActivityTable(generic.ListView):
return RIGSVersion.objects.get_for_multiple_models(filter_models(self.kwargs.get('models'), self.request.user)).order_by("-revision__date_created") return RIGSVersion.objects.get_for_multiple_models(filter_models(self.kwargs.get('models'), self.request.user)).order_by("-revision__date_created")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ActivityTable, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "{} Activity Stream".format(title(self.kwargs['app'])) context['page_title'] = f"{title(self.kwargs['app'])} Activity Stream"
if self.kwargs['app'] != 'rigboard': if self.kwargs['app'] != 'rigboard':
context['override'] = 'base_{}.html'.format(self.kwargs['app']) context['override'] = f'base_{self.kwargs["app"]}.html'
return context return context
@@ -77,7 +77,7 @@ class ActivityFeed(generic.ListView): # Appears on homepage
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# Call the base implementation first to get a context # Call the base implementation first to get a context
context = super(ActivityFeed, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "Activity Feed" context['page_title'] = "Activity Feed"
maxTimeDelta = datetime.timedelta(hours=1) maxTimeDelta = datetime.timedelta(hours=1)