Compare commits

..

57 Commits

Author SHA1 Message Date
ImgBotApp
d5dc879733 [ImgBot] Optimize images
*Total -- 8,936.02kb -> 7,990.11kb (10.59%)

/assets/static/imgs/square_logo.png -- 23.90kb -> 17.64kb (26.18%)
/RIGS/static/imgs/tappytaptap.gif -- 6,433.15kb -> 5,493.51kb (14.61%)
/RIGS/static/imgs/rigs.jpg -- 277.61kb -> 277.60kb (0%)
/RIGS/static/imgs/training.jpg -- 852.42kb -> 852.42kb (0%)
/RIGS/static/imgs/assets.jpg -- 1,348.94kb -> 1,348.94kb (0%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2022-01-18 19:32:05 +00:00
c537118037 Fix typo in training level list 2022-01-18 17:43:52 +00:00
466a9a9693 Delete broken migration
Manual SQL time whee
2022-01-18 16:20:18 +00:00
d25381b2de Create the training database (#463)
Co-authored-by: josephjboyden <josephjboyden@gmail.com>
2022-01-18 15:47:53 +00:00
dependabot[bot]
eaf891daf7 Build(deps): Bump copy-props from 2.0.4 to 2.0.5 (#468)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-15 12:13:40 +00:00
dependabot[bot]
801d2e8a7d Build(deps): Bump marked from 4.0.8 to 4.0.10 (#466)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-15 11:51:58 +00:00
dependabot[bot]
3d329219b8 Build(deps): Bump follow-redirects from 1.14.6 to 1.14.7 (#467)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-15 11:50:56 +00:00
2ddc8923ba CHORE: Fix pep8 2022-01-14 18:01:59 +00:00
276a86c5be FEAT(Asset): Add filter by date acquired
Date created isn't a DB field, so isn't efficient to filter by...
2022-01-14 17:54:20 +00:00
484f155e43 FEAT(Asset): Add ability to generate whole page of labels 2021-12-31 12:55:13 +00:00
fdbdaab52e FEAT(Asset): Add filter for only cables 2021-12-30 18:49:52 +00:00
Tom Price
a01e351e89 Markdown (#214)
* Add basic markdown support site wide

* Improved MD support.

Add some styling for images in MD

Add support for the bastardisation of the MD html for RML.

* Add processing for <ul> in RML

* Add OL processing to RML

* Fix a bug with squares appearing around the last page number

* Remove rml formatting in event_detail

* Improve handling of code blocks in RML

* Add MD to rigboard

Reduce MD title sizes as they were offensively large

* Add parsing of markdown when editing event items

* Improved list handling in RML

* Add tests for markdown support.

Focuses mainly on RML as that's where it will break

* Add indications of where MD support is enabled as per comment by @samozzy in #178.

Isn't quite a full description, but for the most part this should be enough for the people who know how to use it see where they can use it.

* Add failing test for markdown processing none

* Fix for failing test in e0d56e

* Add failing test for using single line breaks as per comment on #214

* Enable line break extension for single breaks in paragraphs by new lines.

Pass tests in ef3de607c3

* Enable GH flavour linebreaks in JS rendered markdown

* Made RML bullets pretty :)

* Added WYSIWYG editor. Works for notes & description, fails miserably for items :(

* Fixed for event items. Will probably fail tests because selenium can't type in simpleMDE :(

* FIX: Re-enable markdown on paperwork

Strikethrough is broken in all sorts of places for whatever reason

* FEAT: Markdown support on asset comments

* FIX: Prevent js injection through markdown fields

* Initial fixes

* Basic dark theme for simplemde

* Swap to locally delivered SimpleMDE

* Region for selenium testing of SimpleMDE

Bleh, Javascript all around

* Tests passing!

Fixed not using region for item modal, and overflow error on paperwork with really long description. Looks junk but I'm not really bothered

* Pep8 fixes

* Fallback for null HCapatcha sitekey

I.e. when we're on a branch

* Fix item description print being broken

* Actually fix sitekey problem

* Fixes for using markdown in asset comments

* Properly initialise markdown on asset comments

Co-authored-by: David Taylor <david@taylorhq.com>
Co-authored-by: FreneticScribbler <aj@aronajones.com>
2021-12-22 21:22:15 +00:00
dependabot[bot]
708a387774 Build(deps): Bump lodash from 4.17.20 to 4.17.21 (#461)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-19 12:12:01 +00:00
dependabot[bot]
af6fe582e0 Build(deps): Bump hosted-git-info from 2.8.8 to 2.8.9 (#460)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

---
updated-dependencies:
- dependency-name: hosted-git-info
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-19 12:11:52 +00:00
dependabot[bot]
905a144e7d Build(deps): Bump path-parse from 1.0.6 to 1.0.7 (#459)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-19 12:11:43 +00:00
0e64021f01 Pin >8 ver of postcss 2021-12-19 11:49:58 +00:00
2eb87a51f8 Update for gulp-sass change 2021-12-19 11:39:26 +00:00
30fac1d1b9 NPM Dependency update 2021-12-19 11:26:55 +00:00
c4fec483ae FIX/CHNG: Clients may see line prices on event auth form
Not sure why they couldn't previous, its not like we only quote totals...
2021-12-13 12:13:47 +00:00
3028fb92d9 Fix up the copy button stuff 2021-12-13 12:07:42 +00:00
215697ba64 Change navbar logo link when authenticated to RIGS 2021-11-23 09:22:06 +00:00
d966bddfd7 QOL: Add copy to clipboard buttons to emails on event auth request modal 2021-11-07 13:40:04 +00:00
0d5e48b89c Patching tests always feels a little like cheating 2021-11-07 12:57:31 +00:00
bd2c94d3e3 FIX: Wrong display on event checklist detail 2021-11-07 12:48:07 +00:00
21d09d951d FEAT: Add supplier edit/create buttons to asset form
Closes #454
2021-11-05 11:33:37 +00:00
014b00bc30 Fallback for pk being null on event display ID
This should never happen, but it is...though only in live, so I need to push this up for testing. Ref  #451
2021-11-04 23:03:03 +00:00
3f8fc82260 FIX: Duplicating an event clears collected by 2021-11-04 21:41:39 +00:00
41c1c44754 FIX #453: Venue display not working
Classic copy paste error...
2021-11-03 13:57:16 +00:00
8a2b107516 FIX: View event button on event checklist detail 2021-10-20 20:22:35 +01:00
f8c52803a5 CHANGE: Ignore cancelled events in HS lists 2021-10-19 13:53:49 +01:00
85d1850f08 CHANGE: Do not add VAT on internal events
This concurs with discussions with the SU
2021-10-18 17:57:55 +01:00
e146d9314a FIX: Mark safe page title in modal header 2021-10-09 14:21:19 +01:00
2c3dff79ba D'oh 2021-10-09 11:14:24 +01:00
9ee8cd0f8b Asset search and URLs convert lower to uppercase
Closes #440
2021-10-09 11:00:35 +01:00
d3391d9e3e FIX: Update EC detail now that medium power info can be filled out for large events 2021-10-08 18:38:11 +01:00
James Herbert
0086461d6c Change Zs field in Event Checklist from integer to decimal (#450)
Co-authored-by: David Taylor <david@taylorhq.com> 
Co-authored-by: James Herbert <james@artyzan.net>
2021-10-08 18:31:12 +01:00
8bafeabe5f Add a badge for outstanding invoices
The header badge displays the total

Also fixes the previous commit as I don't think that would have worked.
2021-10-04 09:50:40 +01:00
f214f9a835 FEAT: Invoices waiting badge goes green with none waiting 2021-10-04 09:24:21 +01:00
b31d53a3c5 Minor fixes to contact detail stuff on event auth form 2021-09-27 20:44:20 +01:00
62a891c6ec Fallback for when there is no person 2021-09-27 20:27:15 +01:00
8c0c0941c2 Update event authorisation status chip with more statusi
Closes #446
2021-09-23 11:39:54 +01:00
abb0e35690 Uncomment headless flag for old style tests 2021-09-18 10:04:34 +01:00
bec0d4aee5 Aronafail 2021-09-18 09:53:23 +01:00
f1e43b707e Bypass hCaptcha in automated testing 2021-09-18 09:47:21 +01:00
796f5b44b0 Navbar works properly again 2021-09-13 16:14:18 +01:00
6458f016f0 Switch to hCapatcha 2021-09-13 16:14:05 +01:00
9ca953423f Revamp registration form 2021-09-13 16:13:47 +01:00
dependabot[bot]
4c5d958c6d Build(deps): Bump sqlparse from 0.4.1 to 0.4.2 (#442)
Bumps [sqlparse](https://github.com/andialbrecht/sqlparse) from 0.4.1 to 0.4.2.
- [Release notes](https://github.com/andialbrecht/sqlparse/releases)
- [Changelog](https://github.com/andialbrecht/sqlparse/blob/master/CHANGELOG)
- [Commits](https://github.com/andialbrecht/sqlparse/compare/0.4.1...0.4.2)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-11 10:56:42 +01:00
85ca7b0880 Fix event button on invoice detail page
Was very confusing at first when I got a random event from several years ago!
2021-09-11 10:40:53 +01:00
44f9509eda Make very long asset children lists scroll 2021-09-08 15:41:14 +01:00
a2be4cbe5e Add some missing links to asset detail 2021-09-08 15:31:30 +01:00
dependabot[bot]
bb2f369ab5 Build(deps): Bump pillow from 8.2.0 to 8.3.2 (#441)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.2.0 to 8.3.2.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.2.0...8.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-08 12:48:44 +01:00
2fdb2f260f FEAT: Add ability to generate label images for cable assets
To come SoonTM: ability to generate a A4 page of labels at once
2021-09-07 14:54:01 +01:00
6de3cb5d8c Allow filling out of electrical checks for large events 2021-09-02 12:11:05 +01:00
7c38af66f6 May fix windows/chrome RA name chooser issue
No idea tbh
2021-08-31 19:47:49 +01:00
f1a624ec8f Improve RA detail layout slightly 2021-08-31 19:39:24 +01:00
ab01beb2cd Add title links to ra/ec detail 2021-08-31 19:33:03 +01:00
129 changed files with 7074 additions and 5616 deletions

1
.gitignore vendored
View File

@@ -26,6 +26,7 @@ var/
.installed.cfg .installed.cfg
*.egg *.egg
node_modules/ node_modules/
data/
# Continer extras # Continer extras
.vagrant .vagrant

15
Pipfile
View File

@@ -19,11 +19,10 @@ cssselect = "~=1.1.0"
cssutils = "~=1.0.2" cssutils = "~=1.0.2"
dj-database-url = "~=0.5.0" dj-database-url = "~=0.5.0"
dj-static = "~=0.0.6" dj-static = "~=0.0.6"
Django = "~=3.1.12" Django = "~=3.2"
django-debug-toolbar = "~=3.2" django-debug-toolbar = "~=3.2"
django-filter = "~=2.4.0" django-filter = "~=2.4.0"
django-ical = "~=1.7.1" django-ical = "~=1.7.1"
django-recaptcha = "~=2.0.6"
django-recurrence = "~=1.10.3" django-recurrence = "~=1.10.3"
django-registration-redux = "~=2.9" django-registration-redux = "~=2.9"
django-reversion = "~=3.0.9" django-reversion = "~=3.0.9"
@@ -34,12 +33,11 @@ envparse = "~=0.2.0"
gunicorn = "~=20.0.4" gunicorn = "~=20.0.4"
icalendar = "~=4.0.7" icalendar = "~=4.0.7"
idna = "~=2.10" idna = "~=2.10"
importlib-metadata = "~=3.4.0" lxml = "~=4.7.1"
lxml = "~=4.6.3"
Markdown = "~=3.3.3" Markdown = "~=3.3.3"
msgpack = "~=1.0.2" msgpack = "~=1.0.2"
pep517 = "~=0.9.1" pep517 = "~=0.9.1"
Pillow = "~=8.2.0" Pillow = "~=9.0.0"
premailer = "~=3.7.0" premailer = "~=3.7.0"
progress = "~=1.5" progress = "~=1.5"
psutil = "~=5.8.0" psutil = "~=5.8.0"
@@ -57,7 +55,7 @@ retrying = "~=1.3.3"
simplejson = "~=3.17.2" simplejson = "~=3.17.2"
six = "~=1.15.0" six = "~=1.15.0"
soupsieve = "~=2.1" soupsieve = "~=2.1"
sqlparse = "~=0.4.1" 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"
@@ -77,6 +75,10 @@ zipp = "~=3.4.0"
"zope.schema" = "~=6.0.1" "zope.schema" = "~=6.0.1"
sentry-sdk = "*" sentry-sdk = "*"
diff-match-patch = "*" diff-match-patch = "*"
python-barcode = "*"
django-hCaptcha = "*"
importlib-metadata = "*"
django-hcaptcha = "*"
[dev-packages] [dev-packages]
selenium = "~=3.141.0" selenium = "~=3.141.0"
@@ -88,6 +90,7 @@ pytest-django = "*"
pluggy = "*" pluggy = "*"
pytest-splinter = "*" pytest-splinter = "*"
pytest = "*" pytest = "*"
pytest-reverse = "*"
[requires] [requires]
python_version = "3.9" python_version = "3.9"

1015
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -66,8 +66,8 @@ INSTALLED_APPS = (
'debug_toolbar', 'debug_toolbar',
'registration', 'registration',
'reversion', 'reversion',
'captcha',
'widget_tweaks', 'widget_tweaks',
'hcaptcha',
) )
MIDDLEWARE = ( MIDDLEWARE = (
@@ -187,12 +187,9 @@ LOGOUT_URL = '/user/logout/'
ACCOUNT_ACTIVATION_DAYS = 7 ACCOUNT_ACTIVATION_DAYS = 7
# reCAPTCHA settings # CAPTCHA settings
RECAPTCHA_PUBLIC_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001')
RECAPTCHA_PRIVATE_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000')
NOCAPTCHA = True
SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error']
# Email # Email
EMAILER_TEST = False EMAILER_TEST = False
@@ -263,3 +260,5 @@ USE_GRAVATAR = True
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf" 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'

View File

@@ -84,7 +84,7 @@ class BootstrapSelectElement(Region):
return [self.BootstrapSelectOption(self, i) for i in options] return [self.BootstrapSelectOption(self, i) for i in options]
def set_option(self, name, selected): def set_option(self, name, selected):
options = list((x for x in self.options if x.name == name)) options = [x for x in self.options if x.name == name]
assert len(options) == 1 assert len(options) == 1
options[0].set_selected(selected) options[0].set_selected(selected)
@@ -117,6 +117,15 @@ class TextBox(Region):
self.root.send_keys(value) self.root.send_keys(value)
class SimpleMDETextArea(Region):
@property
def value(self):
return self.driver.execute_script("return document.querySelector('#' + arguments[0]).nextSibling.nextSibling.CodeMirror.getDoc().getValue();", self.root.get_attribute("id"))
def set_value(self, value):
self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.nextSibling.CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
class CheckBox(Region): class CheckBox(Region):
def toggle(self): def toggle(self):
self.root.click() self.root.click()

View File

@@ -8,18 +8,13 @@ from pytest_django.asserts import assertRedirects, assertContains, assertNotCont
from pytest_django.asserts import assertTemplateUsed, assertInHTML from pytest_django.asserts import assertTemplateUsed, assertInHTML
from PyRIGS import urls from PyRIGS import urls
from RIGS.models import Event from RIGS.models import Event, Profile
from assets.models import Asset from assets.models import Asset
from django.db import connection from django.db import connection
import pytest
from django.core.management import call_command
from django.template.defaultfilters import striptags from django.template.defaultfilters import striptags
from django.urls.exceptions import NoReverseMatch from django.urls.exceptions import NoReverseMatch
from RIGS.models import Event from django.test import TestCase, TransactionTestCase
from assets.models import Asset
from django.db import connection
from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
@@ -49,7 +44,7 @@ def get_request_url(url):
@pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData', @pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData',
'deleteSampleData']) 'deleteSampleData', 'generateSampleTrainingData', 'generate_sample_training_users'])
def test_production_exception(command): def test_production_exception(command):
from django.core.management.base import CommandError from django.core.management.base import CommandError
with pytest.raises(CommandError, match=".*production"): with pytest.raises(CommandError, match=".*production"):
@@ -67,79 +62,76 @@ class TestSampleDataGenerator(TestCase):
assert Event.objects.all().count() == 0 assert Event.objects.all().count() == 0
class TestSampleDataGenerator(TestCase): @override_settings(DEBUG=True)
@override_settings(DEBUG=True) @pytest.mark.skip(reason="broken")
def setUp(self): def test_unauthenticated(client): # Nothing should be available to the unauthenticated
call_command('generateSampleData') call_command('generateSampleData')
for url in find_urls_recursive(urls.urlpatterns):
def test_unauthenticated(self): # Nothing should be available to the unauthenticated request_url = get_request_url(url)
for url in find_urls_recursive(urls.urlpatterns): if request_url and 'user' not in request_url: # User module is full of edge cases
request_url = get_request_url(url) response = client.get(request_url, follow=True, HTTP_HOST='example.com')
if request_url and 'user' not in request_url: # User module is full of edge cases assertContains(response, 'Login')
response = self.client.get(request_url, follow=True, HTTP_HOST='example.com') if 'application/json+oembed' in response.content.decode():
assertContains(response, 'Login') assertTemplateUsed(response, 'login_redirect.html')
if 'application/json+oembed' in response.content.decode(): else:
assertTemplateUsed(response, 'login_redirect.html') if "embed" in str(url):
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
else: else:
if "embed" in str(url): expected_url = "{0}?next={1}".format(reverse('login'), request_url)
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url) assertRedirects(response, expected_url)
else: call_command('deleteSampleData')
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
assertRedirects(response, expected_url)
def test_page_titles(self):
assert self.client.login(username='superuser', password='superuser')
for url in filter((lambda u: "embed" not in u.name), find_urls_recursive(urls.urlpatterns)):
request_url = get_request_url(url)
response = self.client.get(request_url)
if hasattr(response, "context_data") and "page_title" in response.context_data:
expected_title = striptags(response.context_data["page_title"])
assertInHTML('<title>{} | Rig Information Gathering System'.format(expected_title),
response.content.decode())
print("{} | {}".format(request_url, expected_title)) # If test fails, tell me where!
self.client.logout()
def test_basic_access(self): @override_settings(DEBUG=True)
assert self.client.login(username="basic", password="basic") @pytest.mark.skip(reason="broken")
def test_basic_access(client):
call_command('generateSampleData')
assert client.login(username="basic", password="basic")
url = reverse('asset_list') url = reverse('asset_list')
response = self.client.get(url) response = client.get(url)
# Check edit and duplicate buttons NOT shown in list # Check edit and duplicate buttons NOT shown in list
assertNotContains(response, 'Edit') assertNotContains(response, 'Edit')
assertNotContains(response, assertNotContains(response,
'Duplicate') # If this line is randomly failing, check the debug toolbar HTML hasn't crept in 'Duplicate') # If this line is randomly failing, check the debug toolbar HTML hasn't crept in
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id}) url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = self.client.get(url) response = client.get(url)
assertNotContains(response, 'Purchase Details') assertNotContains(response, 'Purchase Details')
assertNotContains(response, 'View Revision History') assertNotContains(response, 'View Revision History')
urlz = {'asset_history', 'asset_update', 'asset_duplicate'} urlz = {'asset_history', 'asset_update', 'asset_duplicate'}
for url_name in urlz: for url_name in urlz:
request_url = reverse(url_name, kwargs={'pk': Asset.objects.first().asset_id}) request_url = reverse(url_name, kwargs={'pk': Asset.objects.first().asset_id})
response = self.client.get(request_url, follow=True) response = client.get(request_url, follow=True)
assert response.status_code == 403
request_url = reverse('supplier_create')
response = self.client.get(request_url, follow=True)
assert response.status_code == 403 assert response.status_code == 403
request_url = reverse('supplier_update', kwargs={'pk': 1}) request_url = reverse('supplier_create')
response = self.client.get(request_url, follow=True) response = client.get(request_url, follow=True)
assert response.status_code == 403 assert response.status_code == 403
self.client.logout()
def test_keyholder_access(self): request_url = reverse('supplier_update', kwargs={'pk': 1})
assert self.client.login(username="keyholder", password="keyholder") response = client.get(request_url, follow=True)
assert response.status_code == 403
client.logout()
call_command('deleteSampleData')
url = reverse('asset_list')
response = self.client.get(url)
# Check edit and duplicate buttons shown in list
assertContains(response, 'Edit')
assertContains(response, 'Duplicate')
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id}) @override_settings(DEBUG=True)
response = self.client.get(url) @pytest.mark.skip(reason="broken")
assertContains(response, 'Purchase Details') def test_keyholder_access(client):
assertContains(response, 'View Revision History') call_command('generateSampleData')
self.client.logout() assert client.login(username="keyholder", password="keyholder")
url = reverse('asset_list')
response = client.get(url)
# Check edit and duplicate buttons shown in list
assertContains(response, 'Edit')
assertContains(response, 'Duplicate')
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = client.get(url)
assertContains(response, 'Purchase Details')
assertContains(response, 'View Revision History')
client.logout()
call_command('deleteSampleData')

View File

@@ -50,7 +50,7 @@ class SecureAPIRequest(generic.View):
'profile': 'RIGS.view_profile', 'profile': 'RIGS.view_profile',
'event': None, 'event': None,
'supplier': None, 'supplier': None,
'training_item': None, # TODO 'training_item': None, # TODO
} }
''' '''
@@ -78,6 +78,9 @@ class SecureAPIRequest(generic.View):
fields = request.GET.get('fields', None) fields = request.GET.get('fields', None)
if fields: if fields:
fields = fields.split(",") fields = fields.split(",")
filters = request.GET.get('filters', [])
if filters:
filters = filters.split(",")
# Supply data for one record # Supply data for one record
if pk: if pk:
@@ -98,8 +101,13 @@ class SecureAPIRequest(generic.View):
for field in fields: for field in fields:
q = Q(**{field + "__icontains": part}) q = Q(**{field + "__icontains": part})
qs.append(q) qs.append(q)
queries.append(reduce(operator.or_, qs)) queries.append(reduce(operator.or_, qs))
for f in filters:
q = Q(**{f: True})
queries.append(q)
# Build the data response list # Build the data response list
results = [] results = []
query = reduce(operator.and_, queries) query = reduce(operator.and_, queries)

View File

@@ -1 +0,0 @@
default_app_config = 'RIGS.apps.RIGSAppConfig'

View File

@@ -24,7 +24,7 @@ class InvoiceIndex(generic.ListView):
template_name = 'invoice_list.html' template_name = 'invoice_list.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(InvoiceIndex, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
total = 0 total = 0
for i in context['object_list']: for i in context['object_list']:
total += i.balance total += i.balance
@@ -33,20 +33,7 @@ class InvoiceIndex(generic.ListView):
return context return context
def get_queryset(self): def get_queryset(self):
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must return self.model.objects.outstanding_invoices()
sql = "SELECT * FROM " \
"(SELECT " \
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
"AS sub " \
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
"ORDER BY invoice_date"
query = self.model.objects.raw(sql)
return query
class InvoiceDetail(generic.DetailView): class InvoiceDetail(generic.DetailView):
@@ -54,8 +41,9 @@ class InvoiceDetail(generic.DetailView):
template_name = 'invoice_detail.html' template_name = 'invoice_detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(InvoiceDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "Invoice {} ({}) ".format(self.object.display_id, self.object.invoice_date.strftime("%d/%m/%Y")) invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date}) "
if self.object.void: if self.object.void:
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>" context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
elif self.object.is_closed: elif self.object.is_closed:
@@ -130,7 +118,7 @@ class InvoiceArchive(generic.ListView):
paginate_by = 25 paginate_by = 25
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(InvoiceArchive, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "Invoice Archive" context['page_title'] = "Invoice Archive"
context['description'] = "This page displays all invoices: outstanding, paid, and void" context['description'] = "This page displays all invoices: outstanding, paid, and void"
return context return context
@@ -209,7 +197,7 @@ class PaymentCreate(generic.CreateView):
template_name = 'payment_form.html' template_name = 'payment_form.html'
def get_initial(self): def get_initial(self):
initial = super(generic.CreateView, self).get_initial() initial = super().get_initial()
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None)) invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
if invoicepk is None: if invoicepk is None:
raise Http404() raise Http404()

View File

@@ -8,6 +8,7 @@ from django.utils import timezone
from reversion import revisions as reversion from reversion import revisions as reversion
from RIGS import models from RIGS import models
from training.models import TrainingLevel
# Override the django form defaults to use the HTML date/time/datetime UI elements # Override the django form defaults to use the HTML date/time/datetime UI elements
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'}) forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
@@ -96,10 +97,10 @@ class EventForm(forms.ModelForm):
raise forms.ValidationError( raise forms.ValidationError(
'You haven\'t provided any client contact details. Please add a person or organisation.', 'You haven\'t provided any client contact details. Please add a person or organisation.',
code='contact') code='contact')
return super(EventForm, self).clean() return super().clean()
def save(self, commit=True): def save(self, commit=True):
m = super(EventForm, self).save(commit=False) m = super().save(commit=False)
if (commit): if (commit):
m.save() m.save()
@@ -138,7 +139,7 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm): class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(InternalClientEventAuthorisationForm, self).__init__(**kwargs) super().__init__(**kwargs)
self.fields['uni_id'].required = True self.fields['uni_id'].required = True
self.fields['account_code'].required = True self.fields['account_code'].required = True
@@ -153,7 +154,7 @@ class EventAuthorisationRequestForm(forms.Form):
class EventRiskAssessmentForm(forms.ModelForm): class EventRiskAssessmentForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EventRiskAssessmentForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for name, field in self.fields.items(): for name, field in self.fields.items():
if str(name) == 'supervisor_consulted': if str(name) == 'supervisor_consulted':
field.widget = forms.CheckboxInput() field.widget = forms.CheckboxInput()
@@ -164,6 +165,9 @@ class EventRiskAssessmentForm(forms.ModelForm):
], attrs={'class': 'custom-control-input', 'required': 'true'}) ], attrs={'class': 'custom-control-input', 'required': 'true'})
def clean(self): def clean(self):
if self.cleaned_data.get('big_power'):
if not self.cleaned_data.get('power_mic').level_qualifications.filter(level__department=TrainingLevel.POWER).exists():
self.add_error('power_mic', forms.ValidationError("Your Power MIC must be a Power Technician.", code="power_tech_required"))
# Check expected values # Check expected values
unexpected_values = [] unexpected_values = []
for field, value in models.RiskAssessment.expected_values.items(): for field, value in models.RiskAssessment.expected_values.items():
@@ -181,7 +185,7 @@ class EventRiskAssessmentForm(forms.ModelForm):
class EventChecklistForm(forms.ModelForm): class EventChecklistForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EventChecklistForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['date'].widget.format = '%Y-%m-%d' self.fields['date'].widget.format = '%Y-%m-%d'
for name, field in self.fields.items(): for name, field in self.fields.items():
if field.__class__ == forms.NullBooleanField: if field.__class__ == forms.NullBooleanField:

View File

@@ -70,6 +70,11 @@ class EventRiskAssessmentDetail(generic.DetailView):
model = models.RiskAssessment model = models.RiskAssessment
template_name = 'risk_assessment_detail.html' template_name = 'risk_assessment_detail.html'
def get_context_data(self, **kwargs):
context = super(EventRiskAssessmentDetail, self).get_context_data(**kwargs)
context['page_title'] = "Risk Assessment for Event <a href='{}'>{} {}</a>".format(self.object.event.get_absolute_url(), self.object.event.display_id, self.object.event.name)
return context
class EventRiskAssessmentList(generic.ListView): class EventRiskAssessmentList(generic.ListView):
paginate_by = 20 paginate_by = 20
@@ -77,7 +82,7 @@ class EventRiskAssessmentList(generic.ListView):
template_name = 'hs_object_list.html' template_name = 'hs_object_list.html'
def get_queryset(self): def get_queryset(self):
return self.model.objects.order_by('reviewed_at').select_related('event') return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventRiskAssessmentList, self).get_context_data(**kwargs) context = super(EventRiskAssessmentList, self).get_context_data(**kwargs)
@@ -107,7 +112,7 @@ class EventChecklistDetail(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventChecklistDetail, self).get_context_data(**kwargs) context = super(EventChecklistDetail, self).get_context_data(**kwargs)
context['page_title'] = "Event Checklist for Event {} {}".format(self.object.event.display_id, self.object.event.name) context['page_title'] = "Event Checklist for Event <a href='{}'>{} {}</a>".format(self.object.event.get_absolute_url(), self.object.event.display_id, self.object.event.name)
return context return context
@@ -182,6 +187,9 @@ class EventChecklistList(generic.ListView):
model = models.EventChecklist model = models.EventChecklist
template_name = 'hs_object_list.html' template_name = 'hs_object_list.html'
def get_queryset(self):
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventChecklistList, self).get_context_data(**kwargs) context = super(EventChecklistList, self).get_context_data(**kwargs)
context['title'] = 'Event Checklist' context['title'] = 'Event Checklist'
@@ -210,7 +218,7 @@ class HSList(generic.ListView):
template_name = 'hs_list.html' template_name = 'hs_list.html'
def get_queryset(self): def get_queryset(self):
return models.Event.objects.all().order_by('-start_date').select_related('riskassessment').prefetch_related('checklists') return models.Event.objects.all().exclude(status=models.Event.CANCELLED).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(HSList, self).get_context_data(**kwargs)

View File

@@ -5,6 +5,7 @@ from assets import models
from RIGS import models as rigsmodels from RIGS import models as rigsmodels
from training import models as tmodels from training import models as tmodels
class Command(BaseCommand): class Command(BaseCommand):
help = 'Deletes testing sample data' help = 'Deletes testing sample data'
@@ -34,6 +35,8 @@ class Command(BaseCommand):
self.delete_objects(tmodels.TrainingCategory) self.delete_objects(tmodels.TrainingCategory)
self.delete_objects(tmodels.TrainingItem) self.delete_objects(tmodels.TrainingItem)
self.delete_objects(tmodels.TrainingLevel) self.delete_objects(tmodels.TrainingLevel)
self.delete_objects(tmodels.TrainingItemQualification)
self.delete_objects(tmodels.TrainingLevelRequirement)
def delete_objects(self, model): def delete_objects(self, model):
for obj in model.objects.all(): for obj in model.objects.all():

View File

@@ -3,6 +3,7 @@
from django.db import models, migrations from django.db import models, migrations
import RIGS.models import RIGS.models
import versioning
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -25,6 +26,6 @@ class Migration(migrations.Migration):
], ],
options={ options={
}, },
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
] ]

View File

@@ -3,6 +3,7 @@
from django.db import models, migrations from django.db import models, migrations
import RIGS.models import RIGS.models
import versioning
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -21,6 +22,6 @@ class Migration(migrations.Migration):
], ],
options={ options={
}, },
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
] ]

View File

@@ -4,6 +4,7 @@
from django.db import models, migrations from django.db import models, migrations
from django.conf import settings from django.conf import settings
import RIGS.models import RIGS.models
import versioning
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -41,7 +42,7 @@ class Migration(migrations.Migration):
], ],
options={ options={
}, },
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
migrations.CreateModel( migrations.CreateModel(
name='EventItem', name='EventItem',
@@ -70,7 +71,7 @@ class Migration(migrations.Migration):
], ],
options={ options={
}, },
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
migrations.AddField( migrations.AddField(
model_name='event', model_name='event',

View File

@@ -4,6 +4,7 @@ import RIGS.models
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
import versioning
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -58,7 +59,7 @@ class Migration(migrations.Migration):
'ordering': ['event'], 'ordering': ['event'],
'permissions': [('review_eventchecklist', 'Can review Event Checklists')], 'permissions': [('review_eventchecklist', 'Can review Event Checklists')],
}, },
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
migrations.CreateModel( migrations.CreateModel(
name='EventChecklistCrew', name='EventChecklistCrew',
@@ -69,7 +70,7 @@ class Migration(migrations.Migration):
('end', models.DateTimeField()), ('end', models.DateTimeField()),
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.eventchecklist')), ('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.eventchecklist')),
], ],
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
migrations.CreateModel( migrations.CreateModel(
name='EventChecklistVehicle', name='EventChecklistVehicle',
@@ -78,7 +79,7 @@ class Migration(migrations.Migration):
('vehicle', models.CharField(max_length=255)), ('vehicle', models.CharField(max_length=255)),
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to='RIGS.eventchecklist')), ('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to='RIGS.eventchecklist')),
], ],
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
migrations.CreateModel( migrations.CreateModel(
name='RiskAssessment', name='RiskAssessment',
@@ -117,7 +118,7 @@ class Migration(migrations.Migration):
'ordering': ['event'], 'ordering': ['event'],
'permissions': [('review_riskassessment', 'Can review Risk Assessments')], 'permissions': [('review_riskassessment', 'Can review Risk Assessments')],
}, },
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, versioning.versioning.RevisionMixin),
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='eventcrew', model_name='eventcrew',

View File

@@ -0,0 +1,34 @@
# Generated by Django 3.1.13 on 2021-10-07 22:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0041_auto_20210302_1204'),
]
operations = [
migrations.AlterField(
model_name='eventchecklist',
name='fd_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='eventchecklist',
name='w1_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='eventchecklist',
name='w2_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='eventchecklist',
name='w3_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.13 on 2021-10-27 14:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0042_auto_20211007_2338'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='initials',
field=models.CharField(max_length=5, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.11 on 2022-01-09 14:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0043_auto_20211027_1519'),
]
operations = [
migrations.AddField(
model_name='profile',
name='is_supervisor',
field=models.BooleanField(default=False),
),
]

View File

@@ -20,13 +20,16 @@ from reversion.models import Version
class Profile(AbstractUser): class Profile(AbstractUser):
initials = models.CharField(max_length=5, unique=True, null=True, blank=False) initials = models.CharField(max_length=5, null=True, blank=False)
phone = models.CharField(max_length=13, blank=True, default='') phone = models.CharField(max_length=13, blank=True, default='')
api_key = models.CharField(max_length=40, blank=True, editable=False, default='') api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
is_approved = models.BooleanField(default=False) is_approved = models.BooleanField(default=False)
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that... # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
last_emailed = models.DateTimeField(blank=True, null=True) last_emailed = models.DateTimeField(blank=True, null=True)
dark_theme = models.BooleanField(default=False) dark_theme = models.BooleanField(default=False)
is_supervisor = models.BooleanField(default=False)
reversion_hide = True
@classmethod @classmethod
def make_api_key(cls): def make_api_key(cls):
@@ -65,10 +68,8 @@ class Profile(AbstractUser):
def __str__(self): def __str__(self):
return self.name return self.name
# TODO move to versioning - currently get import errors with that
class RevisionMixin:
class RevisionMixin(object):
@property @property
def is_first_version(self): def is_first_version(self):
versions = Version.objects.get_for_object(self) versions = Version.objects.get_for_object(self)
@@ -98,7 +99,7 @@ class RevisionMixin(object):
version = self.current_version version = self.current_version
if version is None: if version is None:
return None return None
return "V{0} | R{1}".format(version.pk, version.revision.pk) return f"V{version.pk} | R{version.revision.pk}"
class Person(models.Model, RevisionMixin): class Person(models.Model, RevisionMixin):
@@ -206,7 +207,7 @@ class VatRate(models.Model, RevisionMixin):
get_latest_by = 'start_at' get_latest_by = 'start_at'
def __str__(self): def __str__(self):
return self.comment + " " + str(self.start_at) + " @ " + str(self.as_percent) + "%" return f"{self.comment} {self.start_at} @ {self.as_percent}%"
class Venue(models.Model, RevisionMixin): class Venue(models.Model, RevisionMixin):
@@ -343,11 +344,14 @@ class Event(models.Model, RevisionMixin):
@property @property
def display_id(self): def display_id(self):
if self.is_rig: if self.pk:
return str("N%05d" % self.pk) if self.is_rig:
else: return str("N%05d" % self.pk)
return self.pk return self.pk
return "????"
# Calculated values # Calculated values
""" """
EX Vat EX Vat
@@ -369,6 +373,9 @@ class Event(models.Model, RevisionMixin):
@property @property
def vat(self): def vat(self):
# No VAT is owed on internal transfers
if self.internal:
return 0
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01')) return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
""" """
@@ -468,7 +475,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 "{}: {}".format(self.display_id, self.name) return f"{self.display_id}: {self.name}"
def clean(self): def clean(self):
errdict = {} errdict = {}
@@ -514,11 +521,11 @@ class EventItem(models.Model, RevisionMixin):
ordering = ['order'] ordering = ['order']
def __str__(self): def __str__(self):
return "{}.{}: {} | {}".format(self.event_id, self.order, self.event.name, self.name) return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return str("item {}".format(self.name)) return f"item {self.name}"
@reversion.register @reversion.register
@@ -536,7 +543,24 @@ class EventAuthorisation(models.Model, RevisionMixin):
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return "{} (requested by {})".format(self.event.display_id, self.sent_by.initials) return f"{self.event.display_id} (requested by {self.sent_by.initials})"
class InvoiceManager(models.Manager):
def outstanding_invoices(self):
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
sql = "SELECT * FROM " \
"(SELECT " \
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
"AS sub " \
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
"ORDER BY invoice_date"
query = self.raw(sql)
return query
@reversion.register(follow=['payment_set']) @reversion.register(follow=['payment_set'])
@@ -547,6 +571,8 @@ class Invoice(models.Model, RevisionMixin):
reversion_perm = 'RIGS.view_invoice' reversion_perm = 'RIGS.view_invoice'
objects = InvoiceManager()
@property @property
def sum_total(self): def sum_total(self):
return self.event.sum_total return self.event.sum_total
@@ -645,7 +671,6 @@ class RiskAssessment(models.Model, RevisionMixin):
# Power # Power
big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?") big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
# If yes to the above two, you must answer...
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True, power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)") verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)")
outside = models.BooleanField(help_text="Is the event outdoors?") outside = models.BooleanField(help_text="Is the event outdoors?")
@@ -777,21 +802,21 @@ class EventChecklist(models.Model, 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.IntegerField(blank=True, null=True, 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=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current") fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
# 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")
w1_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)") w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description") w2_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")
w2_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)") w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description") w3_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")
w3_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)") w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</small>") 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>")

View File

@@ -38,7 +38,7 @@ class RigboardIndex(generic.TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# get super context # get super context
context = super(RigboardIndex, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# call out method to get current events # call out method to get current events
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists') context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
@@ -50,7 +50,7 @@ class WebCalendar(generic.TemplateView):
template_name = 'calendar.html' template_name = 'calendar.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(WebCalendar, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['view'] = kwargs.get('view', '') context['view'] = kwargs.get('view', '')
context['date'] = kwargs.get('date', '') context['date'] = kwargs.get('date', '')
return context return context
@@ -61,8 +61,8 @@ class EventDetail(generic.DetailView):
model = models.Event model = models.Event
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
title = "{} | {}".format(self.object.display_id, self.object.name) title = f"{self.object.display_id} | {self.object.name}"
if self.object.dry_hire: if self.object.dry_hire:
title += " <span class='badge badge-secondary'>Dry Hire</span>" title += " <span class='badge badge-secondary'>Dry Hire</span>"
context['page_title'] = title context['page_title'] = title
@@ -84,7 +84,7 @@ class EventCreate(generic.CreateView):
template_name = 'event_form.html' template_name = 'event_form.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventCreate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "New Event" context['page_title'] = "New Event"
context['edit'] = True context['edit'] = True
context['currentVAT'] = models.VatRate.objects.current_rate() context['currentVAT'] = models.VatRate.objects.current_rate()
@@ -110,8 +110,8 @@ class EventUpdate(generic.UpdateView):
template_name = 'event_form.html' template_name = 'event_form.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventUpdate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "Event {}".format(self.object.display_id) context['page_title'] = f"Event {self.object.display_id}"
context['edit'] = True context['edit'] = True
form = context['form'] form = context['form']
@@ -134,7 +134,7 @@ class EventUpdate(generic.UpdateView):
if hasattr(self.object, 'authorised'): if hasattr(self.object, 'authorised'):
messages.warning(self.request, messages.warning(self.request,
'This event has already been authorised by the client, any changes to the price will require reauthorisation.') 'This event has already been authorised by the client, any changes to the price will require reauthorisation.')
return super(EventUpdate, self).render_to_response(context, **response_kwargs) return super().render_to_response(context, **response_kwargs)
def get_success_url(self): def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk}) return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
@@ -142,7 +142,7 @@ class EventUpdate(generic.UpdateView):
class EventDuplicate(EventUpdate): class EventDuplicate(EventUpdate):
def get_object(self, queryset=None): def get_object(self, queryset=None):
old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating) old = super().get_object(queryset) # Get the object (the event you're duplicating)
new = copy.copy(old) # Make a copy of the object in memory new = copy.copy(old) # Make a copy of the object in memory
new.based_on = old # Make the new event based on the old event new.based_on = old # Make the new event based on the old event
new.purchase_order = None # Remove old PO new.purchase_order = None # Remove old PO
@@ -151,6 +151,7 @@ class EventDuplicate(EventUpdate):
# Clear checked in by if it's a dry hire # Clear checked in by if it's a dry hire
if new.dry_hire is True: if new.dry_hire is True:
new.checked_in_by = None new.checked_in_by = None
new.collector = None
# Remove all the authorisation information from the new event # Remove all the authorisation information from the new event
new.auth_request_to = '' new.auth_request_to = ''
@@ -166,8 +167,8 @@ class EventDuplicate(EventUpdate):
return new return new
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventDuplicate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "Duplicate of Event {}".format(self.object.display_id) context['page_title'] = f"Duplicate of Event {self.object.display_id}"
context["duplicate"] = True context["duplicate"] = True
return context return context
@@ -209,8 +210,7 @@ class EventArchive(generic.ListView):
paginate_by = 25 paginate_by = 25
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# get super context context = super().get_context_data(**kwargs)
context = super(EventArchive, self).get_context_data(**kwargs)
context['start'] = self.request.GET.get('start', None) context['start'] = self.request.GET.get('start', None)
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d')) context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
@@ -265,7 +265,7 @@ class EventArchive(generic.ListView):
# Preselect related for efficiency # Preselect related for efficiency
qs.select_related('person', 'organisation', 'venue', 'mic') qs.select_related('person', 'organisation', 'venue', 'mic')
if len(qs) == 0: if not qs.exists():
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.") messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
return qs return qs
@@ -282,7 +282,7 @@ class EventAuthorise(generic.UpdateView):
self.template_name = self.success_template self.template_name = self.success_template
messages.add_message(self.request, messages.SUCCESS, messages.add_message(self.request, messages.SUCCESS,
'Success! Your event has been authorised. ' + 'Success! Your event has been authorised. ' +
'You will also receive email confirmation to %s.' % self.object.email) f'You will also receive email confirmation to {self.object.email}.')
return self.render_to_response(self.get_context_data()) return self.render_to_response(self.get_context_data())
@property @property
@@ -296,10 +296,10 @@ class EventAuthorise(generic.UpdateView):
return forms.InternalClientEventAuthorisationForm return forms.InternalClientEventAuthorisationForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventAuthorise, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['event'] = self.event context['event'] = self.event
context['tos_url'] = settings.TERMS_OF_HIRE_URL context['tos_url'] = settings.TERMS_OF_HIRE_URL
context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name) context['page_title'] = f"{self.event.display_id}: {self.event.name}"
if self.event.dry_hire: if self.event.dry_hire:
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>' context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
context['preview'] = self.preview context['preview'] = self.preview
@@ -318,7 +318,7 @@ class EventAuthorise(generic.UpdateView):
return super(EventAuthorise, self).get(request, *args, **kwargs) return super(EventAuthorise, self).get(request, *args, **kwargs)
def get_form(self, **kwargs): def get_form(self, **kwargs):
form = super(EventAuthorise, self).get_form(**kwargs) form = super().get_form(**kwargs)
form.instance.event = self.event form.instance.event = self.event
form.instance.email = self.request.email form.instance.email = self.request.email
form.instance.sent_by = self.request.sent_by form.instance.sent_by = self.request.sent_by
@@ -334,7 +334,7 @@ class EventAuthorise(generic.UpdateView):
except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist): except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist):
raise SuspiciousOperation( raise SuspiciousOperation(
"This URL is invalid. Please ask your TEC contact for a new URL") "This URL is invalid. Please ask your TEC contact for a new URL")
return super(EventAuthorise, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin): class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
@@ -344,7 +344,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
@method_decorator(decorators.nottinghamtec_address_required) @method_decorator(decorators.nottinghamtec_address_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(EventAuthorisationRequest, self).dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
@property @property
def object(self): def object(self):
@@ -405,13 +405,13 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
css = finders.find('css/email.css') css = finders.find('css/email.css')
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs) response = super().render_to_response(context, **response_kwargs)
assert isinstance(response, HttpResponse) assert isinstance(response, HttpResponse)
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform() response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
return response return response
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventAuthoriseRequestEmailPreview, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['hmac'] = signing.dumps({ context['hmac'] = signing.dumps({
'pk': self.object.pk, 'pk': self.object.pk,
'email': self.request.GET.get('email', 'hello@world.test'), 'email': self.request.GET.get('email', 'hello@world.test'),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 MiB

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

After

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

6
RIGS/static/js/marked.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -2,9 +2,11 @@
{% load static %} {% load static %}
{% load invoices_waiting from filters %} {% load invoices_waiting from filters %}
{% load invoices_outstanding from filters %}
{% load total_invoices_todo from filters %}
{% block titleheader %} {% block titleheader %}
<a class="navbar-brand" href="/">RIGS</a> <a class="navbar-brand" style="margin-left: auto; margin-right: auto;" href="/">RIGS</a>
{% endblock %} {% endblock %}
{% block titleelements %} {% block titleelements %}
@@ -45,14 +47,17 @@
{% endif %} {% endif %}
{% if perms.RIGS.view_invoice %} {% if perms.RIGS.view_invoice %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
{% total_invoices_todo as todo %}
{% invoices_waiting as waiting %}
{% invoices_outstanding as outstanding %}
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownInvoices" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownInvoices" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Invoices <span class="badge badge-danger badge-pill">{% invoices_waiting %}</span> Invoices <span class="badge {% if todo == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ todo }}</span>
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownInvoices"> <div class="dropdown-menu" aria-labelledby="navbarDropdownInvoices">
{% if perms.RIGS.add_invoice %} {% if perms.RIGS.add_invoice %}
<a class="dropdown-item text-nowrap" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting <span class="badge badge-danger badge-pill">{% invoices_waiting %}</span></a> <a class="dropdown-item text-nowrap" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting <span class="badge {% if waiting == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ waiting }}</span></a>
{% endif %} {% endif %}
<a class="dropdown-item" href="{% url 'invoice_list' %}"><span class="fas fa-pound-sign text-warning"></span> Outstanding</a> <a class="dropdown-item" href="{% url 'invoice_list' %}"><span class="fas fa-pound-sign text-warning"></span> Outstanding <span class="badge {% if outstanding == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ outstanding }}</span></a>
<a class="dropdown-item" href="{% url 'invoice_archive' %}"><span class="fas fa-book"></span> Archive</a> <a class="dropdown-item" href="{% url 'invoice_archive' %}"><span class="fas fa-book"></span> Archive</a>
</div> </div>
</li> </li>

View File

@@ -8,7 +8,7 @@
<div class="row"> <div class="row">
<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.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_eventchecklist review='ec_review' %}
</div> </div>
</div> </div>
@@ -102,6 +102,10 @@
<td>{{crew.role}}</td> <td>{{crew.role}}</td>
<td>{{crew.end}}</td> <td>{{crew.end}}</td>
</tr> </tr>
{% empty %}
<tr>
<td colspan="4" class="text-center bg-warning">Apparently this event happened by magic...</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@@ -109,9 +113,27 @@
</div> </div>
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header">Power {% include 'partials/event_size.html' with object=object.event.riskassessment %}</div> <div class="card-header">Power {% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
{% if object.event.riskassessment.event_size != 2 %}
<div class="card-body"> <div class="card-body">
{% if object.event.riskassessment.event_size == 1 %} {% if object.event.riskassessment.event_size == 0 %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
<dd class="col-2">
{{ object.rcds|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
<dd class="col-2">
{{ object.supply_test|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
<dd class="col-2">
{{ object.earthing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
<dd class="col-2">
{{ object.pat|yesnoi }}
</dd>
</dl>
{% else %}
<dl class="row"> <dl class="row">
<dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt> <dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt>
<dd class="col-2"> <dd class="col-2">
@@ -216,28 +238,8 @@
</dl> </dl>
<hr> <hr>
{% include 'partials/ec_power_info.html' %} {% include 'partials/ec_power_info.html' %}
{% else %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
<dd class="col-2">
{{ object.rcds|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
<dd class="col-2">
{{ object.supply_test|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
<dd class="col-2">
{{ object.earthing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
<dd class="col-2">
{{ object.pat|yesnoi }}
</dd>
</dl>
{% endif %} {% endif %}
</div> </div>
{% endif %}
</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='ec_edit' pk=object.pk %}

View File

@@ -244,12 +244,19 @@
</div> </div>
</div> </div>
</div> </div>
{% elif event.riskassessment.event_size == 1 %} {% else %}
<div class="row my-3" id="size-1"> <div class="row my-3" id="size-1">
<div class="col-12"> <div class="col-12">
{% if event.riskassessment.event_size == 1 %}
<div class="card border-warning"> <div class="card border-warning">
<div class="card-header">Electrical Checks <small>for Medium TEC Events </small></div> <div class="card-header">Electrical Checks <small>for Medium TEC Events </small></div>
<div class="card-body"> <div class="card-body">
{% else %}
<div class="card border-danger">
<div class="card-header">Electrical Checks <small>for Large TEC Events</small></div>
<div class="card-body">
<div class="alert alert-danger"><strong>Here be dragons. Ensure you have appeased the Power Gods before continuing... (If you didn't check with a Supervisor, <em>you cannot continue your event!</em>)</strong></div>
{% endif %}
{% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %} {% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %}
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %} {% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %} {% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
@@ -339,17 +346,6 @@
</div> </div>
</div> </div>
</div> </div>
{% else %}
<div class="row my-3" id="size-2">
<div class="col-12">
<div class="card border-danger">
<div class="card-header">Electrical Checks <small>for Large TEC Events</small></div>
<div class="card-body">
<p>Outside the scope of this assessment. <strong>I really hope you checked with a supervisor...</strong></p>
</div>
</div>
</div>
</div>
{% endif %} {% endif %}
<div class="row mt-3"> <div class="row mt-3">
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">

View File

@@ -1,5 +1,7 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %} {% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load markdown_tags %}
{% block content %} {% block content %}
<div class="row my-3 py-3"> <div class="row my-3 py-3">
{% if not request.is_ajax %} {% if not request.is_ajax %}
@@ -43,10 +45,10 @@
{% if perms.RIGS.view_event %} {% if perms.RIGS.view_event %}
<h4>Notes</h4> <h4>Notes</h4>
<hr> <hr>
<p class="dont-break-out">{{ event.notes|linebreaksbr }}</p> <p class="dont-break-out">{{ event.notes|markdown }}</p>
{% endif %} {% endif %}
<br> <br>
{% include 'item_table.html' %} {% include 'partials/item_table.html' %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,11 +8,13 @@
{% block css %} {% block css %}
{{ block.super }} {{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/> <link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/simplemde.min.css' %}">
{% endblock %} {% endblock %}
{% block preload_js %} {% block preload_js %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/selects.js' %}"></script> <script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/simplemde.min.js' %}"></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
@@ -63,11 +65,21 @@
{% endif %} {% endif %}
}); });
$(document).ready(function () { $(document).ready(function () {
setupMDE('#id_description');
setupMDE('#id_notes');
setupMDE('#item_description');
$('#itemModal').on('shown.bs.modal', function (e) {
$('#item_description').data('mde_editor').value(
$('#item_description').val()
);
});
setupItemTable($("#{{ form.items_json.id_for_label }}").val()); setupItemTable($("#{{ form.items_json.id_for_label }}").val());
}); });
$(function () { $(function () {
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
}) });
</script> </script>
{% endblock %} {% endblock %}
@@ -168,7 +180,7 @@
<label for="{{ form.description.id_for_label }}" <label for="{{ form.description.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.description.label }}</label> class="col-sm-4 col-form-label">{{ form.description.label }}</label>
<div class="col-sm-8"> <div class="col-sm-12">
{% render_field form.description class+="form-control" %} {% render_field form.description class+="form-control" %}
</div> </div>
</div> </div>
@@ -345,7 +357,7 @@
<div class="col-sm-12"> <div class="col-sm-12">
<div class="form-group" data-toggle="tooltip" title="Notes on the event. This is only visible to keyholders, and is not displayed on the paperwork"> <div class="form-group" data-toggle="tooltip" title="Notes on the event. This is only visible to keyholders, and is not displayed on the paperwork">
<label for="{{ form.notes.id_for_label }}">{{ form.notes.label }}</label> <label for="{{ form.notes.id_for_label }}">{{ form.notes.label }}</label>
{% render_field form.notes class+="form-control" %} {% render_field form.notes class+="form-control md-enabled" %}
</div> </div>
</div> </div>
{% include 'partials/item_table.html' %} {% include 'partials/item_table.html' %}

View File

@@ -74,6 +74,14 @@
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/> <lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/> <lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
</blockTableStyle> </blockTableStyle>
<listStyle name="ol"
bulletFormat="%s."
bulletFontSize="10" />
<listStyle name="ul"
start="bulletchar"
bulletFontSize="10"/>
</stylesheet> </stylesheet>
<template > {# Note: page is 595x842 points (1 point=1/72in) #} <template > {# Note: page is 595x842 points (1 point=1/72in) #}

View File

@@ -1,4 +1,6 @@
{% load markdown_tags %}
{% load filters %} {% load filters %}
<setNextFrame name="main"/> <setNextFrame name="main"/>
<nextFrame/> <nextFrame/>
<blockTable style="headLayout" colWidths="330,165"> <blockTable style="headLayout" colWidths="330,165">
@@ -10,10 +12,8 @@
<b>{{object.start_date|date:"D jS N Y"}}</b> <b>{{object.start_date|date:"D jS N Y"}}</b>
</para> </para>
<keepInFrame> <keepInFrame maxHeight="500" onOverflow="shrink">
<para style="style.event_description"> {{ object.description|default_if_none:""|markdown:"rml" }}
{{ object.description|default_if_none:""|linebreaksxml }}
</para>
</keepInFrame> </keepInFrame>
</td> </td>
<td> <td>
@@ -184,25 +184,27 @@
{% if item.description %} {% if item.description %}
</para> </para>
<para style="item_description"> <para style="item_description">
<em>{{ item.description|linebreaksxml }}</em> {{ item.description|markdown:"rml" }}
</para> </para>
<para> <para>
{% endif %} {% endif %}
</para> </para>
</td> </td>
<td>£ {{ item.cost|floatformat:2 }}</td> <td>£{{ item.cost|floatformat:2 }}</td>
<td>{{ item.quantity }}</td> <td>{{ item.quantity }}</td>
<td>£ {{ item.total_cost|floatformat:2 }}</td> <td>£{{ item.total_cost|floatformat:2 }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</blockTable> </blockTable>
<keepTogether> <keepTogether>
<blockTable style="totalTable" colWidths="300,115,80"> <blockTable style="totalTable" colWidths="300,115,80">
{% if object.vat > 0 %}
<tr> <tr>
<td>{% if quote %}VAT Registration Number: 170734807{% endif %}</td> <td>{% if quote %}VAT Registration Number: 170734807</td>
<td>Total (ex. VAT)</td> <td>Total (ex. VAT){% endif %}</td>
<td>£ {{ object.sum_total|floatformat:2 }}</td> <td>£ {{ object.sum_total|floatformat:2 }}</td>
</tr> </tr>
{% endif %}
<tr> <tr>
<td> <td>
{% if quote %} {% if quote %}
@@ -211,8 +213,10 @@
</para> </para>
{% endif %} {% endif %}
</td> </td>
{% if object.vat > 0 %}
<td>VAT @ {{ object.vat_rate.as_percent|floatformat:2 }}%</td> <td>VAT @ {{ object.vat_rate.as_percent|floatformat:2 }}%</td>
<td>£ {{ object.vat|floatformat:2 }}</td> <td>£{{ object.vat|floatformat:2 }}</td>
{% endif %}
</tr> </tr>
<tr> <tr>
<td> <td>
@@ -224,7 +228,7 @@
</td> </td>
{% if invoice %} {% if invoice %}
<td>Total</td> <td>Total</td>
<td>£ {{ object.total|floatformat:2 }}</td> <td>£{{ object.total|floatformat:2 }}</td>
{% else %} {% else %}
<td> <td>
<para> <para>
@@ -233,7 +237,7 @@
</td> </td>
<td> <td>
<para> <para>
<b>£ {{ object.total|floatformat:2 }}</b> <b>£{{ object.total|floatformat:2 }}</b>
</para> </para>
</td> </td>
{% endif %} {% endif %}
@@ -267,7 +271,7 @@
<tr> <tr>
<td>{{ payment.get_method_display }}</td> <td>{{ payment.get_method_display }}</td>
<td>{{ payment.date }}</td> <td>{{ payment.date }}</td>
<td>£ {{ payment.amount|floatformat:2 }}</td> <td>£{{ payment.amount|floatformat:2 }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</blockTable> </blockTable>
@@ -275,18 +279,18 @@
<tr> <tr>
<td></td> <td></td>
<td>Payment Total</td> <td>Payment Total</td>
<td>£ {{ object.invoice.payment_total|floatformat:2 }}</td> <td>£{{ object.invoice.payment_total|floatformat:2 }}</td>
</tr> </tr>
<tr> <tr>
<td></td> <td></td>
<td> <td>
<para> <para>
<b>Balance</b> (ex. VAT) <b>Balance</b> {% if object.vat > 0 %}(ex. VAT){% endif %}
</para> </para>
</td> </td>
<td> <td>
<para> <para>
<b>£ {{ object.invoice.balance|floatformat:2 }}</b> <b>£{{ object.invoice.balance|floatformat:2 }}</b>
</para> </para>
</td> </td>
</tr> </tr>
@@ -316,7 +320,7 @@
<tr> <tr>
<td>General Enquires and 24 Hour Emergency Contact: 0115 84 68720</td> <td>General Enquires and 24 Hour Emergency Contact: 0115 84 68720</td>
</tr> </tr>
{% else %} {% elif object.vat > 0 %}
<tr> <tr>
<td> <td>
<para>VAT Registration Number: 170734807</para> <para>VAT Registration Number: 170734807</para>

View File

@@ -5,15 +5,13 @@
<p>Hi {{ to_name|default:"there" }},</p> <p>Hi {{ to_name|default:"there" }},</p>
<p><b>{{ request.user.get_full_name }}</b> has requested that you authorise <b>{{ object.display_id }} <p><b>{{ request.user.get_full_name }}</b> has requested that you authorise <b>{{ object.display_id }}
| {{ object.name }}</b>{% if not to_name %} on behalf of <b>{{ object.person.name }}</b>{% endif %}.</p> | {{ object.name }}</b>{% if not to_name %} on behalf of <b>{% if object.person %}{{ object.person.name }}{% else %}{{ object.organisation.name }}{% endif %}</b>{% endif %}.</p>
<p> <p>
Please find the link below to complete the event booking process. Please find the link below to complete the event booking process.
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #} Remember that only Presidents or Treasurers are allowed to sign off payments. You may need to forward
Remember that only Presidents or Treasurers are allowed to sign off payments. You may need to forward this
this email on.
email on.
{% endif %}
</p> </p>

View File

@@ -1,6 +1,6 @@
Hi {{ to_name|default:"there" }}, Hi {{ to_name|default:"there" }},
{{ request.user.get_full_name }} has requested that you authorise N{{ object.pk|stringformat:"05d" }}| {{ object.name }}{% if not to_name %} on behalf of {{ object.person.name }}{% endif %}. {{ request.user.get_full_name }} has requested that you authorise N{{ object.pk|stringformat:"05d" }}| {{ object.name }}{% if not to_name %} on behalf of {% if object.person %}{{ object.person.name }}{% else %}{{ object.organisation.name }}{% endif %}{% endif %}.
Please find the link below to complete the event booking process. Please find the link below to complete the event booking process.
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #} {% if object.event.organisation and object.event.organisation.union_account %}{# internal #}

View File

@@ -1,24 +1,48 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %} {% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% load static %}
{% load button from filters %}
{% block title %}Request Authorisation{% endblock %} {% block title %}Request Authorisation{% endblock %}
{% block js %}
<script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/popover.js' %}"></script>
<script src="{% static 'js/clipboard.min.js' %}"></script>
<script>
var clipboard = new ClipboardJS('.btn');
clipboard.on('success', function(e) {
$(e.trigger).popover('show');
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
e.clearSelection();
});
</script>
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<div class="alert alert-warning"> <div class="alert alert-warning pb-0">
<h1>Send authorisation request email.</h1> <h1>Send authorisation request email.</h1>
<p>Pressing send will email the address provided. Please triple check everything before continuing.</p> <p>Pressing send will email the address provided. <strong>Please triple check everything before continuing.</strong></p>
</div> </div>
<div class="alert alert-info"> <div class="alert alert-info pb-0">
{% if object.person.email or object.organisation.email %}
<dl class="dl-horizontal"> <dl class="dl-horizontal">
{% if object.person.email %}
<dt>Person Email</dt> <dt>Person Email</dt>
<dd>{{ object.person.email }}</dd> <dd><span id="person-email">{{ object.person.email }}</span>{% button 'copy' id='#person-email' %}</dd>
{% endif %}
{% if object.organisation.email %}
<dt>Organisation Email</dt> <dt>Organisation Email</dt>
<dd>{{ object.organisation.email }}</dd> <dd><span id="org-email">{{ object.organisation.email }}</span>{% button 'copy' id='#org-email' %}</dd>
{% endif %}
</dl> </dl>
{% else %}
<p>No email addresses saved to the client &#3232;_&#3232;</p>
{% endif %}
</div> </div>
<form action="{{ form.action|default:request.path }}" method="POST" id="auth-request-form"> <form action="{{ form.action|default:request.path }}" method="POST" id="auth-request-form">
{% csrf_token %} {% csrf_token %}

View File

@@ -4,10 +4,11 @@
{% block content %} {% block content %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table mb-0"> <table class="table mb-0 table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">Event</th> <th scope="col">Event</th>
<th scope="col">MIC</th>
<th scope="col">Dates</th> <th scope="col">Dates</th>
<th scope="col">RA</th> <th scope="col">RA</th>
<th scope="col">Checklists</th> <th scope="col">Checklists</th>
@@ -16,7 +17,8 @@
<tbody> <tbody>
{% for event in object_list %} {% for event in object_list %}
<tr id="event_row"> <tr id="event_row">
<th scope="row" id="event_number"><a href="{% url 'event_detail' event.pk %}">{{ event }}</a></th> <th scope="row" id="event_number"><a href="{% url 'event_detail' event.pk %}">{{ event }}</a><br><small>{{ event.get_status_display }}</small></th>
<td>{% if event.mic is not None %}<a href="{% url 'profile_detail' event.mic.pk %}">{% else %}<span class="text-danger">{% endif %}{{ event.mic }}{% if event.mic is not None %}</a>{% else %}</span>{%endif%}</td>
<!--Dates--> <!--Dates-->
<td id="event_dates"> <td id="event_dates">
<span><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></span> <span><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></span>

View File

@@ -15,7 +15,7 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="table-responsive"> <div class="table-responsive">
<table class="table mb-0"> <table class="table mb-0 table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">Event</th> <th scope="col">Event</th>
@@ -32,7 +32,7 @@
{% for object in object_list %} {% for object in object_list %}
<tr class="{% if object.reviewed_by %}table-success{%endif%}"> <tr class="{% if object.reviewed_by %}table-success{%endif%}">
{# General #} {# General #}
<th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a></th> <th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a><br><small>{{ object.event.get_status_display }}</small></th>
{% for field in object_list.0.fieldz %} {% for field in object_list.0.fieldz %}
<td>{{ object|get_field:field }}</td> <td>{{ object|get_field:field }}</td>
{% endfor %} {% endfor %}

View File

@@ -5,7 +5,7 @@
<div class="row py-4"> <div class="row py-4">
<div class="col-sm-12 text-right px-0"> <div class="col-sm-12 text-right px-0">
<div class="btn-group"> <div class="btn-group">
<a href="{% url 'event_detail' object.pk %}" class="btn btn-primary">Open Event Page <span class="fas fa-eye"></span></a> <a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary">Open Event Page <span class="fas fa-eye"></span></a>
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-danger" title="Delete Invoice"> <a href="{% url 'invoice_delete' object.pk %}" class="btn btn-danger" title="Delete Invoice">
<span class="fas fa-times"></span> <span <span class="fas fa-times"></span> <span
class="d-none d-sm-inline">Delete</span> class="d-none d-sm-inline">Delete</span>

View File

@@ -1,27 +1,29 @@
<div class="col-sm-6"> <div class="col-sm-6">
{% if event.person %}
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header">Contact Details</div> <div class="card-header">Contact Details</div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-sm-5">Person</dt> <dt class="col-sm-5">Person</dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
{% if event.person %} {{ event.person.name }}
{{ event.person.name }}
{% endif %}
</dd> </dd>
{% if event.person.email %}
<dt class="col-sm-5">Email</dt> <dt class="col-sm-5">Email</dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
<span class="overflow-ellipsis">{{ event.person.email }}</span> <span class="overflow-ellipsis">{{ event.person.email }}</span>
</dd> </dd>
{% endif %}
{% if event.person.phone %}
<dt class="col-sm-5">Phone Number</dt> <dt class="col-sm-5">Phone Number</dt>
<dd class="col-sm-7">{{ event.person.phone }}</dd> <dd class="col-sm-7">{{ event.person.phone }}</dd>
{% endif %}
</dl> </dl>
</div> </div>
</div> </div>
{% endif %}
{% if event.organisation %} {% if event.organisation %}
<div class="card mt-3"> <div class="card">
<div class="card-header">Organisation Details</div> <div class="card-header">Organisation Details</div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
@@ -29,9 +31,10 @@
<dd class="col-sm-7"> <dd class="col-sm-7">
{{ event.organisation.name }} {{ event.organisation.name }}
</dd> </dd>
{% if event.organisation.phone %}
<dt class="col-sm-5">Phone Number</dt> <dt class="col-sm-5">Phone Number</dt>
<dd class="col-sm-7">{{ object.organisation.phone }}</dd> <dd class="col-sm-7">{{ event.organisation.phone }}</dd>
{% endif %}
</dl> </dl>
</div> </div>
</div> </div>
@@ -43,15 +46,12 @@
<div class="card-header">Event Info</div> <div class="card-header">Event Info</div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
{% if event.venue %}
<dt class="col-sm-5">Event Venue</dt> <dt class="col-sm-5">Event Venue</dt>
<dd class="col-sm-7"> <dd class="col-sm-7">
{% if object.venue %} {{ event.venue }}
<a href="{% url 'venue_detail' object.venue.pk %}" class="modal-href">
{{ object.venue }}
</a>
{% endif %}
</dd> </dd>
{% endif %}
<dt class="col-sm-5">Status</dt> <dt class="col-sm-5">Status</dt>
<dd class="col-sm-7">{{ event.get_status_display }}</dd> <dd class="col-sm-7">{{ event.get_status_display }}</dd>

View File

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

View File

@@ -1,4 +1,5 @@
{% load namewithnotes from filters %} {% load namewithnotes from filters %}
{% load markdown_tags %}
<div class="card card-info"> <div class="card card-info">
<div class="card-header">Event Info</div> <div class="card-header">Event Info</div>
<div class="card-body"> <div class="card-body">
@@ -46,7 +47,7 @@
<dd class="col-sm-12">&nbsp;</dd> <dd class="col-sm-12">&nbsp;</dd>
<dt class="col-sm-6">Event Description</dt> <dt class="col-sm-6">Event Description</dt>
<dd class="dont-break-out col-sm-12">{{ event.description|linebreaksbr }}</dd> <dd class="dont-break-out col-sm-12">{{ event.description|markdown }}</dd>
<dd class="col-sm-12">&nbsp;</dd> <dd class="col-sm-12">&nbsp;</dd>

View File

@@ -6,6 +6,10 @@
<span class="badge badge-success">PO: {{ event.purchase_order }}</span> <span class="badge badge-success">PO: {{ event.purchase_order }}</span>
{% elif event.authorised %} {% elif event.authorised %}
<span class="badge badge-success">Authorisation: Complete <span class="fas fa-check"></span></span> <span class="badge badge-success">Authorisation: Complete <span class="fas fa-check"></span></span>
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
<span class="badge badge-warning"> Authorisation: Issue <span class="fas fa-exclamation-circle"></span></span>
{% elif event.auth_request_to %}
<span class="badge badge-info"> Authorisation: Sent <span class="fas fa-paper-plane"></span></span>
{% else %} {% else %}
<span class="badge badge-danger">Authorisation: <span class="fas fa-times"></span></span> <span class="badge badge-danger">Authorisation: <span class="fas fa-times"></span></span>
{% endif %} {% endif %}

View File

@@ -67,9 +67,9 @@
</h4> </h4>
{% if event.is_rig and not event.cancelled %} {% if event.is_rig and not event.cancelled %}
<h5> <h5>
{{ event.person.name }} <a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
{% if event.organisation %} {% if event.organisation %}
for {{ event.organisation.name }} for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
{% endif %} {% endif %}
</h5> </h5>
{% endif %} {% endif %}

View File

@@ -16,10 +16,10 @@
id="item_name"/> id="item_name"/>
</div> </div>
</div> </div>
<div class="form-group form-row"> <div class="form-group form-row" data-toggle="tooltip" title="A detailed description of the kit. MD enabled.">
<label for="item_description" class="col-sm-2 col-form-label">Description</label> <label for="item_description" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10"> <div class="col-sm-10">
<textarea type="text" placeholder="Description" class="form-control" <textarea type="text" placeholder="Description" class="form-control md-enabled"
id="item_description" rows="8"></textarea> id="item_description" rows="8"></textarea>
</div> </div>
</div> </div>

View File

@@ -1,16 +1,17 @@
{% load markdown_tags %}
<tr id="item-{{item.pk}}" data-pk="{{item.pk}}" class="item_row"> <tr id="item-{{item.pk}}" data-pk="{{item.pk}}" class="item_row">
<th scope="row"> <th scope="row">
<span class="name">{{ item.name }}</span> <span class="name">{{ item.name }}</span>
<div class="item-description"> <div class="item-description">
<em class="description">{{item.description|linebreaksbr}}</em> <em class="description">{{item.description|markdown}}</em>
</div> </div>
</th> </th>
{% if perms.RIGS.view_event %} {% if perms.RIGS.view_event %}
<td>£&nbsp;<span class="cost">{{item.cost|floatformat:2}}</span></td> <td>£<span class="cost">{{item.cost|floatformat:2}}</span></td>
{% endif %} {% endif %}
<td class="quantity">{{item.quantity}}</td> <td class="quantity">{{item.quantity}}</td>
{% if perms.RIGS.view_event %} {% if perms.RIGS.view_event %}
<td>£&nbsp;<span class="sub-total" data-subtotal="{{item.total_cost}}">{{item.total_cost|floatformat:2}}</span></td> <td>£<span class="sub-total" data-subtotal="{{item.total_cost}}">{{item.total_cost|floatformat:2}}</span></td>
{% endif %} {% endif %}
{% if edit %} {% if edit %}
<td class="vert-align text-right"> <td class="vert-align text-right">

View File

@@ -23,16 +23,17 @@
</thead> </thead>
<tbody id="item-table-body"> <tbody id="item-table-body">
{% for item in object.items.all %} {% for item in object.items.all %}
{% include 'item_row.html' %} {% include 'partials/item_row.html' %}
{% endfor %} {% endfor %}
</tbody> </tbody>
{% if auth or perms.RIGS.view_event %} {% if auth or perms.RIGS.view_event %}
<tfoot> <tfoot style="font-weight: bold">
<tr> <tr>
<td rowspan="3" colspan="2"></td> <td rowspan="3" colspan="2"></td>
<td>Total (ex. VAT)</td> <td>Total {% if object.vat > 0 or not object.pk %}(ex. VAT){% endif %}</td>
<td colspan="2">£ <span id="sumtotal">{{object.sum_total|default:0|floatformat:2}}</span></td> <td colspan="2">£<span id="sumtotal">{{object.sum_total|default:0|floatformat:2}}</span></td>
</tr> </tr>
{% if object.vat > 0 or not object.pk %}
<tr> <tr>
{% if not object.pk %} {% if not object.pk %}
<td id="vat-rate" data-rate="{{currentVAT.rate}}">VAT @ <td id="vat-rate" data-rate="{{currentVAT.rate}}">VAT @
@@ -41,12 +42,13 @@
<td id="vat-rate" data-rate="{{object.vat_rate.rate}}">VAT @ <td id="vat-rate" data-rate="{{object.vat_rate.rate}}">VAT @
{{object.vat_rate.as_percent|floatformat|default:"TBD"}}%</td> {{object.vat_rate.as_percent|floatformat|default:"TBD"}}%</td>
{% endif %} {% endif %}
<td colspan="2">£ <span id="vat">{{object.vat|default:0|floatformat:2}}</span></td> <td colspan="2">£<span id="vat">{{object.vat|default:0|floatformat:2}}</span></td>
</tr> </tr>
<tr> <tr>
<td>Total</td> <td>Total</td>
<td colspan="2">£ <span id="total">{{object.total|default:0|floatformat:2}}</span></td> <td colspan="2">£<span id="total">{{object.total|default:0|floatformat:2}}</span></td>
</tr> </tr>
{% endif %}
</tfoot> </tfoot>
{% endif %} {% endif %}
</table> </table>
@@ -59,9 +61,9 @@
<em class="description"></em> <em class="description"></em>
</div> </div>
</td> </td>
<td>£&nbsp;<span class="cost"></span></td> <td>£<span class="cost"></span></td>
<td class="quantity"></td> <td class="quantity"></td>
<td>£&nbsp;<span class="sub-total"></span></td> <td>£<span class="sub-total"></span></td>
{% if edit %} {% if edit %}
<td class="vert-align text-right"> <td class="vert-align text-right">
<div class="btn-group" role="group" aria-label="Action buttons"> <div class="btn-group" role="group" aria-label="Action buttons">

View File

@@ -1,5 +1,4 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %} {% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% block title %}Risk Assessment for Event N{{ object.event.pk|stringformat:"05d" }} {{ object.event.name }}{% endblock %}
{% load help_text from filters %} {% load help_text from filters %}
{% load yesnoi from filters %} {% load yesnoi from filters %}
{% load linkornone from filters %} {% load linkornone from filters %}
@@ -7,7 +6,6 @@
{% block content %} {% block content %}
<div class="row py-3"> <div class="row py-3">
<div class="col-12"> <div class="col-12">
<h3>Risk Assessment for Event N{{ object.event.pk|stringformat:"05d" }} {{ object.event.name }}</h3>
<div class="card card-default mb-3"> <div class="card card-default mb-3">
<div class="card-header">General</div> <div class="card-header">General</div>
<div class="card-body"> <div class="card-body">
@@ -97,58 +95,64 @@
</dl> </dl>
</div> </div>
</div> </div>
<div class="card card-default mb-3"> <div class="row">
<div class="card-header">Site Details</div> <div class="col-lg-6 col-12">
<div class="card-body"> <div class="card card-default mb-3">
<dl class="row"> <div class="card-header">Site Details</div>
<dt class="col-sm-6">{{ object|help_text:'known_venue' }}</dt> <div class="card-body">
<dd class="col-sm-6"> <dl class="row">
{{ object.known_venue|yesnoi:'invert' }} <dt class="col-10">{{ object|help_text:'known_venue' }}</dt>
</dd> <dd class="col-2">
<dt class="col-sm-6">{{ object|help_text:'safe_loading'|safe }}</dt> {{ object.known_venue|yesnoi:'invert' }}
<dd class="col-sm-6"> </dd>
{{ object.safe_loading|yesnoi:'invert' }} <dt class="col-10">{{ object|help_text:'safe_loading'|safe }}</dt>
</dd> <dd class="col-2">
<dt class="col-sm-6">{{ object|help_text:'safe_storage' }}</dt> {{ object.safe_loading|yesnoi:'invert' }}
<dd class="col-sm-6"> </dd>
{{ object.safe_storage|yesnoi:'invert' }} <dt class="col-10">{{ object|help_text:'safe_storage' }}</dt>
</dd> <dd class="col-2">
<dt class="col-sm-6">{{ object|help_text:'area_outside_of_control' }}</dt> {{ object.safe_storage|yesnoi:'invert' }}
<dd class="col-sm-6"> </dd>
{{ object.area_outside_of_control|yesnoi:'invert' }} <dt class="col-10">{{ object|help_text:'area_outside_of_control' }}</dt>
</dd> <dd class="col-2">
<dt class="col-sm-6">{{ object|help_text:'barrier_required' }}</dt> {{ object.area_outside_of_control|yesnoi:'invert' }}
<dd class="col-sm-6"> </dd>
{{ object.barrier_required|yesnoi:'invert' }} <dt class="col-10">{{ object|help_text:'barrier_required' }}</dt>
</dd> <dd class="col-2">
<dt class="col-sm-6">{{ object|help_text:'nonstandard_emergency_procedure' }}</dt> {{ object.barrier_required|yesnoi:'invert' }}
<dd class="col-sm-6"> </dd>
{{ object.nonstandard_emergency_procedure|yesnoi:'invert' }} <dt class="col-10">{{ object|help_text:'nonstandard_emergency_procedure' }}</dt>
</dd> <dd class="col-2">
</dl> {{ object.nonstandard_emergency_procedure|yesnoi:'invert' }}
</dd>
</dl>
</div>
</div>
</div> </div>
</div> <div class="col-lg-6 col-12">
<div class="card card-default mb-3"> <div class="card card-default mb-3">
<div class="card-header">Structures</div> <div class="card-header">Structures</div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-sm-6">{{ object|help_text:'special_structures' }}</dt> <dt class="col-10">{{ object|help_text:'special_structures' }}</dt>
<dd class="col-sm-6"> <dd class="col-2">
{{ object.special_structures|yesnoi:'invert' }} {{ object.special_structures|yesnoi:'invert' }}
</dd> </dd>
<dt class="col-sm-6">{{ object|help_text:'suspended_structures' }}</dt> <dt class="col-10">{{ object|help_text:'suspended_structures' }}</dt>
<dd class="col-sm-6"> <dd class="col-2">
{{ object.suspended_structures|yesnoi:'invert' }} {{ object.suspended_structures|yesnoi:'invert' }}
</dd> </dd>
<dt class="col-sm-6">{{ object|help_text:'persons_responsible_structures' }}</dt> <dt class="col-12">{{ object|help_text:'persons_responsible_structures' }}</dt>
<dd class="col-sm-6"> <dd class="col-12">
{{ object.persons_responsible_structures.name|default:'N/A'|linebreaks }} {{ object.persons_responsible_structures.name|default:'N/A'|linebreaks }}
</dd> </dd>
<dt class="col-6">{{ object|help_text:'rigging_plan'|safe }}</dt> <dt class="col-12">{{ object|help_text:'rigging_plan'|safe }}</dt>
<dd class="col-6"> <dd class="col-12">
{{ object.rigging_plan|linkornone }} {{ object.rigging_plan|linkornone|default:'N/A' }}
</dd> </dd>
</dl> </dl>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,17 +5,14 @@
{% load nice_errors from filters %} {% load nice_errors from filters %}
{% block css %} {% block css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/> <link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
{% endblock %} {% endblock %}
{% block preload_js %} {% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}" async></script> <script src="{% static 'js/selects.js' %}" async></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script> <script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script> <script src="{% static 'js/tooltip.js' %}"></script>

View File

@@ -10,7 +10,6 @@ from django.utils.safestring import SafeData, mark_safe
from django.utils.text import normalize_newlines from django.utils.text import normalize_newlines
from RIGS import models from RIGS import models
from training import models as tmodels
register = template.Library() register = template.Library()
@@ -115,10 +114,8 @@ def orderby(request, field, attr):
return dict_.urlencode() return dict_.urlencode()
# Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
@register.filter(needs_autoescape=True)
def get_field(obj, field, autoescape=True): def get_field(obj, field, autoescape=True):
value = getattr(obj, field) value = getattr(obj, field)
if(isinstance(value, bool)): if(isinstance(value, bool)):
@@ -213,6 +210,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
clazz += " btn-primary " clazz += " btn-primary "
icon = "fa-plus" icon = "fa-plus"
text = "New" text = "New"
elif type == 'copy':
return {'copy': True, 'id': id, 'style': style}
elif type == 'search': elif type == 'search':
return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style} return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style}
elif type == 'submit': elif type == 'submit':
@@ -220,6 +219,16 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style} return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}
@register.simple_tag @register.simple_tag # TODO Can these be done with annotation/aggregation?
def invoices_waiting(): def invoices_waiting():
return len(models.Event.objects.waiting_invoices()) return len(models.Event.objects.waiting_invoices())
@register.simple_tag
def invoices_outstanding():
return len(models.Invoice.objects.outstanding_invoices())
@register.simple_tag
def total_invoices_todo():
return len(models.Event.objects.waiting_invoices()) + len(models.Invoice.objects.outstanding_invoices())

View File

@@ -0,0 +1,56 @@
from bs4 import BeautifulSoup
from django import template
from django.utils.safestring import mark_safe
import markdown
__author__ = 'ghost'
register = template.Library()
@register.filter(name="markdown")
def markdown_filter(text, input_format='html'):
# markdown library can't handle text=None
if text is None:
return text
html = markdown.markdown(text, extensions=['markdown.extensions.nl2br'])
# Convert format to RML
soup = BeautifulSoup(html, "html.parser")
# Prevent code injection
for script in soup('script'):
script.string = "Your script shall not pass!"
if input_format == 'html':
return mark_safe('<div class="markdown">' + str(soup) + '</div>')
elif input_format == 'rml':
# Image aren't supported so remove them
for img in soup('img'):
img.parent.extract()
# <code> should become <font>
for c in soup('code'):
c.name = 'font'
c['face'] = "Courier"
# blockquotes don't exist but we can still do something to show
for bq in soup('blockquote'):
bq.name = 'pre'
bq.string = bq.text
for alist in soup.find_all(['ul', 'ol']):
alist['style'] = alist.name
for li in alist.find_all('li', recursive=False):
text = li.find(text=True)
text.wrap(soup.new_tag('p'))
if alist.parent.name != 'li':
indent = soup.new_tag('indent')
indent['left'] = '0.6cm'
alist.wrap(indent)
# Paragraphs have a different tag
for p in soup('p'):
p.name = 'para'
return mark_safe(str(soup))

View File

@@ -96,7 +96,7 @@ class CreateEvent(FormPage):
_warning_selector = (By.XPATH, '/html/body/div[1]/div[1]') _warning_selector = (By.XPATH, '/html/body/div[1]/div[1]')
form_items = { form_items = {
'description': (regions.TextBox, (By.ID, 'id_description')), 'description': (regions.SimpleMDETextArea, (By.ID, 'id_description')),
'name': (regions.TextBox, (By.ID, 'id_name')), 'name': (regions.TextBox, (By.ID, 'id_name')),
'start_date': (regions.DatePicker, (By.ID, 'id_start_date')), 'start_date': (regions.DatePicker, (By.ID, 'id_start_date')),
@@ -110,7 +110,7 @@ class CreateEvent(FormPage):
'collected_by': (regions.TextBox, (By.ID, 'id_collector')), 'collected_by': (regions.TextBox, (By.ID, 'id_collector')),
'po': (regions.TextBox, (By.ID, 'id_purchase_order')), 'po': (regions.TextBox, (By.ID, 'id_purchase_order')),
'notes': (regions.TextBox, (By.ID, 'id_notes')) 'notes': (regions.SimpleMDETextArea, (By.ID, 'id_notes'))
} }
def select_event_type(self, type_name): def select_event_type(self, type_name):

View File

@@ -1,7 +1,7 @@
from pypom import Region from pypom import Region
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from PyRIGS.tests.regions import TextBox, Modal from PyRIGS.tests.regions import TextBox, Modal, SimpleMDETextArea
class Header(Region): class Header(Region):
@@ -42,7 +42,7 @@ class ItemModal(Modal):
form_items = { form_items = {
'name': (TextBox, (By.ID, 'item_name')), 'name': (TextBox, (By.ID, 'item_name')),
'description': (TextBox, (By.ID, 'item_description')), 'description': (SimpleMDETextArea, (By.ID, 'item_description')),
'quantity': (TextBox, (By.ID, 'item_quantity')), 'quantity': (TextBox, (By.ID, 'item_quantity')),
'price': (TextBox, (By.ID, 'item_cost')) 'price': (TextBox, (By.ID, 'item_cost'))
} }

View File

@@ -721,12 +721,12 @@ def test_ec_create_medium(logged_in_browser, live_server, admin_user, medium_ra)
page.fd_voltage_l2 = 235 page.fd_voltage_l2 = 235
page.fd_voltage_l3 = 0 page.fd_voltage_l3 = 0
page.fd_phase_rotation = True page.fd_phase_rotation = True
page.fd_earth_fault = 666 page.fd_earth_fault = "1.21"
page.fd_pssc = 1984 page.fd_pssc = 1984
page.w1_description = "In the carpark, by the bins" page.w1_description = "In the carpark, by the bins"
page.w1_polarity = True page.w1_polarity = True
page.w1_voltage = 240 page.w1_voltage = 240
page.w1_earth_fault = 333 page.w1_earth_fault = "0.42"
page.submit() page.submit()
assert page.success assert page.success

View File

@@ -3,6 +3,8 @@ from datetime import date
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils.safestring import SafeText
from RIGS.templatetags.markdown_tags import markdown_filter
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from pytest_django.asserts import assertRedirects, assertNotContains, assertContains from pytest_django.asserts import assertRedirects, assertNotContains, assertContains
@@ -170,6 +172,7 @@ class TestInvoiceDelete(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
is_active=True, is_staff=True) is_active=True, is_staff=True)
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
cls.events = { cls.events = {
1: models.Event.objects.create(name="TE E1", start_date=date.today()), 1: models.Event.objects.create(name="TE E1", start_date=date.today()),
2: models.Event.objects.create(name="TE E2", start_date=date.today()) 2: models.Event.objects.create(name="TE E2", start_date=date.today())
@@ -281,11 +284,11 @@ def test_xframe_headers(admin_client, basic_event):
response = admin_client.get(event_url, follow=True) response = admin_client.get(event_url, follow=True)
with pytest.raises(KeyError): with pytest.raises(KeyError):
response._headers["X-Frame-Options"] response.headers["X-Frame-Options"]
response = admin_client.get(login_url, follow=True) response = admin_client.get(login_url, follow=True)
with pytest.raises(KeyError): with pytest.raises(KeyError):
response._headers["X-Frame-Options"] response.headers["X-Frame-Options"]
def test_oembed(client, basic_event): def test_oembed(client, basic_event):
@@ -363,6 +366,215 @@ def test_checklist_review(admin_client, admin_user, checklist):
def test_ra_redirect(admin_client, admin_user, ra): def test_ra_redirect(admin_client, admin_user, ra):
request_url = reverse('event_ra', kwargs={'pk': ra.event.pk}) request_url = reverse('event_ra', kwargs={'pk': ra.event.pk})
expected_url = reverse('ra_edit', kwargs={'pk': ra.pk}) expected_url = reverse('ra_edit', kwargs={'pk': ra.pk})
response = admin_client.get(request_url, follow=True) response = admin_client.get(request_url, follow=True)
assertRedirects(response, expected_url, status_code=302, target_status_code=200) assertRedirects(response, expected_url, status_code=302, target_status_code=200)
class TestMarkdownTemplateTags(TestCase):
markdown = """
An h1 header
============
Paragraphs are separated by a blank line.
2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists
look like:
* this one
* that one
* the other one
Note that --- not considering the asterisk --- the actual text
content starts at 4-columns in.
> Block quotes are
> written like so.
>
> They can span multiple paragraphs,
> if you like.
Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all
in chapters 12--14"). Three dots ... will be converted to an ellipsis.
Unicode is supported.
An h2 header
------------
Here's a numbered list:
1. first item
2. second item
3. third item
Note again how the actual text starts at 4 columns in (4 characters
from the left side). Here's a code sample:
# Let me re-iterate ...
for i in 1 .. 10 { do-something(i) }
As you probably guessed, indented 4 spaces. By the way, instead of
indenting the block, you can use delimited blocks, if you like:
~~~
define foobar() {
print "Welcome to flavor country!";
}
~~~
(which makes copying & pasting easier). You can optionally mark the
delimited block for Pandoc to syntax highlight it:
~~~python
import time
# Quick, count to ten!
for i in range(10):
# (but not *too* quick)
time.sleep(0.5)
print i
~~~
### An h3 header ###
Now a nested list:
1. First, get these ingredients:
* carrots
* celery
* lentils
2. Boil some water.
3. Dump everything in the pot and follow
this algorithm:
find wooden spoon
uncover pot
stir
cover pot
balance wooden spoon precariously on pot handle
wait 10 minutes
goto first step (or shut off burner when done)
Do not bump wooden spoon or it will fall.
Notice again how text always lines up on 4-space indents (including
that last line which continues item 3 above).
Here's a link to [a website](http://foo.bar). Here's a footnote [^1].
[^1]: Footnote text goes here.
Tables can look like this:
size material color
---- ------------ ------------
9 leather brown
10 hemp canvas natural
11 glass transparent
Table: Shoes, their sizes, and what they're made of
(The above is the caption for the table.) Pandoc also supports
multi-line tables:
-------- -----------------------
keyword text
-------- -----------------------
red Sunsets, apples, and
other red or reddish
things.
green Leaves, grass, frogs
and other things it's
not easy being.
-------- -----------------------
A horizontal rule follows.
***
Here's a definition list:
apples
: Good for making applesauce.
oranges
: Citrus!
tomatoes
: There's no "e" in tomatoe.
Again, text is indented 4 spaces. (Put a blank line between each
term/definition pair to spread things out more.)
Here's a "line block":
| Line one
| Line too
| Line tree
and images can be specified like so:
![example image](example-image.jpg "An exemplary image")
Inline math equations go in like so: $\\omega = d\\phi / dt$. Display
math should get its own line and be put in in double-dollarsigns:
$$I = \\int \rho R^{2} dV$$
And note that you can backslash-escape any punctuation characters
which you wish to be displayed literally, ex.: \\`foo\\`, \\*bar\\*, etc.
"""
def test_html_safe(self):
html = markdown_filter(self.markdown)
self.assertIsInstance(html, SafeText)
def test_img_strip(self):
rml = markdown_filter(self.markdown, 'rml')
self.assertNotIn("<img", rml)
def test_code(self):
rml = markdown_filter(self.markdown, 'rml')
self.assertIn('<font face="Courier">monospace</font>', rml)
def test_blockquote(self):
rml = markdown_filter(self.markdown, 'rml')
self.assertIn("<pre>\nBlock quotes", rml)
def test_lists(self):
rml = markdown_filter(self.markdown, 'rml')
self.assertIn("<li><para>second item</para></li>", rml) # <ol>
self.assertIn("<li><para>that one</para></li>", rml) # <ul>
def test_in_print(self):
event = models.Event.objects.create(
name="MD Print Test",
description=self.markdown,
start_date='2016-01-01',
)
user = models.Profile.objects.create(
username='RML test',
is_superuser=True, # Don't care about permissions
is_active=True,
)
user.set_password('rmltester')
user.save()
self.assertTrue(self.client.login(username=user.username, password='rmltester'))
response = self.client.get(reverse('event_print', kwargs={'pk': event.pk}))
self.assertEqual(response.status_code, 200)
# By the time we have a PDF it should be larger than the original by some margin
# RML hard fails if something doesn't work
self.assertGreater(len(response.content), len(self.markdown))
def test_nonetype(self):
html = markdown_filter(None)
self.assertIsNone(html)
def test_linebreaks(self):
html = markdown_filter(self.markdown)
self.assertIn("Itemized lists<br/>\nlook like", html)

View File

@@ -6,7 +6,7 @@ class PersonList(GenericListView):
model = models.Person model = models.Person
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(PersonList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "People" context['page_title'] = "People"
context['create'] = 'person_create' context['create'] = 'person_create'
context['edit'] = 'person_update' context['edit'] = 'person_update'
@@ -19,7 +19,7 @@ class PersonDetail(GenericDetailView):
model = models.Person model = models.Person
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(PersonDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['history_link'] = 'person_history' context['history_link'] = 'person_history'
context['detail_link'] = 'person_detail' context['detail_link'] = 'person_detail'
context['update_link'] = 'person_update' context['update_link'] = 'person_update'
@@ -49,7 +49,7 @@ class OrganisationList(GenericListView):
model = models.Organisation model = models.Organisation
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(OrganisationList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['create'] = 'organisation_create' context['create'] = 'organisation_create'
context['edit'] = 'organisation_update' context['edit'] = 'organisation_update'
context['can_edit'] = self.request.user.has_perm('RIGS.change_organisation') context['can_edit'] = self.request.user.has_perm('RIGS.change_organisation')
@@ -62,7 +62,7 @@ class OrganisationDetail(GenericDetailView):
model = models.Organisation model = models.Organisation
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(OrganisationDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['history_link'] = 'organisation_history' context['history_link'] = 'organisation_history'
context['detail_link'] = 'organisation_detail' context['detail_link'] = 'organisation_detail'
context['update_link'] = 'organisation_update' context['update_link'] = 'organisation_update'
@@ -92,7 +92,7 @@ class VenueList(GenericListView):
model = models.Venue model = models.Venue
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(VenueList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['create'] = 'venue_create' context['create'] = 'venue_create'
context['edit'] = 'venue_update' context['edit'] = 'venue_update'
context['can_edit'] = self.request.user.has_perm('RIGS.change_venue') context['can_edit'] = self.request.user.has_perm('RIGS.change_venue')
@@ -104,7 +104,7 @@ class VenueDetail(GenericDetailView):
model = models.Venue model = models.Venue
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(VenueDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['history_link'] = 'venue_history' context['history_link'] = 'venue_history'
context['detail_link'] = 'venue_detail' context['detail_link'] = 'venue_detail'
context['update_link'] = 'venue_update' context['update_link'] = 'venue_update'

View File

@@ -1 +0,0 @@
default_app_config = 'assets.apps.AssetsAppConfig'

24
assets/converters.py Normal file
View File

@@ -0,0 +1,24 @@
import urllib.parse
class AssetIDConverter: # Forces lowercase to uppercase
regex = '[^/]+'
def to_python(self, value):
return str(value).upper()
def to_url(self, value):
return str(value).upper()
class ListConverter:
regex = '[^/]+'
def to_python(self, value):
return value.split(',')
def to_url(self, value):
string = ""
for i in value:
string += "," + str(i)
return string[1:]

View File

@@ -32,6 +32,8 @@ class AssetSearchForm(forms.Form):
q = forms.CharField(required=False) q = forms.CharField(required=False)
category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False) category = forms.ModelMultipleChoiceField(models.AssetCategory.objects.all(), required=False)
status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False) status = forms.ModelMultipleChoiceField(models.AssetStatus.objects.all(), required=False)
is_cable = forms.BooleanField(required=False)
date_acquired = forms.DateField(required=False)
class SupplierForm(forms.ModelForm): class SupplierForm(forms.ModelForm):
@@ -44,11 +46,3 @@ class CableTypeForm(forms.ModelForm):
class Meta: class Meta:
model = models.CableType model = models.CableType
fields = '__all__' fields = '__all__'
def clean(self): # TODO Does unique_together work better than this?
form_data = self.cleaned_data
queryset = models.CableType.objects.filter(Q(plug=form_data['plug']) & Q(socket=form_data['socket']) & Q(circuits=form_data['circuits']) & Q(cores=form_data['cores']))
# Being identical to itself shouldn't count...
if queryset.exists() and self.instance.pk != queryset[0].pk:
raise forms.ValidationError("A cable type that exactly matches this one already exists, please use that instead.", code="notunique")
return form_data

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.11 on 2022-01-12 19:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0021_auto_20210302_1204'),
]
operations = [
migrations.AlterUniqueTogether(
name='cabletype',
unique_together={('plug', 'socket', 'circuits', 'cores')},
),
]

View File

@@ -6,7 +6,8 @@ from django.urls import reverse
from reversion import revisions as reversion from reversion import revisions as reversion
from reversion.models import Version from reversion.models import Version
from RIGS.models import RevisionMixin, Profile from RIGS.models import Profile
from versioning.versioning import RevisionMixin
class AssetCategory(models.Model): class AssetCategory(models.Model):
@@ -49,7 +50,7 @@ class Supplier(models.Model, RevisionMixin):
ordering = ['name'] ordering = ['name']
def get_absolute_url(self): def get_absolute_url(self):
return reverse('supplier_list') return reverse('supplier_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return self.name return self.name
@@ -75,13 +76,17 @@ class CableType(models.Model):
class Meta: class Meta:
ordering = ['plug', 'socket', '-circuits'] ordering = ['plug', 'socket', '-circuits']
unique_together = ['plug', 'socket', 'circuits', 'cores']
def __str__(self): def __str__(self):
if self.plug and self.socket: if self.plug and self.socket:
return "%s%s" % (self.plug.description, self.socket.description) return f"{self.plug.description}{self.socket.description}"
else: else:
return "Unknown" return "Unknown"
def get_absolute_url(self):
return reverse('cable_type_detail', kwargs={'pk': self.pk})
def get_available_asset_id(wanted_prefix=""): def get_available_asset_id(wanted_prefix=""):
sql = """ sql = """
@@ -144,7 +149,7 @@ class Asset(models.Model, RevisionMixin):
] ]
def __str__(self): def __str__(self):
return "{} | {}".format(self.asset_id, self.description) return f"{self.asset_id} | {self.description}"
def get_absolute_url(self): def get_absolute_url(self):
return reverse('asset_detail', kwargs={'pk': self.asset_id}) return reverse('asset_detail', kwargs={'pk': self.asset_id})

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -64,16 +64,16 @@
<div class="form-group form-row"> <div class="form-group form-row">
{% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %} {% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %}
<div class="col-4"> <div class="col-4">
<button class="btn btn-danger" onclick="setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1">5{{ form.length.help_text }}</button> <button class="btn btn-danger" onclick="setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1" type="button">5{{ form.length.help_text }}</button>
<button class="btn btn-success" onclick="setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1">10{{ form.length.help_text }}</button> <button class="btn btn-success" onclick="setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ form.length.help_text }}</button>
<button class="btn btn-info" onclick="setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1">20{{ form.length.help_text }}</button> <button class="btn btn-info" onclick="setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1" type="button">20{{ form.length.help_text }}</button>
</div> </div>
</div> </div>
<div class="form-group form-row"> <div class="form-group form-row">
{% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text title='CSA' col="col-6" %} {% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text title='CSA' col="col-6" %}
<div class="col-4"> <div class="col-4">
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1">1.5{{ form.csa.help_text }}</button> <button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1" type="button">1.5{{ form.csa.help_text }}</button>
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1">2.5{{ form.csa.help_text }}</button> <button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -12,9 +12,7 @@
}); });
$('#searchButton').click(function (e) { $('#searchButton').click(function (e) {
e.preventDefault(); e.preventDefault();
var url = "{% url 'asset_audit' None %}"; var url = "{% url 'asset_audit' None %}".replace('None', $("#{{form.q.id_for_label}}").val());
var id = $("#{{form.q.id_for_label}}").val();
url = url.replace('None', id);
$.ajax({ $.ajax({
url: url, url: url,
success: function(){ success: function(){

View File

@@ -2,6 +2,9 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% block content %} {% block content %}
<div class="row justify-content-end">
{% include 'partials/asset_buttons.html' %}
</div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
{% include 'partials/asset_detail_form.html' %} {% include 'partials/asset_detail_form.html' %}

View File

@@ -5,11 +5,14 @@
{% block css %} {% block css %}
{{ block.super }} {{ block.super }}
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/> <link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/simplemde.min.css' %}">
{% endblock %} {% endblock %}
{% block preload_js %} {% block preload_js %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/selects.js' %}"></script> <script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/simplemde.min.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
@@ -34,7 +37,7 @@
}) })
.ajaxSelectPicker({ .ajaxSelectPicker({
ajax: { ajax: {
url: '{% url 'asset_search_json' %}', url: "{% url 'asset_search_json' %}",
type: "GET", type: "GET",
data: function () { data: function () {
let params = { let params = {
@@ -72,6 +75,11 @@
preserveSelected: false preserveSelected: false
}); });
</script> </script>
<script>
$(document).ready(function () {
setupMDE('#id_comments');
});
</script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@@ -1,6 +1,7 @@
{% extends 'base_assets.html' %} {% extends 'base_assets.html' %}
{% load paginator from filters %} {% load paginator from filters %}
{% load button from filters %} {% load button from filters %}
{% load ids_from_objects from asset_tags %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% load static %} {% load static %}
@@ -60,27 +61,54 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col px-0"> <div class="col px-0">
<form id="asset-search-form" method="GET" class="form-inline justify-content-end"> <form id="asset-search-form" method="GET">
<div class="input-group px-1 mb-2 mb-sm-0 flex-nowrap"> <div class="form-row">
{% render_field form.q|add_class:'form-control' placeholder='Enter Asset ID/Desc/Serial' %} <div class="col">
<label for="q" class="sr-only">Asset ID/Description/Serial Number:</label> <div class="input-group px-1 mb-2 mb-sm-0 flex-nowrap">
<span class="input-group-append">{% button 'search' id="id_search" %}</span> {% render_field form.q|add_class:'form-control' placeholder='Enter Asset ID/Desc/Serial' %}
</div> <label for="q" class="sr-only">Asset ID/Description/Serial Number:</label>
<div id="category-group" class="form-group px-1" style="margin-bottom: 0;"> <span class="input-group-append">{% button 'search' id="id_search" %}</span>
<label for="category" class="sr-only">Category</label> </div>
{% render_field form.category|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %} </div>
</div> </div>
<div id="status-group" class="form-group px-1" style="margin-bottom: 0;"> <div class="form-row mt-2">
<label for="status" class="sr-only">Status</label> <div class="col">
{% render_field form.status|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %} <div id="category-group" class="form-group px-1" style="margin-bottom: 0;">
</div> <label for="category" class="sr-only">Category</label>
<button id="filter-submit" type="submit" class="btn btn-secondary" style="width: 6em">Filter</button> {% render_field form.category|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Categories" data-header="Categories" data-actions-box="true" %}
</div>
</div>
<div class="col">
<div id="status-group" class="form-group px-1" style="margin-bottom: 0;">
<label for="status" class="sr-only">Status</label>
{% render_field form.status|attr:'multiple'|add_class:'form-control custom-select selectpicker col-sm' data-none-selected-text="Statuses" data-header="Statuses" data-actions-box="true" %}
</div>
</div>
<div class="col mt-2">
<div class="form-check form-check-inline">
{% render_field form.is_cable|add_class:'form-check-input' %}
<label class="form-check-label" for="is_cable">Only Cables?</label>
</div>
</div>
<div class="col-auto">
<div class="form-group d-flex flex-nowrap">
<label for="date_acquired" class="text-nowrap mt-auto">Date Acquired</label>
{% render_field form.date_acquired|add_class:'form-control mx-2' %}
</div>
</div>
<div class="col-auto mr-auto">
<button id="filter-submit" type="submit" class="btn btn-secondary" style="width: 6em">Filter</button>
</div>
</div>
</form> </form>
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">
<div class="col text-right px-0"> <div class="col text-right px-0">
{% button 'new' 'asset_create' style="width: 6em" %} {% button 'new' 'asset_create' style="width: 6em" %}
{% if object_list %}
<a class="btn btn-primary" href="{% url 'generate_labels' object_list|ids_from_objects %}"><span class="fas fa-barcode"></span> Generate Labels</a>
{% endif %}
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE document SYSTEM "rml.dtd">
{% load multiply from filters %}
{% load index from asset_tags %}
<document filename="{{filename}}">
<template>
<pageTemplate id="main">
<pageGraphics>
</pageGraphics>
<frame id="first" x1="5" y1="-10" width="581" height="842"/>
</pageTemplate>
</template>
<stylesheet>
<blockTableStyle id="table">
<!-- show a grid: this also comes in handy for debugging your tables.-->
<lineStyle kind="GRID" colorName="black" thickness="1" start="0,0" stop="-1,-1" />
</blockTableStyle>
</stylesheet>
<story>
<blockTable style="table">
{% for i in images0 %}
<tr>
<td>{% with images0|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
<td>{% with images1|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
<td>{% with images2|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
<td>{% with images3|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
</tr>
{% endfor %}
</blockTable>
</story>
</document>

View File

@@ -11,7 +11,8 @@
<div class="btn-group"> <div class="btn-group">
{% button 'edit' url='asset_update' pk=object.asset_id %} {% button 'edit' url='asset_update' pk=object.asset_id %}
{% button 'duplicate' url='asset_duplicate' pk=object.asset_id %} {% button 'duplicate' url='asset_duplicate' pk=object.asset_id %}
<a type="button" class="btn btn-info" href="{% url 'asset_audit' object.asset_id %}"><i class="fas fa-certificate"></i> Audit</a> <a type="button" class="btn btn-info" href="{% url 'asset_audit' object.asset_id %}"><span class="fas fa-certificate"></span> Audit</a>
<a type="button" class="btn btn-primary" href="{% url 'generate_label' object.asset_id %}"><span class="fas fa-barcode"></span> Generate Label</a>
</div> </div>
{% endif %} {% endif %}
{% if create or edit or duplicate %} {% if create or edit or duplicate %}

View File

@@ -1,4 +1,6 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% load markdown_tags %}
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
Asset Details Asset Details
@@ -38,14 +40,14 @@
<!---TODO: Lower default number of lines in comments box--> <!---TODO: Lower default number of lines in comments box-->
<div class="form-group"> <div class="form-group">
<label for="{{ form.comments.id_for_label }}">Comments</label> <label for="{{ form.comments.id_for_label }}">Comments</label>
{% render_field form.comments|add_class:'form-control' %} {% render_field form.comments|add_class:'form-control md-enabled' %}
</div> </div>
{% else %} {% else %}
<dt>Asset ID</dt> <dt>Asset ID</dt>
<dd>{{ object.asset_id }}</dd> <dd>{{ object.asset_id }}</dd>
<dt>Description</dt> <dt>Description</dt>
<dd style="overflow-wrap: break-word;">{{ object.description }}</dd> <dd>{{ object.description }}</dd>
<dt>Category</dt> <dt>Category</dt>
<dd>{{ object.category }}</dd> <dd>{{ object.category }}</dd>
@@ -57,7 +59,7 @@
<dd>{{ object.serial_number|default:'-' }}</dd> <dd>{{ object.serial_number|default:'-' }}</dd>
<dt>Comments</dt> <dt>Comments</dt>
<dd style="overflow-wrap: break-word;">{{ object.comments|default:'-'|linebreaksbr }}</dd> <dd style="overflow-wrap: break-word;">{{ object.comments|default:'-'|markdown }}</dd>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@@ -17,11 +17,9 @@
{% else %} {% else %}
<dl> <dl>
<dt>Cable Type</dt> <dt>Cable Type</dt>
<dd>{{ object.cable_type|default_if_none:'-' }}</dd> <dd>{% if object.cable_type %}<a href="{{object.cable_type.get_absolute_url}}">{{ object.cable_type }}</a>{%else%}-{%endif%}</dd>
<dt>Length</dt> <dt>Length</dt>
<dd>{{ object.length|default_if_none:'-' }}m</dd> <dd>{{ object.length|default_if_none:'-' }}m</dd>
<dt>Cross Sectional Area</dt> <dt>Cross Sectional Area</dt>
<dd>{{ object.csa|default_if_none:'-' }}mm²</dd> <dd>{{ object.csa|default_if_none:'-' }}mm²</dd>
</dl> </dl>

View File

@@ -28,13 +28,13 @@
<dt>Children</dt> <dt>Children</dt>
{% if object.asset_parent.all %} {% if object.asset_parent.all %}
<div style="max-height: 200px; overflow-y: auto; -webkit-overflow-scrolling: touch; ">
{% for child in object.asset_parent.all %} {% for child in object.asset_parent.all %}
<dd> <dd>
<a href="{% url 'asset_detail' child.asset_id %}"> <a href="{% url 'asset_detail' child.asset_id %}">{{ child }}</a>
{{ child.asset_id }} - {{ child.description }}
</a>
</dd> </dd>
{% endfor %} {% endfor %}
</div>
{% else %} {% else %}
<dd><span>-</span></dd> <dd><span>-</span></dd>
{% endif %} {% endif %}

View File

@@ -1,4 +1,5 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% load linkornone from filters %}
<div class="card mb-2"> <div class="card mb-2">
<div class="card-header"> <div class="card-header">
Purchase Details Purchase Details
@@ -7,11 +8,26 @@
{% if create or edit or duplicate %} {% if create or edit or duplicate %}
<div class="form-group" id="purchased-from-group"> <div class="form-group" id="purchased-from-group">
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label> <label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}"> <div class="row">
{% if object.purchased_from %} <div class="col">
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option> <select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
{% endif %} {% if object.purchased_from %}
</select> <option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
{% endif %}
</select>
</div>
<div class="col align-right">
<div class="btn-group">
<a href="{% url 'supplier_create' %}" class="btn btn-success modal-href"
data-target="#{{ form.purchased_from.id_for_label }}">
<span class="fas fa-plus"></span>
</a>
<a {% if form.supplier.value %}href="{% url 'supplier_update' form.purchased_from.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.purchased_from.id_for_label }}-update" data-target="#{{ form.purchased_from.id_for_label }}">
<span class="fas fa-edit"></span>
</a>
</div>
</div>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -51,14 +67,11 @@
{% else %} {% else %}
<dl> <dl>
<dt>Purchased From</dt> <dt>Purchased From</dt>
<dd>{{ object.purchased_from|default_if_none:'-' }}</dd> <dd>{% if object.purchased_from %}<a href="{{object.purchased_from.get_absolute_url}}">{{ object.purchased_from }}</a>{%else%}-{%endif%}</dd>
<dt>Purchase Price</dt> <dt>Purchase Price</dt>
<dd>£{{ object.purchase_price|default_if_none:'-' }}</dd> <dd>£{{ object.purchase_price|default_if_none:'-' }}</dd>
<dt>Salvage Value</dt> <dt>Salvage Value</dt>
<dd>£{{ object.salvage_value|default_if_none:'-' }}</dd> <dd>£{{ object.salvage_value|default_if_none:'-' }}</dd>
<dt>Date Acquired</dt> <dt>Date Acquired</dt>
<dd>{{ object.date_acquired|default_if_none:'-' }}</dd> <dd>{{ object.date_acquired|default_if_none:'-' }}</dd>
{% if object.date_sold %} {% if object.date_sold %}

View File

@@ -0,0 +1,17 @@
from django import template
from assets import models
register = template.Library()
@register.filter
def ids_from_objects(object_list):
id_list = []
for obj in object_list:
id_list.append(obj.asset_id)
return id_list
@register.filter
def index(indexable, i):
return indexable[i] if i < len(indexable) else None

View File

@@ -70,14 +70,14 @@ class AssetList(BasePage):
class AssetForm(FormPage): class AssetForm(FormPage):
_purchased_from_select_locator = (By.CSS_SELECTOR, 'div#purchased-from-group>div.bootstrap-select') _purchased_from_select_locator = (By.XPATH, '//div[@id="purchased-from-group"]/div/div/div')
_parent_select_locator = (By.CSS_SELECTOR, 'div#parent-group>div.bootstrap-select') _parent_select_locator = (By.CSS_SELECTOR, 'div#parent-group>div.bootstrap-select')
form_items = { form_items = {
'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')), 'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')),
'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.TextBox, (By.ID, 'id_comments')), 'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')), 'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')), 'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')), 'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),

View File

@@ -180,7 +180,7 @@ class TestAssetForm(AutoLoginTest):
def test_asset_edit(self): def test_asset_edit(self):
self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open() self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
self.assertTrue(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly') is not None) self.assertIsNotNone(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly'))
new_description = "Big Shelf" new_description = "Big Shelf"
self.page.description = new_description self.page.description = new_description
@@ -335,7 +335,7 @@ class TestAssetAudit(AutoLoginTest):
self.assertNotIn(self.asset.asset_id, self.page.assets) self.assertNotIn(self.asset.asset_id, self.page.assets)
def test_audit_list(self): def test_audit_list(self):
self.assertEqual(len(models.Asset.objects.filter(last_audited_at=None)), len(self.page.assets)) self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets))
asset_row = self.page.assets[0] asset_row = self.page.assets[0]
self.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click() self.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click()
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal'))) self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))

View File

@@ -64,11 +64,11 @@ def test_x_frame_headers(client, django_user_model, test_asset):
response = client.get(asset_url, follow=True) response = client.get(asset_url, follow=True)
with pytest.raises(KeyError): with pytest.raises(KeyError):
response._headers["X-Frame-Options"] response.headers["X-Frame-Options"]
response = client.get(login_url, follow=True) response = client.get(login_url, follow=True)
with pytest.raises(KeyError): with pytest.raises(KeyError):
response._headers["X-Frame-Options"] response.headers["X-Frame-Options"]
def test_oembed(client, test_asset): def test_oembed(client, test_asset):
@@ -105,7 +105,6 @@ def test_asset_edit(admin_client, test_asset):
def test_cable_edit(admin_client, test_cable): def test_cable_edit(admin_client, test_cable):
url = reverse('asset_update', kwargs={'pk': test_cable.asset_id}) url = reverse('asset_update', kwargs={'pk': test_cable.asset_id})
# TODO Why do I have to send is_cable=True here?
response = admin_client.post(url, {'is_cable': True, 'length': -3, 'csa': -3}) response = admin_client.post(url, {'is_cable': True, 'length': -3, 'csa': -3})
# TODO Can't figure out how to select the 'none' option... # TODO Can't figure out how to select the 'none' option...

View File

@@ -1,21 +1,26 @@
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.urls import path from django.urls import path, register_converter
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from PyRIGS.decorators import has_oembed, permission_required_with_403 from PyRIGS.decorators import has_oembed, permission_required_with_403
from PyRIGS.views import OEmbedView from PyRIGS.views import OEmbedView
from assets import views from . import views, converters
register_converter(converters.AssetIDConverter, 'asset')
register_converter(converters.ListConverter, 'list')
urlpatterns = [ urlpatterns = [
path('', login_required(views.AssetList.as_view()), name='asset_index'), path('', login_required(views.AssetList.as_view()), name='asset_index'),
path('asset/list/', login_required(views.AssetList.as_view()), name='asset_list'), path('asset/list/', login_required(views.AssetList.as_view()), name='asset_list'),
path('asset/id/<str:pk>/', has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name='asset_detail'), path('asset/id/<asset:pk>/', has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name='asset_detail'),
path('asset/create/', permission_required_with_403('assets.add_asset') path('asset/create/', permission_required_with_403('assets.add_asset')
(views.AssetCreate.as_view()), name='asset_create'), (views.AssetCreate.as_view()), name='asset_create'),
path('asset/id/<str:pk>/edit/', permission_required_with_403('assets.change_asset') path('asset/id/<asset:pk>/edit/', permission_required_with_403('assets.change_asset')
(views.AssetEdit.as_view()), name='asset_update'), (views.AssetEdit.as_view()), name='asset_update'),
path('asset/id/<str:pk>/duplicate/', permission_required_with_403('assets.add_asset') path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset')
(views.AssetDuplicate.as_view()), name='asset_duplicate'), (views.AssetDuplicate.as_view()), name='asset_duplicate'),
path('asset/id/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'),
path('asset/<list:ids>/list/label', views.GenerateLabels.as_view(), name='generate_labels'),
path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'), path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'),
path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'), path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),

View File

@@ -1,4 +1,8 @@
import simplejson import simplejson
import random
import base64
from io import BytesIO
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core import serializers from django.core import serializers
@@ -9,6 +13,14 @@ 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 django.views.decorators.csrf import csrf_exempt
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from PyPDF2 import PdfFileMerger, PdfFileReader
from PIL import Image, ImageDraw, ImageFont
from barcode import Code39
from barcode.writer import ImageWriter
from z3c.rml import rml2pdf
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \ from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \
is_ajax, OEmbedView is_ajax, OEmbedView
@@ -42,9 +54,15 @@ class AssetList(LoginRequiredMixin, generic.ListView):
queryset = self.model.objects.all() queryset = self.model.objects.all()
elif len(query_string) >= 3: elif len(query_string) >= 3:
queryset = self.model.objects.filter( queryset = self.model.objects.filter(
Q(asset_id__exact=query_string) | Q(description__icontains=query_string) | Q(serial_number__exact=query_string)) Q(asset_id__exact=query_string.upper()) | Q(description__icontains=query_string) | Q(serial_number__exact=query_string))
else: else:
queryset = self.model.objects.filter(Q(asset_id__exact=query_string)) queryset = self.model.objects.filter(Q(asset_id__exact=query_string.upper()))
if form.cleaned_data['is_cable']:
queryset = queryset.filter(is_cable=True)
if form.cleaned_data['date_acquired']:
queryset = queryset.filter(date_acquired=form.cleaned_data['date_acquired'])
if form.cleaned_data['category']: if form.cleaned_data['category']:
queryset = queryset.filter(category__in=form.cleaned_data['category']) queryset = queryset.filter(category__in=form.cleaned_data['category'])
@@ -58,7 +76,7 @@ class AssetList(LoginRequiredMixin, generic.ListView):
return queryset.select_related('category', 'status') return queryset.select_related('category', 'status')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AssetList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["form"] = self.form context["form"] = self.form
if hasattr(self.form, 'cleaned_data'): if hasattr(self.form, 'cleaned_data'):
context["category_filters"] = self.form.cleaned_data.get('category') context["category_filters"] = self.form.cleaned_data.get('category')
@@ -99,7 +117,7 @@ class AssetDetail(LoginRequiredMixin, AssetIDUrlMixin, generic.DetailView):
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"] = "Asset {}".format(self.object.display_id) context["page_title"] = f"Asset {self.object.display_id}"
return context return context
@@ -112,7 +130,7 @@ class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["edit"] = True context["edit"] = True
context["connectors"] = models.Connector.objects.all() context["connectors"] = models.Connector.objects.all()
context["page_title"] = "Edit Asset: {}".format(self.object.display_id) context["page_title"] = f"Edit Asset: {self.object.display_id}"
return context return context
def get_success_url(self): def get_success_url(self):
@@ -132,7 +150,7 @@ class AssetCreate(LoginRequiredMixin, generic.CreateView):
form_class = forms.AssetForm form_class = forms.AssetForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AssetCreate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["create"] = True context["create"] = True
context["connectors"] = models.Connector.objects.all() context["connectors"] = models.Connector.objects.all()
context["page_title"] = "Create Asset" context["page_title"] = "Create Asset"
@@ -159,8 +177,9 @@ class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["create"] = None context["create"] = None
context["duplicate"] = True context["duplicate"] = True
context['previous_asset_id'] = self.get_object().asset_id old_id = self.get_object().asset_id
context["page_title"] = "Duplication of Asset: {}".format(context['previous_asset_id']) context['previous_asset_id'] = old_id
context["page_title"] = f"Duplication of Asset: {old_id}"
return context return context
@@ -183,7 +202,7 @@ class AssetAuditList(AssetList):
return self.model.objects.filter(Q(last_audited_at__isnull=True)).select_related('category', 'status') return self.model.objects.filter(Q(last_audited_at__isnull=True)).select_related('category', 'status')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AssetAuditList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['page_title'] = "Asset Audit List" context['page_title'] = "Asset Audit List"
return context return context
@@ -194,7 +213,7 @@ class AssetAudit(AssetEdit):
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"] = "Audit Asset: {}".format(self.object.display_id) context["page_title"] = f"Audit Asset: {self.object.display_id}"
return context return context
def get_success_url(self): def get_success_url(self):
@@ -211,7 +230,7 @@ class SupplierList(GenericListView):
ordering = ['name'] ordering = ['name']
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SupplierList, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['create'] = 'supplier_create' context['create'] = 'supplier_create'
context['edit'] = 'supplier_update' context['edit'] = 'supplier_update'
context['can_edit'] = self.request.user.has_perm('assets.change_supplier') context['can_edit'] = self.request.user.has_perm('assets.change_supplier')
@@ -238,7 +257,7 @@ class SupplierDetail(GenericDetailView):
model = models.Supplier model = models.Supplier
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SupplierDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['history_link'] = 'supplier_history' context['history_link'] = 'supplier_history'
context['update_link'] = 'supplier_update' context['update_link'] = 'supplier_update'
context['detail_link'] = 'supplier_detail' context['detail_link'] = 'supplier_detail'
@@ -257,7 +276,7 @@ class SupplierCreate(GenericCreateView, ModalURLMixin):
form_class = forms.SupplierForm form_class = forms.SupplierForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SupplierCreate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if is_ajax(self.request): if is_ajax(self.request):
context['override'] = "base_ajax.html" context['override'] = "base_ajax.html"
else: else:
@@ -303,8 +322,8 @@ class CableTypeDetail(generic.DetailView):
template_name = 'cable_type_detail.html' template_name = 'cable_type_detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CableTypeDetail, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["page_title"] = "Cable Type {}".format(str(self.object)) context["page_title"] = f"Cable Type {self.object}"
return context return context
@@ -314,7 +333,7 @@ class CableTypeCreate(generic.CreateView):
form_class = forms.CableTypeForm form_class = forms.CableTypeForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CableTypeCreate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["create"] = True context["create"] = True
context["page_title"] = "Create Cable Type" context["page_title"] = "Create Cable Type"
@@ -330,11 +349,92 @@ class CableTypeUpdate(generic.UpdateView):
form_class = forms.CableTypeForm form_class = forms.CableTypeForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CableTypeUpdate, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["edit"] = True context["edit"] = True
context["page_title"] = "Edit Cable Type" context["page_title"] = f"Edit Cable Type {self.object}"
return context return context
def get_success_url(self): def get_success_url(self):
return reverse("cable_type_detail", kwargs={"pk": self.object.pk}) return reverse("cable_type_detail", kwargs={"pk": self.object.pk})
def generate_label(pk):
black = (0, 0, 0)
white = (255, 255, 255)
size = (700, 200)
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20)
obj = get_object_or_404(models.Asset, asset_id=pk)
asset_id = f"Asset: {obj.asset_id}"
if obj.is_cable:
length = f"Length: {obj.length}m"
csa = f"CSA: {obj.csa}mm²"
image = Image.new("RGB", size, white)
logo = Image.open("static/imgs/square_logo.png")
draw = ImageDraw.Draw(image)
draw.text((210, 140), asset_id, fill=black, font=font)
if obj.is_cable:
draw.text((210, 170), length, fill=black, font=font)
draw.text((360, 170), csa, fill=black, font=font)
draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", fill=black, font=font)
barcode = Code39(str(obj.asset_id), writer=ImageWriter())
logo_size = (200, 200)
image.paste(logo.resize(logo_size, Image.ANTIALIAS))
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
width, height = barcode_image.size
image.paste(barcode_image.crop((0, 0, width, 135)), (int(((size[0] + logo_size[0]) - width) / 2), 0))
return image
class GenerateLabel(generic.View): # TODO Caching
def get(self, request, pk):
response = HttpResponse(content_type="image/png")
generate_label(pk).save(response, "PNG")
return response
class GenerateLabels(generic.View):
def get(self, request, ids):
response = HttpResponse(content_type='application/pdf')
template = get_template('labels_print.xml')
images = []
for asset_id in ids:
image = generate_label(asset_id)
in_mem_file = BytesIO()
image.save(in_mem_file, format="PNG")
# reset file pointer to start
in_mem_file.seek(0)
img_bytes = in_mem_file.read()
base64_encoded_result_bytes = base64.b64encode(img_bytes)
base64_encoded_result_str = base64_encoded_result_bytes.decode('ascii')
images.append(base64_encoded_result_str)
context = {
'images0': images[::4],
'images1': images[1::4],
'images2': images[2::4],
'images3': images[3::4],
'filename': "Asset Label Sheet generated at {}".format(timezone.now())
}
merger = PdfFileMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
merged = BytesIO()
merger.write(merged)
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
response.write(merged.getvalue())
return response

View File

@@ -2,9 +2,7 @@ from django.conf import settings
import django import django
import pytest import pytest
from django.core.management import call_command from django.core.management import call_command
from RIGS.models import VatRate, Profile from RIGS.models import VatRate
import random
from django.db import connection
from PyRIGS.tests import pages from PyRIGS.tests import pages
import os import os
from selenium import webdriver from selenium import webdriver

View File

@@ -3,7 +3,7 @@
var gulp = require('gulp'); var gulp = require('gulp');
const terser = require('gulp-uglify'); const terser = require('gulp-uglify');
const sass = require('gulp-sass'); const sass = require('gulp-sass')(require('node-sass'));
const flatten = require('gulp-flatten'); const flatten = require('gulp-flatten');
const autoprefixer = require('autoprefixer') const autoprefixer = require('autoprefixer')
const postcss = require('gulp-postcss') const postcss = require('gulp-postcss')
@@ -15,8 +15,6 @@ const cssnano = require('cssnano');
const con = require('gulp-concat'); const con = require('gulp-concat');
const gulpif = require('gulp-if'); const gulpif = require('gulp-if');
sass.compiler = require('node-sass');
function fonts(done) { function fonts(done) {
return gulp.src('node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.*') return gulp.src('node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.*')
.pipe(gulp.dest('pipeline/built_assets/fonts')) .pipe(gulp.dest('pipeline/built_assets/fonts'))
@@ -29,7 +27,9 @@ function styles(done) {
'node_modules/fullcalendar/main.css', 'node_modules/fullcalendar/main.css',
'node_modules/bootstrap-select/dist/css/bootstrap-select.css', 'node_modules/bootstrap-select/dist/css/bootstrap-select.css',
'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css', 'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css',
'node_modules/flatpickr/dist/flatpickr.css',]) 'node_modules/flatpickr/dist/flatpickr.css',
'node_modules/simplemde/dist/simplemde.min.css'
])
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
.pipe(sass().on('error', sass.logError)) .pipe(sass().on('error', sass.logError))
.pipe(gulpif(function(file) { return bs_select.includes(file.relative);}, con('selects.css'))) .pipe(gulpif(function(file) { return bs_select.includes(file.relative);}, con('selects.css')))
@@ -64,6 +64,7 @@ function scripts() {
'node_modules/fullcalendar/main.js', 'node_modules/fullcalendar/main.js',
'node_modules/bootstrap-select/dist/js/bootstrap-select.js', 'node_modules/bootstrap-select/dist/js/bootstrap-select.js',
'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js', 'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js',
'node_modules/simplemde/dist/simplemde.min.js',
'node_modules/konami/konami.js', 'node_modules/konami/konami.js',
'pipeline/source_assets/js/**/*.js',]) 'pipeline/source_assets/js/**/*.js',])
.pipe(gulpif(function(file) { return base_scripts.includes(file.relative);}, con('base.js'))) .pipe(gulpif(function(file) { return base_scripts.includes(file.relative);}, con('base.js')))

7782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,35 +5,37 @@
"author": "Tom Price", "author": "Tom Price",
"license": "Custom", "license": "Custom",
"dependencies": { "dependencies": {
"@forevolve/bootstrap-dark": "^1.0.0-alpha.1075", "@forevolve/bootstrap-dark": "^2.1.0",
"@fortawesome/fontawesome-free": "^5.15.2", "@fortawesome/fontawesome-free": "^5.15.4",
"ajax-bootstrap-select": "^1.4.5", "ajax-bootstrap-select": "^1.4.5",
"autocompleter": "^6.0.3", "autocompleter": "^6.1.2",
"autoprefixer": "^9.8.0", "autoprefixer": "^10.4.0",
"bootstrap": "^4.5.2", "bootstrap": "^4.5.2",
"bootstrap-select": "^1.13.17", "bootstrap-select": "^1.13.17",
"clipboard": "^2.0.6", "clipboard": "^2.0.8",
"cssnano": "^4.1.10", "cssnano": "^5.0.13",
"flatpickr": "^4.6.6", "flatpickr": "^4.6.6",
"fullcalendar": "^5.3.2", "fullcalendar": "^5.10.1",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-flatten": "^0.4.0", "gulp-flatten": "^0.4.0",
"gulp-if": "^3.0.0", "gulp-if": "^3.0.0",
"gulp-postcss": "^8.0.0", "gulp-postcss": "^9.0.1",
"gulp-sass": "^4.1.0", "gulp-sass": "^5.0.0",
"gulp-sourcemaps": "^2.6.5", "gulp-sourcemaps": "^3.0.0",
"gulp-uglify": "^3.0.2", "gulp-uglify": "^3.0.2",
"html5sortable": "^0.10.0", "html5sortable": "^0.13.3",
"jquery": "^3.5.1", "jquery": "^3.6.0",
"konami": "^1.6.2", "konami": "^1.6.3",
"moment": "^2.27.0", "moment": "^2.27.0",
"node-sass": "^5.0.0", "node-sass": "^7.0.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"uglify-js": "^3.12.6" "postcss": "^8.4.5",
"simplemde": "^1.11.2",
"uglify-js": "^3.14.5"
}, },
"devDependencies": { "devDependencies": {
"browser-sync": "^2.26.12" "browser-sync": "^2.27.7"
}, },
"scripts": { "scripts": {
"gulp": "gulp", "gulp": "gulp",

View File

@@ -1,3 +1,7 @@
marked.setOptions({
breaks: true,
})
function setupItemTable(items_json) { function setupItemTable(items_json) {
objectitems = JSON.parse(items_json) objectitems = JSON.parse(items_json)
$.each(objectitems, function (key, val) { $.each(objectitems, function (key, val) {
@@ -6,12 +10,12 @@ function setupItemTable(items_json) {
newitem = -1; newitem = -1;
} }
function nl2br (str, is_xhtml) { function nl2br(str, is_xhtml) {
var breakTag = (is_xhtml || typeof is_xhtml === 'undefined') ? '<br />' : '<br>'; 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'); 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();
} }
@@ -32,6 +36,16 @@ function updatePrices() {
$('#total').text(parseFloat(sum + vat).toFixed(2)); $('#total').text(parseFloat(sum + vat).toFixed(2));
} }
function setupMDE(selector) {
editor = new SimpleMDE({
element: $(selector)[0],
forceSync: true,
toolbar: ["bold", "italic", "strikethrough", "|", "unordered-list", "ordered-list", "|", "link", "|", "preview", "guide"],
status: true,
});
$(selector).data('mde_editor',editor);
}
$('#item-table').on('click', '.item-delete', function () { $('#item-table').on('click', '.item-delete', function () {
delete objectitems[$(this).data('pk')] delete objectitems[$(this).data('pk')]
$('#item-' + $(this).data('pk')).remove(); $('#item-' + $(this).data('pk')).remove();
@@ -106,7 +120,7 @@ $('body').on('submit', '#item-form', function (e) {
// update the table // update the table
$row = $('#item-' + pk); $row = $('#item-' + pk);
$row.find('.name').html(escapeHtml(fields.name)); $row.find('.name').html(escapeHtml(fields.name));
$row.find('.description').html(nl2br(escapeHtml(fields.description))); $row.find('.description').html(marked(fields.description));
$row.find('.cost').html(parseFloat(fields.cost).toFixed(2)); $row.find('.cost').html(parseFloat(fields.cost).toFixed(2));
$row.find('.quantity').html(fields.quantity); $row.find('.quantity').html(fields.quantity);

View File

@@ -28,6 +28,9 @@
color: $gray-100 !important; color: $gray-100 !important;
border-color: $darktheme; border-color: $darktheme;
} }
.btn-link {
color: white;
}
.bs-popover-right > .arrow::after { .bs-popover-right > .arrow::after {
border-right-color: $darktheme; border-right-color: $darktheme;
} }
@@ -133,4 +136,21 @@
-webkit-box-shadow: 0 0 0px 1000px rgba($info, .3) inset; -webkit-box-shadow: 0 0 0px 1000px rgba($info, .3) inset;
transition: background-color 5000s ease-in-out 0s; transition: background-color 5000s ease-in-out 0s;
} }
.editor-toolbar > a {
color: white !important;
}
.editor-toolbar > a:hover {
background: transparent !important;
}
.editor-toolbar > a.active {
background: $info !important;
}
.cm-s-paper {
color: white;
background-color: $darktheme;
border-color: #bbb;
}
.CodeMirror-cursor {
border-color: white !important;
}
} }

View File

@@ -226,3 +226,33 @@ html.embedded {
max-width: 3em; max-width: 3em;
} }
} }
.markdown {
h1 {
font-size: $h1-font-size * 0.75;
}
h2 {
font-size: $h2-font-size * 0.8;
}
h3 {
font-size: $h3-font-size * 0.85;
}
h4 {
font-size: $h4-font-size * 0.9;
}
h5 {
font-size: $h5-font-size * 0.95;
}
img {
max-width: 100%;
}
}
#rigboard {
.markdown {
img {
max-width: 30rem;
}
}
}

View File

@@ -31,28 +31,26 @@
<a class="skip-link" href='#main'>Skip to content</a> <a class="skip-link" href='#main'>Skip to content</a>
{% include "analytics.html" %} {% include "analytics.html" %}
{% block navbar %} {% block navbar %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark flex-nowrap text-nowrap" role="navigation"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark" role="navigation">
<a class="navbar-brand" href="{% if request.user.is_authenticated %}https://members.nottinghamtec.co.uk{%else%}https://nottinghamtec.co.uk{%endif%}"> <div class="container">
<img src="{% static 'imgs/logo.webp' %}" width="40" height="40" alt="TEC's Logo: Serif 'TEC' vertically next to a blue box with the words 'PA and Lighting', surrounded by graduated rings" id="logo"> <a class="navbar-brand" style="position: absolute; left:0.5em; top: 2px;" href="{% if request.user.is_authenticated %}https://rigs.nottinghamtec.co.uk{%else%}https://nottinghamtec.co.uk{%endif%}">
</a> <img src="{% static 'imgs/logo.webp' %}" width="40" height="40" alt="TEC's Logo: Serif 'TEC' vertically next to a blue box with the words 'PA and Lighting', surrounded by graduated rings" id="logo">
<div class="container" style="padding-left: 0; padding-right: 0;"> </a>
<div class="row w-100 flex-nowrap">
{% block titleheader %} {% block titleheader %}
{% endblock %} {% endblock %}
<button class="navbar-toggler ml-auto" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation" onclick="document.getElementById('logo').classList.toggle('d-none');"> <button class="navbar-toggler ml-auto" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation" onclick="document.getElementById('logo').classList.toggle('d-none');">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse justify-content-between" id="navbarSupportedContent">
<ul class="navbar-nav"> <ul class="navbar-nav">
{% block titleelements %} {% block titleelements %}
{% endblock %} {% endblock %}
</ul> </ul>
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav align-self-end">
{% block titleelements_right %} {% block titleelements_right %}
{% endblock %} {% endblock %}
</ul> </ul>
</div> </div>
</div>
</div> </div>
</nav> </nav>
{% endblock %} {% endblock %}
@@ -79,8 +77,11 @@
</div> </div>
<div class="modal fade" id="modal" role="dialog" tabindex=-1></div> <div class="modal fade" id="modal" role="dialog" tabindex=-1></div>
<script src="{% static 'js/base.js' %}"></script> <script src="{% static 'js/base.js' %}"></script>
<script src="{% static 'js/marked.min.js' %}"></script>
{% include 'partials/dark_theme.html' %} {% include 'partials/dark_theme.html' %}
{% block js %} {% block js %}
{% endblock %} {% endblock %}
</body> </body>

View File

@@ -1,7 +1,7 @@
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">{{page_title}}{% block title %}{% endblock %}</h4> <h4 class="modal-title">{{page_title|safe}}{% block title %}{% endblock %}</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>

View File

@@ -43,20 +43,21 @@
<img class="card-img-top" src="{% static 'imgs/training.jpg' %}" alt="" style="height: 150px; object-fit: cover;"> <img class="card-img-top" src="{% static 'imgs/training.jpg' %}" alt="" style="height: 150px; object-fit: cover;">
<h4 class="card-header">Training Database</h4> <h4 class="card-header">Training Database</h4>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<a class="list-group-item list-group-item-action text-info" href="{% url 'trainee_detail' %}"><span class="fas fa-file-signature align-middle"></span><span class="align-middle"> My Training Record</span></a> <a class="list-group-item list-group-item-action text-info" href="{% url 'trainee_detail' request.user.pk %}"><span class="fas fa-file-signature align-middle"></span><span class="align-middle"> My Training Record</span></a>
<a class="list-group-item list-group-item-action" href="{% url 'trainee_list' %}"><span class="fas fa-clipboard-list align-middle"></span><span class="align-middle"> View Training Records</span></a> <a class="list-group-item list-group-item-action" href="{% url 'trainee_list' %}"><span class="fas fa-users"></span> Trainee List</a>
<a class="list-group-item list-group-item-action" href="{% url 'item_list' %}"><span class="fas fa-eye align-middle"></span><span class="align-middle"> View Training Items</span></a> <a class="list-group-item list-group-item-action" href="{% url 'level_list' %}"><span class="fas fa-layer-group"></span> Level List</a></a>
<a class="list-group-item list-group-item-action" href="{% url 'session_log' %}"><span class="fas fa-plus align-middle"></span><span class="align-middle"> Log Training Session</span></a> <a class="list-group-item list-group-item-action" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> Item List</a></a>
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm mb-3"> <div class="col-sm mb-3">
<div class="card"> <div class="card">
<h4 class="card-header">Quick Links</h4> <h4 class="card-header">Quick Links</h4>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<a class="list-group-item list-group-item-action" href="https://forum.nottinghamtec.co.uk" target="_blank" rel="noopener noreferrer"><span class="fas fa-comment-alt text-info align-middle"></span><span class="align-middle"> TEC Forum</span></a> <a class="list-group-item list-group-item-action" href="https://forum.nottinghamtec.co.uk" target="_blank" rel="noopener noreferrer"><span class="fas fa-comment-alt text-primary align-middle"></span><span class="align-middle"> TEC Forum</span></a>
<a class="list-group-item list-group-item-action" href="//nottinghamtec.sharepoint.com" target="_blank" rel="noopener noreferrer"><span class="fas fa-folder text-info align-middle"></span><span class="align-middle"> TEC Sharepoint</span></a>
<a class="list-group-item list-group-item-action" href="//wiki.nottinghamtec.co.uk" target="_blank" rel="noopener noreferrer"><span class="fas fa-pen-square align-middle"></span><span class="align-middle"> TEC Wiki</span></a> <a class="list-group-item list-group-item-action" href="//wiki.nottinghamtec.co.uk" target="_blank" rel="noopener noreferrer"><span class="fas fa-pen-square align-middle"></span><span class="align-middle"> TEC Wiki</span></a>
{% if perms.RIGS.view_event %} {% if perms.RIGS.change_event %}
<a class="list-group-item list-group-item-action" href="//members.nottinghamtec.co.uk/price" target="_blank" rel="noopener noreferrer"><span class="fas fa-pound-sign text-warning align-middle"></span><span class="align-middle"> Price List</span></a> <a class="list-group-item list-group-item-action" href="//members.nottinghamtec.co.uk/price" target="_blank" rel="noopener noreferrer"><span class="fas fa-pound-sign text-warning align-middle"></span><span class="align-middle"> Price List</span></a>
{% endif %} {% endif %}
</div> </div>

View File

@@ -2,6 +2,8 @@
<button type="submit" class="btn {{ class }}" title="{{ text }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></button> <button type="submit" class="btn {{ class }}" title="{{ text }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></button>
{% elif pk %} {% elif pk %}
<a href="{% url target pk %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%} {% if text == 'Print' %}target="_blank"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a> <a href="{% url target pk %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%} {% if text == 'Print' %}target="_blank"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
{% elif copy %}
<button class="btn btn-secondary btn-sm mr-1" data-clipboard-target="{{id}}" data-content="Copied to clipboard!"><span class="fas fa-copy"></span></button>
{% else %} {% else %}
<a href="{% url target %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a> <a href="{% url target %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
{% endif %} {% endif %}

View File

@@ -1,29 +1,36 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% load static %}
{% block title %}Registration{% endblock %} {% block title %}Registration{% endblock %}
{% block content %} {% block content %}
<div class="col-sm-10 col-sm-offset-1"> <div style="background-image: linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), url({% static 'imgs/wof2014-1-small.jpg' %}); background-repeat: no-repeat; background-size: cover; width: 100vw; height: 100vh; position: relative; left: 50%; right: 50%; margin-left: -50vw; margin-right: -50vw; margin-top: -24px; padding-top: 24px;">
<h3>New User Registration</h3> <div class="container">
{% if form.errors or supplement_form.errors %} <div class="card">
<div class="alert alert-danger"> <h3 class="card-header">New User Registration</h3>
{{form.errors}} <div class="card-body">
{{supplement_form.errors}} {% if form.errors or supplement_form.errors %}
</div> <div class="alert alert-danger">
{% endif %} {{form.errors}}
{{supplement_form.errors}}
<div class="col-sm-8 col-sm-offset-2"> </div>
<form action="" method="post" class="" role="form">{% csrf_token %} {% endif %}
{% for field in form %} <div class="col-sm-8 col-sm-offset-2">
<div class="form-group"> <form method="post" role="form">{% csrf_token %}
<label for="{{ field.id_for_label }}" class="col-form-label col-sm-4">{{ field.label }}</label> {% for field in form %}
<div class="controls col-sm-8"> <div class="form-group form-row">
{% render_field field class+="form-control" placeholder=field.label %} <label for="{{ field.id_for_label }}" class="col-form-label col-sm-4">{{ field.label }}</label>
</div> <div class="controls col-sm-8">
</div> {% render_field field class+="form-control" placeholder=field.label %}
{% endfor %} </div>
<p><input type="submit" value="Register" class="btn btn-primary pull-right"></p> </div>
</form> {% endfor %}
<p><input type="submit" value="Register" class="btn btn-primary pull-right"></p>
</form>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -2,7 +2,10 @@ from django.contrib import admin
from training import models from training import models
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
#admin.site.register(models.Trainee, VersionAdmin) # admin.site.register(models.Trainee, VersionAdmin)
admin.site.register(models.TrainingCategory, VersionAdmin) admin.site.register(models.TrainingCategory, VersionAdmin)
admin.site.register(models.TrainingItem, VersionAdmin) admin.site.register(models.TrainingItem, VersionAdmin)
admin.site.register(models.TrainingLevel, VersionAdmin) admin.site.register(models.TrainingLevel, VersionAdmin)
admin.site.register(models.TrainingItemQualification, VersionAdmin)
admin.site.register(models.TrainingLevelQualification, VersionAdmin)
admin.site.register(models.TrainingLevelRequirement, VersionAdmin)

5
training/decorators.py Normal file
View File

@@ -0,0 +1,5 @@
from PyRIGS.decorators import user_passes_test_with_403
def has_perm_or_supervisor(perm, login_url=None, oembed_view=None):
return user_passes_test_with_403(lambda u: (hasattr(u, 'is_supervisor') and u.is_supervisor) or u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)

View File

@@ -1,13 +1,8 @@
from django import forms from django import forms
from datetime import date
from training import models from training import models
from RIGS.models import Profile from RIGS.models import Profile
class SessionLogForm(forms.Form):
pass
class QualificationForm(forms.ModelForm): class QualificationForm(forms.ModelForm):
class Meta: class Meta:
@@ -16,9 +11,9 @@ class QualificationForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
pk = kwargs.pop('pk', None) pk = kwargs.pop('pk', None)
super(QualificationForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['trainee'].initial = Profile.objects.get(pk=pk) self.fields['trainee'].initial = Profile.objects.get(pk=pk)
self.fields['date'].initial = date.today() self.fields['date'].widget.format = '%Y-%m-%d'
def clean_date(self): def clean_date(self):
date = self.cleaned_data['date'] date = self.cleaned_data['date']
@@ -34,6 +29,7 @@ class QualificationForm(forms.ModelForm):
raise forms.ValidationError('Selected supervisor must actually *be* a supervisor...') raise forms.ValidationError('Selected supervisor must actually *be* a supervisor...')
return supervisor return supervisor
class RequirementForm(forms.ModelForm): class RequirementForm(forms.ModelForm):
depth = forms.ChoiceField(choices=models.TrainingItemQualification.CHOICES) depth = forms.ChoiceField(choices=models.TrainingItemQualification.CHOICES)
@@ -43,5 +39,5 @@ class RequirementForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
pk = kwargs.pop('pk', None) pk = kwargs.pop('pk', None)
super(RequirementForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['level'].initial = models.TrainingLevel.objects.get(pk=pk) self.fields['level'].initial = models.TrainingLevel.objects.get(pk=pk)

View File

@@ -2,6 +2,7 @@ import datetime
import random import random
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.core.management import call_command
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
@@ -31,33 +32,174 @@ class Command(BaseCommand):
self.setup_categories() self.setup_categories()
self.setup_items() self.setup_items()
self.setup_levels() self.setup_levels()
# call_command('generate_sample_training_users')
print("Done generating training data") print("Done generating training data")
def setup_categories(self): def setup_categories(self):
names = [(1, "Basic"), (2, "Sound"), (3, "Lighting"), (4, "Rigging"), (5, "Power"), (6, "Haulage")] names = [(1, "Basic"), (2, "Sound"), (3, "Lighting"), (4, "Rigging"), (5, "Power"), (6, "Haulage")]
for i, name in names: for i, name in names:
category = models.TrainingCategory.objects.create(reference_number=i, name=name) category = models.TrainingCategory.objects.create(reference_number=i, name=name)
category.save() category.save()
self.categories.append(category) self.categories.append(category)
def setup_items(self): def setup_items(self):
names = ["Motorised Power Towers", "Catering", "Forgetting Cables", "Gazebo Construction", "Balanced Audio", "Unbalanced Audio", "BBQ/Bin Interactions", "Pushing Boxes", "How Not To Die", "Setting up projectors", "Basketing truss", "First Aid", "Digging Trenches", "Avoiding Bin Lorries", "Getting cherry pickers stuck in mud", "Crashing the Van"] names = [
"Motorised Power Towers",
for i,name in enumerate(names): "Catering",
item = models.TrainingItem.objects.create(category=random.choice(self.categories), reference_number=random.randint(0, 100), name=name) "Forgetting Cables",
"Gazebo Construction",
"Balanced Audio",
"Unbalanced Audio",
"BBQ/Bin Interactions",
"Pushing Boxes",
"How Not To Die",
"Setting up projectors",
"Basketing truss",
"First Aid",
"Digging Trenches",
"Avoiding Bin Lorries",
"Getting cherry pickers stuck in mud",
"Crashing the Van",
"Getting pigs to fly",
"Basketing picnics",
"Python programming",
"Building Cables",
"Unbuilding Cables",
"Cat Herding",
"Pancake making",
"Tidying up",
"Reading Manuals",
"Bikeshedding",
"DJing",
"Partying",
"Teccie Gym",
"Putting dust covers on",
"Cleaning Lights",
"Water Skiing",
"Drinking",
"Fundamentals of Audio",
"Fundamentals of Photons",
"Social Interaction",
"Discourse Searching",
"Discord Searching",
"Coiling Cables",
"Kit Amnesties",
"Van Insurance",
"Subhire Insurance",
"Paperwork",
"More Paperwork",
"Second Aid",
"Being Old",
"Maxihoists",
"Sleazyhoists",
"Telehoists",
"Prolyte",
"Prolights",
"Making Phonecalls",
"Quoting For A Rig",
"Basic MIC",
"Advanced MIC",
"Avoiding MIC",
"Washing Cables",
"Cable Ramp",
"Van Loading",
"Trailer Loading",
"Storeroom Loading",
"Welding",
"Fire Extinguishers",
"Boring Conference AV",
"Flyaway",
"Short Leads",
"RF Systems",
"QLab",
"Use of Ladders",
"Working at Height",
"Organising Training",
"Organising Organising Training Training",
"Mental Health First Aid",
"Writing RAMS",
"Makros Runs",
"PAT",
"Kit Fixing",
"Kit Breaking",
"Replacing Lamps",
"Flying Pig Systems",
"Procrastination",
"Drinking Beer",
"Sending Emails",
"Email Signatures",
"Digital Sound Desks",
"Digital Lighting Desks",
"Painting PS10s",
"Chain Lubrication",
"Big Power",
"BIGGER POWER",
"Pixel Mapping",
"RDM",
"Ladder Inspections",
"Losing Crimpaz",
"Scrapping Trilite",
"Bin Diving",
"Wiki Editing"]
for i, name in enumerate(names):
category = random.choice(self.categories)
previous_item = models.TrainingItem.objects.filter(category=category).last()
if previous_item is not None:
number = previous_item.reference_number + 1
else:
number = 0
item = models.TrainingItem.objects.create(category=category, reference_number=number, name=name)
self.items.append(item) self.items.append(item)
def setup_levels(self): def setup_levels(self):
self.levels.append(models.TrainingLevel.objects.create(level=models.TrainingLevel.TA, description="Passion will hatred faithful evil suicide noble battle. Truth aversion gains grandeur noble. Dead play gains prejudice god ascetic grandeur zarathustra dead good. Faithful ultimate justice overcome love will mountains inexpedient.")) items = self.items.copy()
for i,name in models.TrainingLevel.DEPARTMENTS: ta = models.TrainingLevel.objects.create(
technician = models.TrainingLevel.objects.create(level=models.TrainingLevel.TECHNICIAN, department=i, description="Moral pinnacle derive ultimate war dead. Strong fearful joy contradict battle christian faithful enlightenment prejudice zarathustra moral.") level=models.TrainingLevel.TA,
supervisor = models.TrainingLevel.objects.create(level=models.TrainingLevel.SUPERVISOR, department=i, description="Spirit holiest merciful mountains inexpedient reason value. Suicide ultimate hope.") description="Passion will hatred faithful evil suicide noble battle. Truth aversion gains grandeur noble. Dead play gains prejudice god ascetic grandeur zarathustra dead good. Faithful ultimate justice overcome love will mountains inexpedient.",
supervisor.prerequisite_levels.add(technician) icon="address-card")
self.levels.append(ta)
tech_ccs = models.TrainingLevel.objects.create(
level=models.TrainingLevel.TECHNICIAN,
description="Technician Common Competencies. Spirit abstract endless insofar horror sexuality depths war decrepit against strong aversion revaluation free. Christianity reason joy sea law mountains transvaluation. Sea battle aversion dead ultimate morality self. Faithful morality.",
icon="book-reader")
tech_ccs.prerequisite_levels.add(ta)
super_ccs = models.TrainingLevel.objects.create(level=models.TrainingLevel.SUPERVISOR, description="Depths disgust hope faith of against hatred will victorious. Law...", icon="user-graduate")
for i in range(0, 5):
if len(items) == 0:
break
item = random.choice(items)
items.remove(item)
if i % 3 == 0:
models.TrainingLevelRequirement.objects.create(level=tech_ccs, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
else:
models.TrainingLevelRequirement.objects.create(level=super_ccs, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
icons = {
models.TrainingLevel.SOUND: ('microphone', 'microphone-alt'),
models.TrainingLevel.LIGHTING: ('lightbulb', 'traffic-light'),
models.TrainingLevel.POWER: ('plug', 'bolt'),
models.TrainingLevel.RIGGING: ('link', 'pallet'),
models.TrainingLevel.HAULAGE: ('truck', 'route'),
}
for i, name in models.TrainingLevel.DEPARTMENTS:
technician = models.TrainingLevel.objects.create(level=models.TrainingLevel.TECHNICIAN, department=i, description="Moral pinnacle derive ultimate war dead. Strong fearful joy contradict battle christian faithful enlightenment prejudice zarathustra moral.", icon=icons[i][0])
technician.prerequisite_levels.add(tech_ccs)
supervisor = models.TrainingLevel.objects.create(level=models.TrainingLevel.SUPERVISOR, department=i, description="Spirit holiest merciful mountains inexpedient reason value. Suicide ultimate hope.", icon=icons[i][1])
supervisor.prerequisite_levels.add(super_ccs, technician)
for i in range(0, 30): for i in range(0, 30):
if i % 3 == 0: if len(items) == 0:
models.TrainingLevelRequirement.objects.create(level=technician, item=random.choice(self.items), depth=random.choice(models.TrainingItemQualification.CHOICES)[0]) break
else: item = random.choice(items)
models.TrainingLevelRequirement.objects.create(level=supervisor, item=random.choice(self.items), depth=random.choice(models.TrainingItemQualification.CHOICES)[0]) items.remove(item)
try:
if i % 3 == 0:
models.TrainingLevelRequirement.objects.create(level=technician, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
else:
models.TrainingLevelRequirement.objects.create(level=supervisor, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
except: # noqa
print("Failed create for {}. Weird.".format(item))
self.levels.append(technician) self.levels.append(technician)
self.levels.append(supervisor) self.levels.append(supervisor)

View File

@@ -0,0 +1,77 @@
import datetime
import random
from django.contrib.auth.models import Group, Permission
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
from reversion import revisions as reversion
from training import models
from RIGS.models import Profile
class Command(BaseCommand):
help = 'Adds training users'
can_import_settings = True
profiles = []
committee_group = None
def handle(self, *args, **options):
print("Generating useful training users")
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
raise CommandError('You cannot run this command in production')
random.seed('otherwise it is done by time, which could lead to inconsistent tests')
with transaction.atomic():
self.setup_groups()
self.setup_useful_profiles()
print("Done generating useful training users")
def setup_groups(self):
self.committee_group = Group.objects.create(name='Committee')
perms = [
"add_trainingitemqualification",
"change_trainingitemqualification",
"delete_trainingitemqualification",
"add_traininglevelqualification",
"change_traininglevelqualification",
"delete_traininglevelqualification",
"add_traininglevelrequirement",
"change_traininglevelrequirement",
"delete_traininglevelrequirement"]
for permId in perms:
self.committee_group.permissions.add(Permission.objects.get(codename=permId))
self.committee_group.save()
def setup_useful_profiles(self):
supervisor = Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor",
initials="SV",
email="supervisor@example.com", is_active=True,
is_staff=True, is_approved=True)
supervisor.set_password('supervisor')
supervisor.groups.add(Group.objects.get(name="Keyholders"))
supervisor.save()
models.TrainingLevelQualification.objects.create(
trainee=supervisor,
level=models.TrainingLevel.objects.filter(
level__gte=models.TrainingLevel.SUPERVISOR).exclude(
department=models.TrainingLevel.HAULAGE).exclude(
department__isnull=True).first(),
confirmed_on=timezone.now(),
confirmed_by=models.Trainee.objects.first())
committee_user = Profile.objects.create(username="committee", first_name="Committee", last_name="Member",
initials="CM",
email="committee@example.com", is_active=True, is_approved=True)
committee_user.groups.add(self.committee_group)
supervisor.groups.add(Group.objects.get(name="Keyholders"))
committee_user.set_password('committee')
committee_user.save()

View File

@@ -0,0 +1,282 @@
import os
import datetime
import re
import xml.etree.ElementTree as ET
from django.core.management.base import BaseCommand
from django.conf import settings
from django.db.utils import IntegrityError
from django.utils.timezone import make_aware
from training import models
from RIGS.models import Profile
class Command(BaseCommand):
epoch = datetime.date(1970, 1, 1)
id_map = {}
def handle(self, *args, **options):
self.import_Trainees()
self.import_TrainingCatagory()
self.import_TrainingItem()
self.import_TrainingItemQualification()
self.import_TrainingLevel()
self.import_TrainingLevelQualification()
self.import_TrainingLevelRequirements()
@staticmethod
def xml_path(file):
return os.path.join(settings.BASE_DIR, 'data/{}'.format(file))
@staticmethod
def parse_xml(file):
tree = ET.parse(file)
return tree.getroot()
def import_Trainees(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('Members.xml'))
for child in root:
try:
name = child.find('Member_x0020_Name').text
first_name = name.split()[0]
last_name = " ".join(name.split()[1:])
profile = Profile.objects.filter(first_name=first_name, last_name=last_name).first()
if profile:
self.id_map[child.find('ID').text] = profile.pk
print(f"Found existing user {profile}, matching data")
tally[0] += 1
else:
# PYTHONIC, BABY
initials = first_name[0] + "".join([name_section[0] for name_section in re.split("\\s*-", last_name.replace("(", ""))])
# print(initials)
new_profile = Profile.objects.create(username=name.replace(" ", ""),
first_name=first_name,
last_name=last_name,
initials=initials)
self.id_map[child.find('ID').text] = new_profile.pk
tally[1] += 1
print(f"No match found, creating new user {new_profile}")
except AttributeError: # W.T.F
print("Trainee #{} is FUBAR".format(child.find('ID').text))
print('Trainees - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_TrainingCatagory(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('Categories.xml'))
for child in root:
obj, created = models.TrainingCategory.objects.update_or_create(
pk=int(child.find('ID').text),
reference_number=int(child.find('Category_x0020_Number').text),
name=child.find('Category_x0020_Name').text
)
if created:
tally[1] += 1
else:
tally[0] += 1
print('Categories - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_TrainingItem(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('Training Items.xml'))
for child in root:
if child.find('active').text == '0':
active = False
else:
active = True
number = int(child.find('Item_x0020_Number').text)
name = child.find('Item_x0020_Name').text
category = models.TrainingCategory.objects.get(pk=int(child.find('Category_x0020_ID').text))
try:
obj, created = models.TrainingItem.objects.update_or_create(
pk=int(child.find('ID').text),
reference_number=number,
name=name,
category=category,
active=active
)
except IntegrityError:
print("Training Item {}.{} {} has a duplicate reference number".format(category.reference_number, number, name))
if created:
tally[1] += 1
else:
tally[0] += 1
print('Training Items - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_TrainingItemQualification(self):
tally = [0, 0, 0]
root = self.parse_xml(self.xml_path('Training Records.xml'))
for child in root:
depths = [("Training_Started", models.TrainingItemQualification.STARTED),
("Training_Complete", models.TrainingItemQualification.COMPLETE),
("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT), ]
for (depth, depth_index) in depths:
if child.find('{}_Date'.format(depth)) is not None:
if child.find('{}_Assessor_ID'.format(depth)) is None:
print("Training Record #{} had no supervisor. Assigning System User.".format(child.find('ID').text))
supervisor = Profile.objects.get(first_name="God")
continue
supervisor = Profile.objects.get(pk=self.id_map[child.find('{}_Assessor_ID'.format(depth)).text])
if child.find('Member_ID') is None:
print("Training Record #{} didn't train anybody and has been ignored. Dammit {}".format(child.find('ID').text, supervisor.name))
tally[2] += 1
continue
try:
obj, created = models.TrainingItemQualification.objects.update_or_create(
item=models.TrainingItem.objects.get(pk=int(child.find('Training_Item_ID').text)),
trainee=Profile.objects.get(pk=self.id_map[child.find('Member_ID').text]),
depth=depth_index,
date=child.find('{}_Date'.format(depth)).text[:-9], # Stored as datetime with time as midnight because fuck you I guess
supervisor=supervisor
)
notes = child.find('{}_Notes'.format(depth))
if notes is not None:
obj.notes = notes.text
obj.save()
if created:
tally[1] += 1
else:
tally[0] += 1
except IntegrityError: # Eh?
print("Training Record #{} is probably duplicate. ಠ_ಠ".format(child.find('ID').text))
except AttributeError:
print(child.find('ID').text)
print('Training Item Qualifications - Updated: {}, Created: {}, Broken: {}'.format(tally[0], tally[1], tally[2]))
def import_TrainingLevel(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('Training Levels.xml'))
for child in root:
name = child.find('Level_x0020_Name').text
if name == "Technical Assistant":
level = models.TrainingLevel.TA
depString = None
elif "Common" in name:
levelString = name.split()[0]
if levelString == "Technician":
level = models.TrainingLevel.TECHNICIAN
elif levelString == "Supervisor":
level = models.TrainingLevel.SUPERVISOR
depString = None
else:
depString = name.split()[-1]
levelString = name.split()[0]
if levelString == "Technician":
level = models.TrainingLevel.TECHNICIAN
elif levelString == "Supervisor":
level = models.TrainingLevel.SUPERVISOR
else:
print(levelString)
continue
for dep in models.TrainingLevel.DEPARTMENTS:
if dep[1] == depString:
department = dep[0]
desc = ""
if child.find('Desc') is not None:
desc = child.find('Desc').text
obj, created = models.TrainingLevel.objects.update_or_create(
pk=int(child.find('ID').text),
description=desc,
level=level
)
if depString is not None:
obj.department = department
obj.save()
if created:
tally[1] += 1
else:
tally[0] += 1
for level in models.TrainingLevel.objects.all():
if level.department is not None:
if level.level == models.TrainingLevel.TECHNICIAN:
level.prerequisite_levels.add(models.TrainingLevel.objects.get(level=models.TrainingLevel.TA), models.TrainingLevel.objects.get(level=models.TrainingLevel.TECHNICIAN, department=None))
elif level.level == models.TrainingLevel.SUPERVISOR:
level.prerequisite_levels.add(models.TrainingLevel.objects.get(level=models.TrainingLevel.TECHNICIAN, department=level.department), models.TrainingLevel.objects.get(level=models.TrainingLevel.SUPERVISOR, department=None))
print('Training Levels - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_TrainingLevelQualification(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('Training Level Records.xml'))
for child in root:
try:
trainee = Profile.objects.get(pk=self.id_map[child.find('Member_x0020_ID').text]) if child.find('Member_x0020_ID') is not None else False
level = models.TrainingLevel.objects.get(pk=int(child.find('Training_x0020_Level_x0020_ID').text)) if child.find('Training_x0020_Level_x0020_ID') is not None else False
if trainee and level:
obj, created = models.TrainingLevelQualification.objects.update_or_create(pk=int(child.find('ID').text),
trainee=trainee,
level=level)
else:
print('Training Level Qualification #{} failed to import. Trainee: {} and Level: {}'.format(child.find('ID').text, trainee, level))
continue
if child.find('Date_x0020_Level_x0020_Awarded') is not None:
obj.confirmed_on = make_aware(datetime.datetime.strptime(child.find('Date_x0020_Level_x0020_Awarded').text.split('T')[0], "%Y-%m-%d"))
obj.save()
# confirmed by?
if created:
tally[1] += 1
else:
tally[0] += 1
except IntegrityError: # Eh?
print("Training Level Qualification #{} is duplicate. ಠ_ಠ".format(child.find('ID').text))
print('TrainingLevelQualifications - Updated: {}, Created: {}'.format(tally[0], tally[1]))
def import_TrainingLevelRequirements(self):
tally = [0, 0]
root = self.parse_xml(self.xml_path('Training Level Requirements.xml'))
for child in root:
items = child.find('Items').text.split(",")
for item in items:
try:
item = item.split('.')
obj, created = models.TrainingLevelRequirement.objects.update_or_create(
level=models.TrainingLevel.objects.get(
pk=int(
child.find('Level').text)), item=models.TrainingItem.objects.get(
active=True, reference_number=item[1], category=models.TrainingCategory.objects.get(
reference_number=item[0])), depth=int(
child.find('Depth').text))
if created:
tally[1] += 1
else:
tally[0] += 1
except models.TrainingItem.DoesNotExist:
print("Item with number {} does not exist".format(item))
except models.TrainingItem.MultipleObjectsReturned:
print(models.TrainingItem.objects.filter(reference_number=item[1], category=models.TrainingCategory.objects.get(reference_number=item[0])))
print('TrainingLevelRequirements - Updated: {}, Created: {}'.format(tally[0], tally[1]))

View File

@@ -1,4 +1,4 @@
# Generated by Django 3.1.5 on 2021-07-05 22:01 # Generated by Django 3.2.11 on 2022-01-04 20:08
import RIGS.models import RIGS.models
import django.contrib.auth.models import django.contrib.auth.models
@@ -11,7 +11,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('RIGS', '0041_auto_20210302_1204'), ('RIGS', '0043_auto_20211027_1519'),
] ]
operations = [ operations = [
@@ -19,25 +19,36 @@ class Migration(migrations.Migration):
name='TrainingCategory', name='TrainingCategory',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reference_number', models.CharField(max_length=3)), ('reference_number', models.IntegerField(unique=True)),
('name', models.CharField(max_length=50)), ('name', models.CharField(max_length=50)),
], ],
options={
'verbose_name_plural': 'Training Categories',
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='TrainingItem', name='TrainingItem',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reference_number', models.CharField(max_length=3)), ('reference_number', models.IntegerField()),
('name', models.CharField(max_length=50)), ('name', models.CharField(max_length=50)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='items', to='training.trainingcategory')), ('active', models.BooleanField(default=True)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='training.trainingcategory')),
], ],
options={
'ordering': ['category__reference_number', 'reference_number'],
'unique_together': {('reference_number', 'active', 'category')},
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='TrainingLevel', name='TrainingLevel',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('department', models.CharField(max_length=50, null=True)), ('description', models.TextField(blank=True)),
('department', models.IntegerField(blank=True, choices=[(0, 'Sound'), (1, 'Lighting'), (2, 'Power'), (3, 'Rigging'), (4, 'Haulage')], null=True)),
('level', models.IntegerField(choices=[(0, 'Technical Assistant'), (1, 'Technician'), (2, 'Supervisor')])), ('level', models.IntegerField(choices=[(0, 'Technical Assistant'), (1, 'Technician'), (2, 'Supervisor')])),
('icon', models.CharField(blank=True, max_length=20, null=True)),
('prerequisite_levels', models.ManyToManyField(blank=True, related_name='prerequisites', to='training.TrainingLevel')),
], ],
bases=(models.Model, RIGS.models.RevisionMixin), bases=(models.Model, RIGS.models.RevisionMixin),
), ),
@@ -50,20 +61,38 @@ class Migration(migrations.Migration):
'indexes': [], 'indexes': [],
'constraints': [], 'constraints': [],
}, },
bases=('RIGS.profile',), bases=('RIGS.profile', RIGS.models.RevisionMixin),
managers=[ managers=[
('objects', django.contrib.auth.models.UserManager()), ('objects', django.contrib.auth.models.UserManager()),
], ],
), ),
migrations.CreateModel(
name='TrainingLevelRequirement',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('depth', models.IntegerField(choices=[(0, 'Training Started'), (1, 'Training Complete'), (2, 'Passed Out')])),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='training.trainingitem')),
('level', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requirements', to='training.traininglevel')),
],
options={
'unique_together': {('level', 'item')},
},
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.CreateModel( migrations.CreateModel(
name='TrainingLevelQualification', name='TrainingLevelQualification',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('confirmed_on', models.DateTimeField()), ('confirmed_on', models.DateTimeField(null=True)),
('confirmed_by', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='confirmer', to='training.trainee')), ('confirmed_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='confirmer', to='training.trainee')),
('level', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='training.traininglevel')), ('level', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='training.traininglevel')),
('trainee', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='levels', to='training.trainee')), ('trainee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='level_qualifications', to='training.trainee')),
], ],
options={
'ordering': ['-confirmed_on'],
'unique_together': {('trainee', 'level')},
},
bases=(models.Model, RIGS.models.RevisionMixin),
), ),
migrations.CreateModel( migrations.CreateModel(
name='TrainingItemQualification', name='TrainingItemQualification',
@@ -72,9 +101,13 @@ class Migration(migrations.Migration):
('depth', models.IntegerField(choices=[(0, 'Training Started'), (1, 'Training Complete'), (2, 'Passed Out')])), ('depth', models.IntegerField(choices=[(0, 'Training Started'), (1, 'Training Complete'), (2, 'Passed Out')])),
('date', models.DateField()), ('date', models.DateField()),
('notes', models.TextField(blank=True)), ('notes', models.TextField(blank=True)),
('item', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='training.trainingitem')), ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='training.trainingitem')),
('supervisor', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='qualifications_granted', to='training.trainee')), ('supervisor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='qualifications_granted', to='training.trainee')),
('trainee', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='qualifications_obtained', to='training.trainee')), ('trainee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='qualifications_obtained', to='training.trainee')),
], ],
options={
'order_with_respect_to': 'item',
'unique_together': {('trainee', 'item', 'depth')},
},
), ),
] ]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.11 on 2022-01-05 12:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('training', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='traininglevel',
options={'ordering': ['department', 'level']},
),
]

Some files were not shown because too many files have changed in this diff Show More