Compare commits

..

76 Commits

Author SHA1 Message Date
aa51ce7861 Screw you codestyle 2021-01-29 18:45:16 +00:00
cdc8635e2f Add support for running tests with geckodriver, do this on CI 2021-01-29 18:43:34 +00:00
5d07a1853d Further template refactoring 2021-01-29 18:24:37 +00:00
1eed448828 Restore signals import, screw you import optimisation 2021-01-29 17:56:58 +00:00
e2e83e2cd8 Bunch of test refactoring 2021-01-29 16:39:38 +00:00
534e1000d4 Maybe this works to purge deps postbuild 2021-01-29 14:34:38 +00:00
84119f6685 Ignore test directories from Heroku slug 2021-01-29 14:24:29 +00:00
e50cf8d06c Add format arg to coverage command 2021-01-28 20:22:18 +00:00
a3970cf553 Revert "Minor python cleanup"
This reverts commit 6a4620a2e5.
2021-01-28 18:54:50 +00:00
4b8639faf4 Import optimisation 2021-01-28 18:51:22 +00:00
6a4620a2e5 Minor python cleanup 2021-01-28 18:38:55 +00:00
cc233e0223 Some template cleanup 2021-01-28 18:37:41 +00:00
2351201e13 Another go at making coverage show up 2021-01-28 18:05:57 +00:00
874b8ef37a Try ignoring some third party deprecation warnings 2021-01-28 18:05:46 +00:00
6a0b00dc76 Might fix actions? 2021-01-28 15:30:50 +00:00
1c31608951 Github, you need a Actions YAML validator! 2021-01-28 04:00:57 +00:00
37c0890a4b Belt and braces approach to coverage 2021-01-28 03:59:18 +00:00
c772744a4b Attempt #2 at fixing heroku 2021-01-28 03:44:09 +00:00
5319c2f6e3 Might fix heroku building 2021-01-28 03:33:06 +00:00
aa525372a2 Oops, again
Probably bedtime..
2021-01-28 02:49:55 +00:00
2774690b59 Attempt to bodge asset test 2021-01-28 02:45:04 +00:00
81733d32ba Fix codeclimate config, mark 2 2021-01-28 02:42:34 +00:00
e6527db6f7 Switch back to old coveralls method 2021-01-28 02:39:08 +00:00
d109ee231f Well the YAML was *syntactically* valid.... 2021-01-28 02:33:49 +00:00
cde46cc8e4 Update codeclimate settings, purge some config files 2021-01-28 02:32:38 +00:00
dddf0dc42a Parallel parallel builds were giving me a headache, try this 2021-01-28 02:30:00 +00:00
19c5f282d2 Does this work? 2021-01-28 02:17:40 +00:00
d5d2e9167c Exclude node_modules from codestyle 2021-01-28 02:10:36 +00:00
3b3ba0b87e Different approach to CI dependencies 2021-01-28 02:08:27 +00:00
1fbe59dfb0 See below 2021-01-28 01:50:48 +00:00
65c5307072 Still helps if I commit valid YAML 2021-01-28 01:50:04 +00:00
638816535e Run asset building serverside 2021-01-28 01:47:57 +00:00
8346d705ba Remove some unused gulp dependencies 2021-01-28 00:40:40 +00:00
18eed1a654 Add codeclimate maintainability badge 2021-01-27 23:49:36 +00:00
5174a442bc Try this way of parallel coverage 2021-01-27 20:21:29 +00:00
5f1fd59dd2 Fix screenshot uploading on CI (again) 2021-01-27 19:47:30 +00:00
3be2a9f4b5 Oops, remove obsolete if check 2021-01-27 19:39:24 +00:00
883ef4ed8b Bah, codestyle 2021-01-27 19:36:42 +00:00
2195a60438 Use pytest as our test runner for better parallelism
Also rewrote some asset tests to be in the pytest style. May do some more. Some warnings cleaned up in the process.
2021-01-27 19:33:20 +00:00
12c4b63947 Revert "Does this help the coverage be less weird?"
This reverts commit 39ab9df836.
2021-01-26 23:53:59 +00:00
39ab9df836 Does this help the coverage be less weird? 2021-01-26 23:36:26 +00:00
a99e4b1d1c What about this?
Swear I spend my life jiggerypokerying the damn test suite...
2021-01-26 21:51:59 +00:00
18a091f8ea What about this? 2021-01-26 19:42:54 +00:00
d64d0f54d4 Should fix asset test on CI 2021-01-26 19:37:12 +00:00
8f54897b69 Upload failure screenshots as individual artifacts not a zip
Turns out I can't unzip things from my phone, which is a pain
2021-01-26 19:31:06 +00:00
97830596b5 Fix unauth test to not just immediately pass out 2021-01-26 19:15:29 +00:00
68f401097d Add title checking to the slightly insane assets test 2021-01-26 17:43:01 +00:00
8c5b2f426d Refactor asset audit tests with better selectors
Also fixed a silly title error with the modal
2021-01-26 15:08:51 +00:00
e3fc05772e Exclude tests from the coverage stats
Seems to be artifically deflating our stats
2021-01-26 13:42:58 +00:00
e38205b9e7 Oops, remove unused import 2021-01-25 22:45:02 +00:00
f2b9642772 Switch to gh-a artifact uploading instead of imgur 'hack'
For test failure screenshots. Happy now @mattysmith22? ;p
2021-01-25 22:27:21 +00:00
08939a0e1f Purge old .idea config 2021-01-25 22:18:15 +00:00
15b7f9c7c1 No Ruby compass bodge, no need for rubocop! 2021-01-25 22:17:37 +00:00
c67f7e1e54 Purge old vagrant config 2021-01-25 22:16:25 +00:00
4f268b3168 Cache python dependencies
Should majorly speedup parallelillelelised testing
2021-01-25 22:08:32 +00:00
0e7723828a Update python version in tests 2021-01-25 21:59:23 +00:00
c46a2c53f3 Run parallelised RIGS tests as one matrix job 2021-01-25 21:55:32 +00:00
b3617a74bf Define service in coveralls task 2021-01-25 21:44:05 +00:00
5806cbc72d Change python ver 2021-01-25 21:42:55 +00:00
ab2ff9f146 Fix whoops in requirements.txt 2021-01-25 21:39:44 +00:00
fd5575a818 You valid now? 2021-01-25 21:37:22 +00:00
402a1dd7f0 Tends to help if I push valid yaml 2021-01-25 21:29:32 +00:00
be7688aa75 Upgrade python deps 2021-01-25 18:14:49 +00:00
a472e414f7 Add basic calendar button test
Mainly to pickup on FullCalendar loading errors
2021-01-25 18:09:21 +00:00
618c02aa9c Attempt at parallelising tests where possible 2021-01-25 16:42:12 +00:00
3056b6ef71 Fix audit time check in asset audit test 2021-01-25 16:07:44 +00:00
548c960996 npm upgrade 2021-01-25 15:50:36 +00:00
2cc43dcdb7 Move some gulp deps to dev rather than prod 2021-01-25 15:39:51 +00:00
6db25fb56b Upgrade to heroku-20 stack 2021-01-25 15:39:04 +00:00
8ad629a47e Replace Travis with Github Actions (#416)
Closes #415

Also enables coverage tracking of Django templates, hence the ~30% drop in coverage!
2021-01-25 01:20:12 +00:00
dependabot[bot]
2414eb9724 Build(deps): Bump lxml from 4.5.0 to 4.6.2 (#417)
Bumps [lxml](https://github.com/lxml/lxml) from 4.5.0 to 4.6.2.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.5.0...lxml-4.6.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-01-24 00:23:54 +00:00
3414204209 Refactor buildsystem to NPM/Gulp, port to BS4 & rewrite RIGS tests accordingly... (#412)
* Start to seperate versioning into its own app

* Start reworking invoice things

* Reduced overall font size a touch

* Improvements to generic lists

* Tweak some colours to be a bit less OTT

I need to work out if I can seperate background and primary colours like BS3 did

* Improvements to event table mobile

* First pass at mobile-ising the generic list

* Item table fixes

* Fixed fullcalendar print css not included

* Asset list table improvements

* Tweak asset list to be more in line with other lists

* Versioning template improvements

//TODO Rather than have seperate asset templates, convert 'id' into a template variable

* Tweak versioning templates to allow ID overrides

Asset specific templates begone. Still need to bring back the ID formatting for the Rigboard.

* Asset form fixes

* Use the right autocompleter.js...

* Breakout (most) user stuff to separate module

The model remains in RIGS for now, as it's pretty painful to move...

* Python Format/import opt

* Test Refactor Part 1 - Shuffle things around

* Fix migrations

TODO - need to ensure moved models are *moved* rather than deleted and recreated!

* Start on new tests

* Initial work on event create test reimpl

* Init other tests, more rigs test faffery

* Desaturate theme colors even more

Much closer to BS3

* Fix event item adding

Bit too heavy handed with the deduplication there Arona

* Initial refactor of event item testing

* Upgrade bootstrap-select

* Updated bootstrap-select for BS4

* Initial port of duplicate testing

Needs the latter half rewriting once we have an EventDetail POM

* Refactor date validation test

So close to killing test_functional.EventTest!

* Deduplication of testing code

* pep8

* Fix some tests

And some things that were actually borked

* FIX: Prevent setting access time after start time 

Cherry pick of d274ea4606. Will close #405.

* Refactor calendar tests

* FIX: Don't show asset buttons/history for basic users

* Really ought to get a pre-commit hook for pep8...

* Fully replace test_functional

* Dedupe generic search logic

* Fix the remaining tests

* Ensure submit button is scrolled to in tests

* Fix asset creation test + actually verify its results

* Make CI use latest (stable) chromedriver rather than some ancient one

Since Travis uses the latest stable chrome, should always match. Bash oneliner \o/

* Of course | is part of YAML syntax, of course...

Maybe this works.

* Update python version

Trying to get CI to match my local environment as much as possible...

* Minor test futzing

* Well that wasn't clever of me

* That was even less clever of me

* Revert to old submit wait behaviour

* What about if I did this

* Try disabling chrome cache

* Added screenshot recording of test failures

* Fixed RIGS tests not being run

* Fixed Pep8 - I promise I'll make a pre-commit hook sometime!

* Very initial work at togglable darktheme.

Dammit @alexdaniel654 just when I had my scope creep kinda under control. It'll be v. nice to have though...!

* More dark theme wangling

* Fix some asset template things

* FIX: CI Locale Issues

* Fix sample command

* Initial work at integrating the risk assessment

#136. No clever database structure as yet...

* FIX: Don't set every boolean input to radios

* Different approach to RA linking

* Move text definitions to somewhere more authoratitive

* FIX: Undo breakage causing autopep8

o.O

* Expand detail template

* Use correct view for RA history

* Initial work at coercing activity feed into showing RAs

Also shows Asset/Supplier on the homepage feed.

* Refactor activity feed template logic

Yay for removing arbitrary if/else chains!

* Initial work on caching activity feed

Server side that is. Ref #162.

* Start RA list template

* Refactor RA creation stuff, again

* Add H&S Details to Event Detail View

* Display venue notes in event detail

Notes are no use if nobody reads them. Not sure on this one.

* Add ability to filter event archive by status

Closes #168.

* Fix lingering naive time

* Use locmem cache in sqlite environments

Otherwise the tests just lock up totally. Should close #162

* Update dependencies

Mirrors/supersedes 0e67da82e2

* Add global ctrl/meta-enter shortcut for form submission

Wants rewriting for better efficiency, but hey, it works!

* Update dependencies

* Fix for a situation that should be impossible

* Fix navbar alignment

* FEAT: Improve 'omni'search

- Partialised template
- Added to assets header
- Added ability to search assets/suppliers
- Improved selection logic
- Have it display current query

* Move closemodal into PyRIGS

* Fix tests for search improvements

* Dark mode colour improvements

* Fix table colors for dry hires

* further darktheme fixes

* Remove the dark header from light theme

* Fix reload loops when CSS/JS is changed

* Move dark theme SCSS to separate file, fix inactive pagination styling

* Genercise detail pages

* Testing something re notes

I wonder if I can make that global, rather than per-template...

* Dark theme palette shenanigans

I just can't decide

* Match darktheme palette to forum darktheme palette

Why reinvent the wheel.

* Make supplier detail use the generic template

* Disable mobile event table PoC for now

* Remove the defaults from the RA fields + make them required

* More RA fixes

* Fixes to revisions for RAs

* Add bootstrap 4 test page

* Bunch of dark mode fixes from test page

* Do not use Django 'required' for radio selects

As this requires them to be True, whereas we just need to require that an option be entered.

* Properly fixed popover darktheme

* Fixed search for events

* Style fixes to asset list

* Start RA 'mark review' feature

* Add reviewing to revision history, fix RA editing not working

Also actually commit all the files, that helps

* Fix Power MIC being lost on RA edit

Why it is subtly different to the Event Update behaviour? Who knows

* Invalidate RA review if it is edited after review

* Start work on event checklist

* Add a button for creating and instantly voiding invoices

Handy dandy for when you have loads of cancelled events, like say, a pandemic

* Mooooore status chips, mooore

* Initial shenanigans on storing my overly fancy EC form

* Proof of concept for JSON parsing/storage

\o/

* Add new line functionality for vehicles/drivers

Might it have been easier to create 'dummy' models like with EventItems? Probably...

* Alter rig_count to not include un-checked-in dry hires

* Insert a divider between still-out dry hires and actually upcoming events on rigboard

* Initial work on new checklist handling. No more JSON!

* Versioning module now does magic

Automatic creation of views/urls for anything registered with reversion, with a small amount of hackage to preserve legacy stuff. (and the DAMNED asset IDs!) I would never get distracted...

* Cleanup

* Event checklist crew works

Mostly - its not happy with timezones

* Medium event power stuff done, barring worst case stuff

* Misc fixes

* Validation of power reqs

* Worst case points on checklist

* Templating improvements to RA/EC stuff

* Do event table color logic at python level

* Audit template fixes

* Restrict versioning to one level of depth for speed

Also fixed the template for nested changes

* Event properties internal/authorised always return a explicit boolean rather than sometimes None

* Use template filter for notes

* Fix list templates

TODO: Sensible place to define the 'expected answer' stuff.

* Fix cable table template

* Rethink rigboard color logic again

Also revert some broken stuff

* Test fixes

* Modify auth test so it doesn't try and test for external authorisations

Cause that's not a thing

* Why does this work

Bloody overzealous autoformatter...

* Formatting...

* Initial work on RA tests

* Pages/start of tests for EventChecklists

* Much better coverage of H&S things

* Cleanup & Squash migrations

* Fix wrong variable name in settings.py

* Fix broken invoice list template

* Add revision history to invoices/payments.

Also patches previously introduced reversion permissions hole.

Supersedes and closes #337.

* Various misc fixes

* Fix for my fix

* Curse youuuuu pep8

* Invoice template improvements

* Minor fixes

* More tweaks

* More fixes

* Major improvements/fixes to authorisation templates

* Add ability to mark event checklists as Large Event

This just disables the checks to allow the rest of it to be filled out for large events, though I expect paper forms may still be used...

* Remove database ID from generic list

* Put power threshold values in a collapse

* Use template filter for consistent removal of 'None links'

Plus cleaner template markup! More HTML-in-Python tho, which always feels a bit CSS-in-JS

* Tweak asset list markup

* Begin to change add buttons success -> primary

Also change search primary -> info to avoid clash

* Begin to improve event checklist on mobile

* Asset detail template improvements

* Fix #326 (again)

* Fix errors being squashed

* Fix rigboard validation tests

* Initial work on BS4 button templatetag

Newfeatureitis strikes again

* Allow multiple event checklists per event

TODO: Status chip now needs rethinking

* Minor event detail fixes

* Fix tests

* Rework button tag

* Mobile fixes for search

* Fix event checklist on mobile

* Redo light theme palette

* Switch rigboard new button to primary

* Kill off excess whitespace on rigboard

* Rigboard Timing display tweaks

* Fix tests

* Properly handle eventauthorisations in new versioning

It's not great, not terrible...

* Prevent creating duplicate revisions on event

Potential fix for #322 - I couldn't reproduce even before this change...

* Template improvements

* Minor test fixes

* Revert "Prevent creating duplicate revisions on event"

Apparently it was too strong at preventing dupes...

This reverts commit cce0ad0f9f.

# Conflicts:
#	RIGS/models.py

* Better approach to generic list templates + other deduplication

* Also apply better approach to generic detail pages

* One of these days I'll remember to test BEFORE pushing...

* And now the same for generic forms

* Display tick/cross rather than true/false in boolean version diffs

* Upgrade dependencies

* Fixes fixes fixes

* Fix dependency hell

Probably

* Correct handling of spaces in paperwork filenames

Also normalises display of Invoice IDs. Partial fix for #391.

* Buggerit millennium hand and shrimp

Knew I was gonna forget to fix the tests

* FIX: Set duplicated event status to provisional

Closes #398.

Flip flop. Flip flop.

* Update polyfill for datetime-local

Bloody Firefox. We love to hate you. Proper CSS of the fill to come, SoonTM.

Closes #391

* Curses!

* Minor typo fixes

* Initial pass at soop-consult confirmation screen for RAs

* Fix migration

* Make venue/date editable on EC

For multi venue, multi day events

Defaults to date and venue set on the event. Also made power MIC default to that set in RA

* Clearer logic for RA inverted fields

* (probably) fix tests

* Give keyholders supplier edit perm

* Generic list only displays edit button if user has perm

* Same perm check for generic details

* H&S Details takes up free space on non-internal events

* Remove flash of content when loading new rig page

* First pass at clearer display of asset list filters

* Fix tests / default to headless tests

(fingers crossed)

* Fix autocompleter.js to properly disable edit links again

* Move status color logic back to template

Cause that somehow makes it work better??

* Display note icon on event detail page

* Fix caching

* Put rounded corners back where they belong

* Remove lingering use of 'page-header'

BS removed that style

* More search and replace for BS changes

Thought I'd got them all. Clearly not!

* Remove enforced linebreak on status chips

* Fix horizontal-ness on some forms

* Remove animation on prefers-reduced-motion/low referesh rate devices

Also normalises handling of asset list cable table & improves its use of space on large devices

* Make version changes badges more readable

* First pass at making the calendar less crap

* Fix event table success logic

Yay for copy paste fails >.>

* Use borders rather than block colors for coloured tables under darktheme

* First pass at porting calendar from FC V3 to V5

Two major versions and all they did was rename a bunch of names...TWICE.

* Rework version name method to avoid blank names on eventchecklist vehicles/crew

* Fix cable test

* Made radio button focus much more obvious on dark theme

* Implement Jerb's wording changes

* Fix one test, break another...

* Fix recent change stream list mutation issue

* FIX: Do not naively cache event table

Not that easy, it turns out. Duh.

* FEAT: Implement #413 show associated assets on cable type detail pg

Closes #413

* Allow H&S for non-events

* Update emergency contact number

* Improvements to profile detail page

* Implement some of Jonny's suggested changes

TODO:
- Define event size at RA time, pass through to EC
- Have later power questions be context dependent

* Test fixes

* Add space for power/rigging plans to be linked to RAs

* Start move of event size logic to RA from Ec

* Javascript required shenanigans for RA power

* More moving of event size logic

* Fixing tests for new logic etc

* Why does this work

Indeed, it may not

* FIX: Stupid typo in versioning.py

* Further minor fixes to versioning

* Add icons to H&S menu items

* Should fix calendar breaking in production

* Small alignment fix in asset list

* Squash migrations

Co-authored-by: Matthew Smith <psyms13@nottingham.ac.uk>
2021-01-23 22:22:37 +00:00
099a184f2e Update emergency contact number 2020-11-13 09:24:39 +00:00
dependabot[bot]
0e67da82e2 Build(deps): Bump django from 3.0.3 to 3.0.7 (#411)
Bumps [django](https://github.com/django/django) from 3.0.3 to 3.0.7.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.0.3...3.0.7)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-06-05 18:11:21 +01:00
02e8e8aaf7 Fix risk assessment link 2020-05-11 23:22:11 +01:00
David Taylor
4acd9156d0 Switch to heroku-18 stack (#409)
cedar-14 has been deprectated: https://devcenter.heroku.com/changelog-items/1413
2020-04-15 12:53:44 +01:00
252 changed files with 24663 additions and 24210 deletions

View File

@@ -1,32 +1,16 @@
--- version: 2
engines: plugins:
csslint: csslint:
enabled: true enabled: true
duplication: duplication:
enabled: true enabled: true
config: config:
languages: languages:
- ruby
- javascript - javascript
- python - python
- php
eslint: eslint:
enabled: true enabled: true
fixme: fixme:
enabled: true enabled: true
radon: radon:
enabled: true enabled: true
rubocop:
enabled: true
ratings:
paths:
- "**.css"
- "**.inc"
- "**.js"
- "**.jsx"
- "**.module"
- "**.php"
- "**.py"
- "**.rb"
exclude_paths:
- config/

View File

@@ -1,6 +1,3 @@
[run] [run]
source = plugins = django_coverage_plugin
./ omit = *migrations*, *tests*
omit =
*/migrations/*

View File

@@ -1,2 +0,0 @@
--exclude-exts=.min.css
--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes

View File

@@ -1 +0,0 @@
**/*{.,-}min.js

213
.eslintrc
View File

@@ -1,213 +0,0 @@
ecmaFeatures:
modules: true
jsx: true
env:
amd: true
browser: true
es6: true
jquery: true
node: true
# http://eslint.org/docs/rules/
rules:
# Possible Errors
comma-dangle: [2, never]
no-cond-assign: 2
no-console: 0
no-constant-condition: 2
no-control-regex: 2
no-debugger: 2
no-dupe-args: 2
no-dupe-keys: 2
no-duplicate-case: 2
no-empty: 2
no-empty-character-class: 2
no-ex-assign: 2
no-extra-boolean-cast: 2
no-extra-parens: 0
no-extra-semi: 2
no-func-assign: 2
no-inner-declarations: [2, functions]
no-invalid-regexp: 2
no-irregular-whitespace: 2
no-negated-in-lhs: 2
no-obj-calls: 2
no-regex-spaces: 2
no-sparse-arrays: 2
no-unexpected-multiline: 2
no-unreachable: 2
use-isnan: 2
valid-jsdoc: 0
valid-typeof: 2
# Best Practices
accessor-pairs: 2
block-scoped-var: 0
complexity: [2, 6]
consistent-return: 0
curly: 0
default-case: 0
dot-location: 0
dot-notation: 0
eqeqeq: 2
guard-for-in: 2
no-alert: 2
no-caller: 2
no-case-declarations: 2
no-div-regex: 2
no-else-return: 0
no-empty-label: 2
no-empty-pattern: 2
no-eq-null: 2
no-eval: 2
no-extend-native: 2
no-extra-bind: 2
no-fallthrough: 2
no-floating-decimal: 0
no-implicit-coercion: 0
no-implied-eval: 2
no-invalid-this: 0
no-iterator: 2
no-labels: 0
no-lone-blocks: 2
no-loop-func: 2
no-magic-number: 0
no-multi-spaces: 0
no-multi-str: 0
no-native-reassign: 2
no-new-func: 2
no-new-wrappers: 2
no-new: 2
no-octal-escape: 2
no-octal: 2
no-proto: 2
no-redeclare: 2
no-return-assign: 2
no-script-url: 2
no-self-compare: 2
no-sequences: 0
no-throw-literal: 0
no-unused-expressions: 2
no-useless-call: 2
no-useless-concat: 2
no-void: 2
no-warning-comments: 0
no-with: 2
radix: 2
vars-on-top: 0
wrap-iife: 2
yoda: 0
# Strict
strict: 0
# Variables
init-declarations: 0
no-catch-shadow: 2
no-delete-var: 2
no-label-var: 2
no-shadow-restricted-names: 2
no-shadow: 0
no-undef-init: 2
no-undef: 0
no-undefined: 0
no-unused-vars: 0
no-use-before-define: 0
# Node.js and CommonJS
callback-return: 2
global-require: 2
handle-callback-err: 2
no-mixed-requires: 0
no-new-require: 0
no-path-concat: 2
no-process-exit: 2
no-restricted-modules: 0
no-sync: 0
# Stylistic Issues
array-bracket-spacing: 0
block-spacing: 0
brace-style: 0
camelcase: 0
comma-spacing: 0
comma-style: 0
computed-property-spacing: 0
consistent-this: 0
eol-last: 0
func-names: 0
func-style: 0
id-length: 0
id-match: 0
indent: 0
jsx-quotes: 0
key-spacing: 0
linebreak-style: 0
lines-around-comment: 0
max-depth: 0
max-len: 0
max-nested-callbacks: 0
max-params: 0
max-statements: [2, 30]
new-cap: 0
new-parens: 0
newline-after-var: 0
no-array-constructor: 0
no-bitwise: 0
no-continue: 0
no-inline-comments: 0
no-lonely-if: 0
no-mixed-spaces-and-tabs: 0
no-multiple-empty-lines: 0
no-negated-condition: 0
no-nested-ternary: 0
no-new-object: 0
no-plusplus: 0
no-restricted-syntax: 0
no-spaced-func: 0
no-ternary: 0
no-trailing-spaces: 0
no-underscore-dangle: 0
no-unneeded-ternary: 0
object-curly-spacing: 0
one-var: 0
operator-assignment: 0
operator-linebreak: 0
padded-blocks: 0
quote-props: 0
quotes: 0
require-jsdoc: 0
semi-spacing: 0
semi: 0
sort-vars: 0
space-after-keywords: 0
space-before-blocks: 0
space-before-function-paren: 0
space-before-keywords: 0
space-in-parens: 0
space-infix-ops: 0
space-return-throw-case: 0
space-unary-ops: 0
spaced-comment: 0
wrap-regex: 0
# ECMAScript 6
arrow-body-style: 0
arrow-parens: 0
arrow-spacing: 0
constructor-super: 0
generator-star-spacing: 0
no-arrow-condition: 0
no-class-assign: 0
no-const-assign: 0
no-dupe-class-members: 0
no-this-before-super: 0
no-var: 0
object-shorthand: 0
prefer-arrow-callback: 0
prefer-const: 0
prefer-reflect: 0
prefer-spread: 0
prefer-template: 0
require-yield: 0

63
.github/workflows/django.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: Django CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
runs-on: ubuntu-latest
strategy:
matrix:
browser: ['chrome', 'firefox']
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BROWSER: ${{ matrix.browser }}
steps:
- uses: actions/checkout@v2
- uses: bahmutov/npm-install@v1
- run: node node_modules/gulp/bin/gulp build
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Cache python deps
uses: actions/cache@v2
with:
path: ${{ env.pythonLocation }}
key: ${{ env.pythonLocation }}-${{ hashFiles('requirements.txt') }}
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install pycodestyle coverage coveralls django_coverage_plugin pytest-cov
pip install --upgrade --upgrade-strategy eager -r requirements.txt
python manage.py collectstatic --noinput
- name: Cache gulp output
uses: actions/cache@v2
with:
path: static/
key: static-${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
- name: Basic Checks
run: |
pycodestyle . --exclude=migrations,node_modules
python manage.py check
python manage.py makemigrations --check --dry-run
- name: Run Tests
uses: paambaati/codeclimate-action@v2.7.5
env:
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
with:
coverageCommand: coverage run -m pytest -n 8
coverageLocations: |
${{github.workspace}}/.coverage:coverage.py
- uses: actions/upload-artifact@v2
if: failure()
with:
name: failure-screenshots ${{ matrix.test-group }}
path: screenshots/
retention-days: 5
- name: Coveralls
run: coveralls --service=github

17
.gitignore vendored
View File

@@ -68,19 +68,9 @@ target/
## Directory-based project format: ## Directory-based project format:
.idea/ .idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff: #Built dependencies
# .idea/workspace.xml pipeline/built_assets
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle: # Gradle:
# .idea/gradle.xml # .idea/gradle.xml
@@ -109,5 +99,4 @@ com_crashlytics_export_strings.xml
crashlytics.properties crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
.vscode/ .vscode/
/package-lock.json screenshots/
screenshots/

1
.idea/.name generated
View File

@@ -1 +0,0 @@
PyRIGS

5
.idea/encodings.xml generated
View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pyrigs.iml" filepath="$PROJECT_DIR$/.idea/pyrigs.iml" />
</modules>
</component>
</project>

View File

@@ -1,5 +0,0 @@
<component name="DependencyValidationManager">
<state>
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
</state>
</component>

7
.idea/vcs.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,3 @@
*.sqlite3 *.sqlite3
*.scss
*.md *.md
*.rb **/tests
Vagrantfile
config/vagrant/*
config/vagrant.yml

View File

@@ -1,37 +0,0 @@
language: python
python:
"3.8"
cache: pip
addons:
chrome: stable
before_install:
- export LANGUAGE=en_GB.UTF-8
install:
- |
latest=$(wget -qO- https://chromedriver.storage.googleapis.com/LATEST_RELEASE)
wget https://chromedriver.storage.googleapis.com/$latest/chromedriver_linux64.zip
- unzip chromedriver_linux64.zip
- export PATH=$PATH:$(pwd)
- chmod +x chromedriver
- pip install -r requirements.txt
- pip install coveralls codeclimate-test-reporter pycodestyle
before_script:
- export PATH=$PATH:/usr/lib/chromium-browser/
- python manage.py collectstatic --noinput
script:
- pycodestyle . --exclude=migrations,importer*
- python manage.py check
- python manage.py makemigrations --check --dry-run
- coverage run manage.py test --verbosity=2
after_success:
- coveralls
- codeclimate-test-reporter
notifications:
webhooks: https://fathomless-fjord-24024.herokuapp.com/notify

View File

@@ -1,6 +1,6 @@
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
from django.shortcuts import render
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from RIGS import models from RIGS import models

View File

@@ -8,11 +8,13 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/ https://docs.djangoproject.com/en/1.7/ref/settings/
""" """
import datetime
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os import os
import raven
import secrets import secrets
import datetime
import raven
from envparse import env
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DIR = os.path.dirname(os.path.dirname(__file__))
@@ -20,13 +22,15 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get( SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e')
'SECRET_KEY') else 'gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e'
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(int(os.environ.get('DEBUG'))) if os.environ.get('DEBUG') else True DEBUG = env('DEBUG', cast=bool, default=True)
STAGING = bool(int(os.environ.get('STAGING'))) if os.environ.get('STAGING') else False STAGING = env('STAGING', cast=bool, default=False)
CI = env('CI', cast=bool, default=False)
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com'] ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
@@ -150,11 +154,29 @@ LOGGING = {
} }
} }
# Tests lock up SQLite otherwise
if STAGING or CI:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
}
}
elif DEBUG:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
}
}
else:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'cache_table',
}
}
RAVEN_CONFIG = { RAVEN_CONFIG = {
'dsn': os.environ.get('RAVEN_DSN'), 'dsn': env('RAVEN_DSN', default=""),
# If you are using git, you can also automatically configure the
# release based on the git info.
# 'release': raven.fetch_git_sha(os.path.dirname(os.path.dirname(__file__))),
} }
# User system # User system
@@ -167,10 +189,8 @@ LOGOUT_URL = '/user/logout/'
ACCOUNT_ACTIVATION_DAYS = 7 ACCOUNT_ACTIVATION_DAYS = 7
# reCAPTCHA settings # reCAPTCHA settings
RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', RECAPTCHA_PUBLIC_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key
"6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key RECAPTCHA_PRIVATE_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY',
"6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
NOCAPTCHA = True NOCAPTCHA = True
SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error'] SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error']
@@ -179,13 +199,13 @@ SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error']
EMAILER_TEST = False EMAILER_TEST = False
if not DEBUG or EMAILER_TEST: if not DEBUG or EMAILER_TEST:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.environ.get('EMAIL_HOST') EMAIL_HOST = env('EMAIL_HOST')
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 25)) EMAIL_PORT = env('EMAIL_PORT', cast=int, default=25)
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') EMAIL_HOST_USER = env('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
EMAIL_USE_TLS = bool(int(os.environ.get('EMAIL_USE_TLS', 0))) EMAIL_USE_TLS = env('EMAIL_USE_TLS', cast=bool, default=False)
EMAIL_USE_SSL = bool(int(os.environ.get('EMAIL_USE_SSL', 0))) EMAIL_USE_SSL = env('EMAIL_USE_SSL', cast=bool, default=False)
DEFAULT_FROM_EMAIL = os.environ.get('EMAIL_FROM') DEFAULT_FROM_EMAIL = env('EMAIL_FROM')
else: else:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
@@ -216,6 +236,9 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
STATIC_DIRS = ( STATIC_DIRS = (
os.path.join(BASE_DIR, 'static/') os.path.join(BASE_DIR, 'static/')
) )
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'pipeline/built_assets/'),
]
TEMPLATES = [ TEMPLATES = [
{ {
@@ -244,10 +267,3 @@ 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'
RISK_ASSESSMENT_URL = os.environ.get('RISK_ASSESSMENT_URL') if os.environ.get(
'RISK_ASSESSMENT_URL') else "http://example.com"
RISK_ASSESSMENT_SECRET = os.environ.get('RISK_ASSESSMENT_SECRET') if os.environ.get(
'RISK_ASSESSMENT_SECRET') else secrets.token_hex(15)
IMGUR_UPLOAD_CLIENT_ID = os.environ.get('IMGUR_UPLOAD_CLIENT_ID', '')
IMGUR_UPLOAD_CLIENT_SECRET = os.environ.get('IMGUR_UPLOAD_CLIENT_SECRET', '')

0
PyRIGS/tests/__init__.py Normal file
View File

View File

@@ -1,16 +1,17 @@
import os
import pathlib
import sys
from datetime import datetime
import pytz
from django.conf import settings
from django.test import LiveServerTestCase from django.test import LiveServerTestCase
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from RIGS import models as rigsmodels from RIGS import models as rigsmodels
from . import pages from . import pages
import os from envparse import env
import pytz
from datetime import date, time, datetime, timedelta
from django.conf import settings
import imgurpython
import PyRIGS.settings
import sys
import pathlib
import inspect
def create_datetime(year, month, day, hour, min): def create_datetime(year, month, day, hour, min):
@@ -18,25 +19,26 @@ def create_datetime(year, month, day, hour, min):
return tz.localize(datetime(year, month, day, hour, min)).astimezone(pytz.utc) return tz.localize(datetime(year, month, day, hour, min)).astimezone(pytz.utc)
def create_browser(): def create_browser(browser):
options = webdriver.ChromeOptions() if browser == "firefox":
options.add_argument("--window-size=1920,1080") options = webdriver.FirefoxOptions()
# No caching, please and thank you options.headless = True
options.add_argument("--aggressive-cache-discard") driver = webdriver.Firefox(options=options)
options.add_argument("--disk-cache-size=0") else:
# God Save The Queen options = webdriver.ChromeOptions()
options.add_argument("--lang=en_GB") options.add_argument("--window-size=1920,1080")
if os.environ.get('CI', False):
options.add_argument("--headless") options.add_argument("--headless")
options.add_argument("--no-sandbox") if settings.CI:
driver = webdriver.Chrome(options=options) options.add_argument("--no-sandbox")
driver = webdriver.Chrome(options=options)
return driver return driver
class BaseTest(LiveServerTestCase): class BaseTest(LiveServerTestCase):
def setUp(self): def setUp(self):
super().setUpClass() super().setUpClass()
self.driver = create_browser() self.driver = create_browser(env('BROWSER', default="chrome"))
self.wait = WebDriverWait(self.driver, 15)
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
@@ -50,8 +52,8 @@ class AutoLoginTest(BaseTest):
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True) username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
self.profile.set_password("EventTestPassword") self.profile.set_password("EventTestPassword")
self.profile.save() self.profile.save()
loginPage = pages.LoginPage(self.driver, self.live_server_url).open() login_page = pages.LoginPage(self.driver, self.live_server_url).open()
loginPage.login("EventTest", "EventTestPassword") login_page.login("EventTest", "EventTestPassword")
def screenshot_failure(func): def screenshot_failure(func):
@@ -64,20 +66,9 @@ def screenshot_failure(func):
if not pathlib.Path("screenshots").is_dir(): if not pathlib.Path("screenshots").is_dir():
os.mkdir("screenshots") os.mkdir("screenshots")
self.driver.save_screenshot(screenshot_file) self.driver.save_screenshot(screenshot_file)
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
if settings.IMGUR_UPLOAD_CLIENT_ID != "":
config = {
'album': None,
'name': screenshot_name,
'title': screenshot_name,
'description': ""
}
client = imgurpython.ImgurClient(settings.IMGUR_UPLOAD_CLIENT_ID, settings.IMGUR_UPLOAD_CLIENT_SECRET)
image = client.upload_from_path(screenshot_file, config=config)
print("Error in test {} is at url {}".format(screenshot_name, image['link']), file=sys.stderr)
else:
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
raise e raise e
return wrapper_func return wrapper_func
@@ -88,15 +79,5 @@ def screenshot_failure_cls(cls):
return cls return cls
# Checks if animation is done def assert_times_equal(first_time, second_time):
class animation_is_finished(object): assert first_time.replace(microsecond=0) == second_time.replace(microsecond=0)
def __init__(self):
pass
def __call__(self, driver):
numberAnimating = driver.execute_script('return $(":animated").length')
finished = numberAnimating == 0
if finished:
import time
time.sleep(0.1)
return finished

View File

@@ -1,8 +1,8 @@
from pypom import Page, Region from pypom import Page
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver import Chrome
from selenium.common.exceptions import NoSuchElementException
from PyRIGS.tests import regions from PyRIGS.tests import regions
@@ -31,6 +31,7 @@ class BasePage(Page):
class FormPage(BasePage): class FormPage(BasePage):
_errors_selector = (By.CLASS_NAME, "alert-danger") _errors_selector = (By.CLASS_NAME, "alert-danger")
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
def remove_all_required(self): def remove_all_required(self):
self.driver.execute_script( self.driver.execute_script(
@@ -43,6 +44,7 @@ class FormPage(BasePage):
submit = self.find_element(*self._submit_locator) submit = self.find_element(*self._submit_locator)
ActionChains(self.driver).move_to_element(submit).perform() ActionChains(self.driver).move_to_element(submit).perform()
submit.click() submit.click()
self.wait.until(animation_is_finished())
self.wait.until(lambda x: self.errors != previous_errors or self.success) self.wait.until(lambda x: self.errors != previous_errors or self.success)
@property @property
@@ -72,3 +74,13 @@ class LoginPage(BasePage):
password_element.send_keys(password) password_element.send_keys(password)
self.find_element(*self._submit_locator).click() self.find_element(*self._submit_locator).click()
class animation_is_finished():
def __call__(self, driver):
number_animating = driver.execute_script('return $(":animated").length')
finished = number_animating == 0
if finished:
import time
time.sleep(0.1)
return finished

View File

@@ -1,13 +1,13 @@
from pypom import Region
from django.utils import timezone
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.select import Select
from selenium.webdriver.common.keys import Keys
import datetime import datetime
from django.conf import settings
from pypom import Region
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.select import Select
def parse_bool_from_string(string): def parse_bool_from_string(string):
# Used to convert from attribute strings to boolean values, written after I found this: # Used to convert from attribute strings to boolean values, written after I found this:
@@ -18,18 +18,22 @@ def parse_bool_from_string(string):
else: else:
return False return False
# 12-Hour vs 24-Hour Time. Affects widget display
def get_time_format(): def get_time_format():
# Default # Default
time_format = "%H:%M" time_format = "%H%M"
# If system is 12hr if settings.CI: # The CI is American
if timezone.now().strftime("%p"): time_format = "%I%M%p"
time_format = "%I:%M %p"
return time_format return time_format
def get_date_format():
date_format = "%d%m%Y"
if settings.CI: # And try as I might I can't stop it being so
date_format = "%m%d%Y"
return date_format
class BootstrapSelectElement(Region): class BootstrapSelectElement(Region):
_main_button_locator = (By.CSS_SELECTOR, 'button.dropdown-toggle') _main_button_locator = (By.CSS_SELECTOR, 'button.dropdown-toggle')
_option_box_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu') _option_box_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu')
@@ -71,8 +75,7 @@ class BootstrapSelectElement(Region):
self.open() self.open()
search_box.clear() search_box.clear()
search_box.send_keys(query) search_box.send_keys(query)
status_text = self.find_element(*self._status_locator) self.wait.until(expected_conditions.invisibility_of_element_located(*self._status_locator))
self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator))
@property @property
def options(self): def options(self):
@@ -126,6 +129,22 @@ class CheckBox(Region):
self.toggle() self.toggle()
class RadioSelect(Region): # Currently only works for yes/no radio selects
def set_value(self, value):
if value:
value = "0"
else:
value = "1"
self.find_element(By.XPATH, "//label[@for='{}_{}']".format(self.root.get_attribute("id"), value)).click()
@property
def value(self):
try:
return parse_bool_from_string(self.find_element(By.CSS_SELECTOR, '.custom-control-input:checked').get_attribute("value").lower())
except NoSuchElementException:
return None
class DatePicker(Region): class DatePicker(Region):
@property @property
def value(self): def value(self):
@@ -133,13 +152,13 @@ class DatePicker(Region):
def set_value(self, value): def set_value(self, value):
self.root.clear() self.root.clear()
self.root.send_keys(value.strftime("%d%m%Y")) self.root.send_keys(value.strftime(get_date_format()))
class TimePicker(Region): class TimePicker(Region):
@property @property
def value(self): def value(self):
return datetime.datetime.strptime(self.root.get_attribute("value"), get_time_format()) return datetime.datetime.strptime(self.root.get_attribute("value"), "%H:%M")
def set_value(self, value): def set_value(self, value):
self.root.clear() self.root.clear()
@@ -149,12 +168,12 @@ class TimePicker(Region):
class DateTimePicker(Region): class DateTimePicker(Region):
@property @property
def value(self): def value(self):
return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d " + get_time_format()) return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d %H:%M")
def set_value(self, value): def set_value(self, value):
self.root.clear() self.root.clear()
date = value.date().strftime("%d%m%Y") date = value.date().strftime(get_date_format())
time = value.time().strftime(get_time_format()) time = value.time().strftime(get_time_format())
self.root.send_keys(date) self.root.send_keys(date)
@@ -199,7 +218,7 @@ class ErrorPage(Region):
class Modal(Region): class Modal(Region):
_submit_locator = (By.CSS_SELECTOR, '.btn-primary') _submit_locator = (By.CSS_SELECTOR, '.btn-primary')
_header_selector = (By.TAG_NAME, 'h3') _header_selector = (By.TAG_NAME, 'h4')
form_items = { form_items = {
'name': (TextBox, (By.ID, 'id_name')) 'name': (TextBox, (By.ID, 'id_name'))

View File

@@ -1,28 +1,31 @@
from django.urls import path
from django.conf.urls import include, url
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.contrib.auth.decorators import login_required
from django.conf import settings from django.conf import settings
from django.views.decorators.clickjacking import xframe_options_exempt from django.conf.urls import include
from django.contrib.auth.views import LoginView from django.contrib import admin
from registration.backends.default.views import RegistrationView from django.contrib.auth.decorators import login_required
from PyRIGS.decorators import permission_required_with_403 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
import RIGS from django.urls import path, re_path
import users from django.views.generic import TemplateView
from PyRIGS import views from PyRIGS import views
urlpatterns = [ urlpatterns = [
path('', include('users.urls')), path('', include('versioning.urls')),
path('', include('RIGS.urls')), path('', include('RIGS.urls')),
path('assets/', include('assets.urls')), path('assets/', include('assets.urls')),
path('', login_required(views.Index.as_view()), name='index'),
# API # API
path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()), path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()),
name="api_secure"), name="api_secure"),
path('api/<str:model>/<int:pk>/', login_required(views.SecureAPIRequest.as_view()), path('api/<str:model>/<int:pk>/', login_required(views.SecureAPIRequest.as_view()),
name="api_secure"), name="api_secure"),
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
path('search_help/', views.SearchHelp.as_view(), name='search_help'),
path('', include('users.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
] ]
@@ -31,5 +34,6 @@ if settings.DEBUG:
import debug_toolbar import debug_toolbar
urlpatterns = [ urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)), re_path(r'^__debug__/', include(debug_toolbar.urls)),
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
] + urlpatterns ] + urlpatterns

View File

@@ -1,24 +1,34 @@
from django.core.exceptions import PermissionDenied import datetime
from django.http.response import HttpResponseRedirect import operator
from django.http import HttpResponse from functools import reduce
from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.views import generic
from django.contrib.auth.views import LoginView
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.core import serializers
from django.conf import settings
import simplejson import simplejson
from django.contrib import messages from django.contrib import messages
import datetime from django.core import serializers
import pytz from django.core.exceptions import PermissionDenied
import operator from django.db.models import Q
from registration.views import RegistrationView from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.views import generic
from RIGS import models, forms from RIGS import models
from assets import models as asset_models from assets import models as asset_models
from functools import reduce
def is_ajax(request):
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
# Displays the current rig count along with a few other bits and pieces
class Index(generic.TemplateView):
template_name = 'index.html'
def get_context_data(self, **kwargs):
context = super(Index, self).get_context_data(**kwargs)
context['rig_count'] = models.Event.objects.rig_count()
return context
class SecureAPIRequest(generic.View): class SecureAPIRequest(generic.View):
@@ -136,9 +146,31 @@ class SecureAPIRequest(generic.View):
return HttpResponse(model) return HttpResponse(model)
class ModalURLMixin:
def get_close_url(self, update, detail):
if is_ajax(self.request):
url = reverse_lazy('closemodal')
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
else:
url = reverse_lazy(detail, kwargs={
'pk': self.object.pk,
})
return url
class GenericListView(generic.ListView): class GenericListView(generic.ListView):
template_name = 'generic_list.html'
paginate_by = 20 paginate_by = 20
def get_context_data(self, **kwargs):
context = super(GenericListView, self).get_context_data(**kwargs)
context['page_title'] = self.model.__name__ + "s"
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
def get_queryset(self): def get_queryset(self):
q = self.request.GET.get('q', "") q = self.request.GET.get('q', "")
@@ -159,3 +191,54 @@ class GenericListView(generic.ListView):
if orderBy != "": if orderBy != "":
object_list = object_list.order_by(orderBy) object_list = object_list.order_by(orderBy)
return object_list return object_list
class GenericDetailView(generic.DetailView):
template_name = "generic_detail.html"
def get_context_data(self, **kwargs):
context = super(GenericDetailView, self).get_context_data(**kwargs)
context['page_title'] = "{} | {}".format(self.model.__name__, self.object.name)
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
class GenericUpdateView(generic.UpdateView):
template_name = "generic_form.html"
def get_context_data(self, **kwargs):
context = super(GenericUpdateView, self).get_context_data(**kwargs)
context['page_title'] = "Edit {}".format(self.model.__name__)
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
class GenericCreateView(generic.CreateView):
template_name = "generic_form.html"
def get_context_data(self, **kwargs):
context = super(GenericCreateView, self).get_context_data(**kwargs)
context['page_title'] = "Create {}".format(self.model.__name__)
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
class SearchHelp(generic.TemplateView):
template_name = 'search_help.html'
"""
Called from a modal window (e.g. when an item is submitted to an event/invoice).
May optionally also include some javascript in a success message to cause a load of
the new information onto the page.
"""
class CloseModal(generic.TemplateView):
template_name = 'closemodal.html'
def get_context_data(self, **kwargs):
return {'messages': messages.get_messages(self.request)}

115
README.md
View File

@@ -1,111 +1,18 @@
# TEC PA & Lighting - PyRIGS # # TEC PA & Lighting - PyRIGS #
[![Build Status](https://travis-ci.org/nottinghamtec/PyRIGS.svg)](https://travis-ci.org/nottinghamtec/PyRIGS) ![Build Status](https://github.com/nottinghamtec/PyRIGS/workflows/Django%20CI/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS) [![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS)
[![Maintainability](https://api.codeclimate.com/v1/badges/79ca3b8106911a1d143f/maintainability)](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability)
Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets.
The purpose of this project is to make the system more compatible and easier to understand such that should future changes be needed they can be made without having to understand the intricacies of Rails. For setup information and other such helpful stuff check the [Wiki](https://github.com/nottinghamtec/PyRIGS/wiki)
### What is this repository for? ### # Apps
When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production. - PyRIGS: Base app, stores 'global' information
- RIGS: Rigboard stuff - event calendar etc
Most of the documents here assume a basic knowledge of how Python and Django work (hint, if I don't say something, Google it, you will find 10000's of answers). The documentation is purely to be specific to TEC's application of the framework. - assets: Database of our kit, testing data etc
- versioning: Our custom logic built on top of django-reversion. Semi-modular.
### Editing ### - users: Our custom logic for registration and profiles. Semi-modular.
It is recommended that you use the PyCharm IDE by JetBrains. Whilst other editors are available, this is the best for integration with Django as it can automatically manage all the pesky admin commands that frequently need running, as well as nice integration with git. - training: SoonTM
For the more experienced developer/somebody who doesn't want a full IDE and wants it to open in less than the age of the universe, I can strongly recommend [Sublime Text](http://www.sublimetext.com/). It has a bit of a steeper learning curve, and won't manage anything Django/git related out of the box, but once you get the hang of it is by far the fastest and most powerful editor I have used (for any type of project).
Please contact TJP for details on how to acquire these.
### Python Environment ###
Whilst the Python version used is not critical to the running of the application, using the same version usually helps avoid a lot of issues. Orginally written with the C implementation of Python 2 (CPython 2, specifically the Python 2.7 standard), the application now runs in Python 3.
Once you have your Python distribution installed, go ahead an follow the steps to set up a virtualenv, which will isolate the project from the system environment.
#### PyCharm ####
If you are using the prefered PyCharm IDE, then this should be quite easy.
1. Select "File/Settings" -> "Project Interpreter"
2. Click the small cog in the top right
3. Select "Create VirtualEnv"
4. Enter a name and a location. This doesn't matter where, just make sure it makes sense and you remember it incase you need it later (I recommend calling it "pyrigs" in "~/.virtualenvs/pyrigs")
5. Select the base interpreter to your Python 3 base interpreter (Python 2 will work, just be careful)
6. Click OK, you *don't* want to inherit global packages or make it available to all projects.
7. Open a file such as manage.py. PyCharm should winge that dependances aren't installed. This might take a while to register, but give it change. When it does, click the button to install them and let it do it's thing. If for some reason PyCharm should decide that it doesn't want to help you here, see below for the console instructions on how to do this manually.
To run the Django application follow these steps
1. Select "Run/Edit Configurations"
2. Create a new "Django server", give it a sensible name for when you need it later.
3. You might need to set the interpreter to be your virtualenv.
4. Click "OK"
5. Run the application
#### Console Based ####
If you aren't using PyCharm, or want to use a console for some reason, this is really easy, there is even [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) to help things along. Simply run
```
virtualenv <dir>
```
Where dir is the directory you wish to create the virtualenv in.
Next activate the virtualenv.
```
Windows
<virtualenv_dir>/Scripts/activate.bat
Unix
source <virtualenv_dir>/bin/activate
```
Finally install the requirements using pip
```
cd <pyrigs project directory>
pip install -r requirements.txt
```
This might take a while, but be patient and you should then be ready to go.
To run the server under normal conditions when you are already in the virtualenv (see above)
```
python manage.py runserver
```
Please refer to Django documentation for a full list of options available here.
### Development using docker
```
docker build . -t pyrigs
docker run -it --rm -p=8000:8000 -v $(pwd):/app pyrigs
```
### Sample Data ###
Sample data is available to aid local development and user acceptance testing. To load this data into your local database, first ensure the database is empty:
```
python manage.py flush
```
Then load the sample data using the command:
```
python manage.py generateSampleData
```
4 user accounts are created for convenience:
|Username |Password |
|---------|---------|
|superuser|superuser|
|finance |finance |
|keyholder|keyholder|
|basic |basic |
### Testing ###
Tests are contained in 3 files. `RIGS/test_models.py` contains tests for logic within the data models. `RIGS/test_unit.py` contains "Live server" tests, using raw web requests. `RIGS/test_integration.py` contains user interface tests which take control of a web browser. For automated Travis tests, we use [Sauce Labs](https://saucelabs.com). When debugging locally, ensure that you have the latest version of Google Chrome installed, then install [chromedriver](https://sites.google.com/a/chromium.org/chromedriver/) and ensure it is on the `PATH`.
You can run the entire test suite, or you can run specific sections individually. For example, in order of specificity:
```
python manage.py test
python manage.py test RIGS.test_models
python manage.py test RIGS.test_models.EventTestCase
python manage.py test RIGS.test_models.EventTestCase.test_current_events
```
[![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com)

View File

@@ -1,26 +1,24 @@
from django.contrib import admin from django.contrib import admin
from RIGS import models, forms
from users import forms as user_forms
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _
from reversion.admin import VersionAdmin
from django.contrib.admin import helpers
from django.template.response import TemplateResponse
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.contrib.admin import helpers
from django.contrib.auth.admin import UserAdmin
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import Count from django.db.models import Count
from django.forms import ModelForm from django.forms import ModelForm
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from reversion import revisions as reversion from reversion import revisions as reversion
from reversion.admin import VersionAdmin
from RIGS import models
from users import forms as user_forms
# Register your models here. # Register your models here.
admin.site.register(models.VatRate, VersionAdmin) admin.site.register(models.VatRate, VersionAdmin)
admin.site.register(models.Event, VersionAdmin) admin.site.register(models.Event, VersionAdmin)
admin.site.register(models.EventItem, VersionAdmin) admin.site.register(models.EventItem, VersionAdmin)
admin.site.register(models.Invoice) admin.site.register(models.Invoice, VersionAdmin)
admin.site.register(models.Payment)
def approve_user(modeladmin, request, queryset): def approve_user(modeladmin, request, queryset):
@@ -125,3 +123,13 @@ class VenueAdmin(AssociateAdmin):
class OrganisationAdmin(AssociateAdmin): class OrganisationAdmin(AssociateAdmin):
list_display = ('id', 'name', 'phone', 'email', 'number_of_events') list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account'] merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account']
@admin.register(models.RiskAssessment)
class RiskAssessmentAdmin(VersionAdmin):
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
@admin.register(models.EventChecklist)
class EventChecklistAdmin(VersionAdmin):
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')

View File

@@ -1,36 +1,35 @@
import datetime import datetime
import re import re
import reversion
from django import forms
from django.contrib import messages from django.contrib import messages
from django.urls import reverse_lazy from django.db import transaction
from django.db.models import Q
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse_lazy
from django.views import generic from django.views import generic
from django.db.models import Q
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
from django.db.models import Q
from RIGS import models from RIGS import models
from django import forms
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'}) forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
class InvoiceIndex(generic.ListView): class InvoiceIndex(generic.ListView):
model = models.Invoice model = models.Invoice
template_name = 'invoice_list_active.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(InvoiceIndex, self).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
context['total'] = total context['page_title'] = "Outstanding Invoices ({} Events, £{:.2f})".format(len(list(context['object_list'])), total)
context['count'] = len(list(context['object_list'])) context['description'] = "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger"
return context return context
def get_queryset(self): def get_queryset(self):
@@ -54,6 +53,11 @@ class InvoiceDetail(generic.DetailView):
model = models.Invoice model = models.Invoice
template_name = 'invoice_detail.html' template_name = 'invoice_detail.html'
def get_context_data(self, **kwargs):
context = super(InvoiceDetail, self).get_context_data(**kwargs)
context['page_title'] = "Invoice {} ({})".format(self.object.display_id, self.object.invoice_date.strftime("%d/%m/%Y"))
return context
class InvoicePrint(generic.View): class InvoicePrint(generic.View):
def get(self, request, pk): def get(self, request, pk):
@@ -71,6 +75,7 @@ class InvoicePrint(generic.View):
}, },
'invoice': invoice, 'invoice': invoice,
'current_user': request.user, 'current_user': request.user,
'filename': 'Invoice {} for {} {}.pdf'.format(invoice.display_id, object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name))
} }
rml = template.render(context) rml = template.render(context)
@@ -79,11 +84,8 @@ class InvoicePrint(generic.View):
pdfData = buffer.read() pdfData = buffer.read()
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
response = HttpResponse(content_type='application/pdf') response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = "filename=Invoice %05d - N%05d | %s.pdf" % ( response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
invoice.pk, invoice.event.pk, escapedEventName)
response.write(pdfData) response.write(pdfData)
return response return response
@@ -127,6 +129,12 @@ class InvoiceArchive(generic.ListView):
template_name = 'invoice_list_archive.html' template_name = 'invoice_list_archive.html'
paginate_by = 25 paginate_by = 25
def get_context_data(self, **kwargs):
context = super(InvoiceArchive, self).get_context_data(**kwargs)
context['page_title'] = "Invoice Archive"
context['description'] = "This page displays all invoices: outstanding, paid, and void"
return context
def get_queryset(self): def get_queryset(self):
q = self.request.GET.get('q', "") q = self.request.GET.get('q', "")
@@ -166,8 +174,7 @@ class InvoiceWaiting(generic.ListView):
total = 0 total = 0
for obj in self.get_objects(): for obj in self.get_objects():
total += obj.sum_total total += obj.sum_total
context['total'] = total context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(self.get_objects()), total)
context['count'] = len(self.get_objects())
return context return context
def get_queryset(self): def get_queryset(self):
@@ -192,7 +199,10 @@ class InvoiceWaiting(generic.ListView):
class InvoiceEvent(generic.View): class InvoiceEvent(generic.View):
@transaction.atomic()
@reversion.create_revision()
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
reversion.set_user(self.request.user)
epk = kwargs.get('pk') epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk) event = models.Event.objects.get(pk=epk)
invoice, created = models.Invoice.objects.get_or_create(event=event) invoice, created = models.Invoice.objects.get_or_create(event=event)
@@ -201,6 +211,11 @@ class InvoiceEvent(generic.View):
invoice.invoice_date = datetime.date.today() invoice.invoice_date = datetime.date.today()
messages.success(self.request, 'Invoice created successfully') messages.success(self.request, 'Invoice created successfully')
if kwargs.get('void'):
invoice.void = not invoice.void
invoice.save()
messages.warning(self.request, 'Invoice voided')
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk})) return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))
@@ -218,6 +233,13 @@ class PaymentCreate(generic.CreateView):
initial.update({'invoice': invoice}) initial.update({'invoice': invoice})
return initial return initial
@transaction.atomic()
@reversion.create_revision()
def form_valid(self, form, *args, **kwargs):
reversion.add_to_revision(form.cleaned_data['invoice'])
reversion.set_comment("Payment added")
return super().form_valid(form, *args, **kwargs)
def get_success_url(self): def get_success_url(self):
messages.info(self.request, "location.reload()") messages.info(self.request, "location.reload()")
return reverse_lazy('closemodal') return reverse_lazy('closemodal')
@@ -225,6 +247,14 @@ class PaymentCreate(generic.CreateView):
class PaymentDelete(generic.DeleteView): class PaymentDelete(generic.DeleteView):
model = models.Payment model = models.Payment
template_name = 'payment_confirm_delete.html'
@transaction.atomic()
@reversion.create_revision()
def delete(self, *args, **kwargs):
reversion.add_to_revision(self.get_object().invoice)
reversion.set_comment("Payment removed")
return super().delete(*args, **kwargs)
def get_success_url(self): def get_success_url(self):
return self.request.POST.get('next') return self.request.POST.get('next')

View File

@@ -1,13 +1,11 @@
from datetime import datetime
import simplejson
from django import forms from django import forms
from django.utils import formats
from django.conf import settings from django.conf import settings
from django.core import serializers from django.core import serializers
from django.core.mail import EmailMessage, EmailMultiAlternatives from django.utils import timezone
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm from reversion import revisions as reversion
from registration.forms import RegistrationFormUniqueEmail
from django.contrib.auth.forms import AuthenticationForm
from captcha.fields import ReCaptchaField
import simplejson
from RIGS import models from RIGS import models
@@ -18,8 +16,6 @@ forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'
# Events Shit # Events Shit
class EventForm(forms.ModelForm): class EventForm(forms.ModelForm):
datetime_input_formats = list(settings.DATETIME_INPUT_FORMATS) datetime_input_formats = list(settings.DATETIME_INPUT_FORMATS)
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False) meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
@@ -153,3 +149,129 @@ class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
class EventAuthorisationRequestForm(forms.Form): class EventAuthorisationRequestForm(forms.Form):
email = forms.EmailField(required=True, label='Authoriser Email') email = forms.EmailField(required=True, label='Authoriser Email')
class EventRiskAssessmentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(EventRiskAssessmentForm, self).__init__(*args, **kwargs)
for name, field in self.fields.items():
if str(name) == 'supervisor_consulted':
field.widget = forms.CheckboxInput()
elif field.__class__ == forms.BooleanField:
field.widget = forms.RadioSelect(choices=[
(True, 'Yes'),
(False, 'No')
], attrs={'class': 'custom-control-input', 'required': 'true'})
def clean(self):
# Check expected values
unexpected_values = []
for field, value in models.RiskAssessment.expected_values.items():
if self.cleaned_data.get(field) != value:
unexpected_values.append("<li>{}</li>".format(self._meta.model._meta.get_field(field).help_text))
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
raise forms.ValidationError("Your answers to these questions: <ul>{}</ul> require consulting with a supervisor.".format(''.join([str(elem) for elem in unexpected_values])), code='unusual_answers')
return super(EventRiskAssessmentForm, self).clean()
class Meta:
model = models.RiskAssessment
fields = '__all__'
exclude = ['reviewed_at', 'reviewed_by']
class EventChecklistForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(EventChecklistForm, self).__init__(*args, **kwargs)
self.fields['date'].widget.format = '%Y-%m-%d'
for name, field in self.fields.items():
if field.__class__ == forms.NullBooleanField:
# Only display yes/no to user, the 'none' is only ever set in the background
field.widget = forms.CheckboxInput()
# Parsed from incoming form data by clean, then saved into models when the form is saved
items = {}
related_models = {
'venue': models.Venue,
'power_mic': models.Profile,
}
# Two possible formats
def parsedatetime(self, date_string):
try:
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S'))
except ValueError:
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M'))
# There's probably a thousand better ways to do this, but this one is mine
def clean(self):
vehicles = {key: val for key, val in self.data.items()
if key.startswith('vehicle')}
for key in vehicles:
pk = int(key.split('_')[1])
driver_key = 'driver_' + str(pk)
if(self.data[driver_key] == ''):
raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch')
else:
try:
item = models.EventChecklistVehicle.objects.get(pk=pk)
except models.EventChecklistVehicle.DoesNotExist:
item = models.EventChecklistVehicle()
item.vehicle = vehicles['vehicle_' + str(pk)]
item.driver = models.Profile.objects.get(pk=self.data[driver_key])
item.full_clean('checklist')
# item does not have a database pk yet as it isn't saved
self.items['v' + str(pk)] = item
crewmembers = {key: val for key, val in self.data.items()
if key.startswith('crewmember')}
other_fields = ['start', 'role', 'end']
for key in crewmembers:
pk = int(key.split('_')[1])
for field in other_fields:
value = self.data['{}_{}'.format(field, pk)]
if value == '':
raise forms.ValidationError('Add a {} to crewmember {}'.format(field, pk), code='{}_mismatch'.format(field))
try:
item = models.EventChecklistCrew.objects.get(pk=pk)
except models.EventChecklistCrew.DoesNotExist:
item = models.EventChecklistCrew()
item.crewmember = models.Profile.objects.get(pk=self.data['crewmember_' + str(pk)])
item.start = self.parsedatetime(self.data['start_' + str(pk)])
item.role = self.data['role_' + str(pk)]
item.end = self.parsedatetime(self.data['end_' + str(pk)])
item.full_clean('checklist')
# item does not have a database pk yet as it isn't saved
self.items['c' + str(pk)] = item
return super(EventChecklistForm, self).clean()
def save(self, commit=True):
checklist = super(EventChecklistForm, self).save(commit=False)
if (commit):
# Remove all existing, to be recreated from the form
checklist.vehicles.all().delete()
checklist.crew.all().delete()
checklist.save()
for key in self.items:
item = self.items[key]
reversion.add_to_revision(item)
# finish and save new database items
item.checklist = checklist
item.full_clean()
item.save()
self.items.clear()
return checklist
class Meta:
model = models.EventChecklist
fields = '__all__'
exclude = ['reviewed_at', 'reviewed_by']

217
RIGS/hs.py Normal file
View File

@@ -0,0 +1,217 @@
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils import timezone
from django.views import generic
from reversion import revisions as reversion
from RIGS import models, forms
class EventRiskAssessmentCreate(generic.CreateView):
model = models.RiskAssessment
template_name = 'risk_assessment_form.html'
form_class = forms.EventRiskAssessmentForm
def get(self, *args, **kwargs):
epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
# Check if RA exists
ra = models.RiskAssessment.objects.filter(event=event).first()
if ra is not None:
return HttpResponseRedirect(reverse_lazy('ra_edit', kwargs={'pk': ra.pk}))
return super(EventRiskAssessmentCreate, self).get(self)
def get_form(self, **kwargs):
form = super(EventRiskAssessmentCreate, self).get_form(**kwargs)
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
form.instance.event = event
return form
def get_context_data(self, **kwargs):
context = super(EventRiskAssessmentCreate, self).get_context_data(**kwargs)
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
context['event'] = event
context['page_title'] = 'Create Risk Assessment for Event {}'.format(event.display_id)
return context
def get_success_url(self):
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
class EventRiskAssessmentEdit(generic.UpdateView):
model = models.RiskAssessment
template_name = 'risk_assessment_form.html'
form_class = forms.EventRiskAssessmentForm
def get_success_url(self):
ra = self.get_object()
ra.reviewed_by = None
ra.reviewed_at = None
ra.save()
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(EventRiskAssessmentEdit, self).get_context_data(**kwargs)
rpk = self.kwargs.get('pk')
ra = models.RiskAssessment.objects.get(pk=rpk)
context['event'] = ra.event
context['edit'] = True
context['page_title'] = 'Edit Risk Assessment for Event {}'.format(ra.event.display_id)
return context
class EventRiskAssessmentDetail(generic.DetailView):
model = models.RiskAssessment
template_name = 'risk_assessment_detail.html'
class EventRiskAssessmentList(generic.ListView):
paginate_by = 20
model = models.RiskAssessment
template_name = 'hs_object_list.html'
def get_context_data(self, **kwargs):
context = super(EventRiskAssessmentList, self).get_context_data(**kwargs)
context['title'] = 'Risk Assessment'
context['view'] = 'ra_detail'
context['edit'] = 'ra_edit'
context['review'] = 'ra_review'
context['perm'] = 'perms.RIGS.review_riskassessment'
context['fields'] = [n.name for n in list(self.model._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
return context
class EventRiskAssessmentReview(generic.View):
def get(self, *args, **kwargs):
rpk = kwargs.get('pk')
ra = models.RiskAssessment.objects.get(pk=rpk)
with reversion.create_revision():
reversion.set_user(self.request.user)
ra.reviewed_by = self.request.user
ra.reviewed_at = timezone.now()
ra.save()
return HttpResponseRedirect(reverse_lazy('ra_list'))
class EventChecklistDetail(generic.DetailView):
model = models.EventChecklist
template_name = 'event_checklist_detail.html'
def get_context_data(self, **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)
return context
class EventChecklistEdit(generic.UpdateView):
model = models.EventChecklist
template_name = 'event_checklist_form.html'
form_class = forms.EventChecklistForm
def get_success_url(self):
ec = self.get_object()
ec.reviewed_by = None
ec.reviewed_at = None
ec.save()
return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(EventChecklistEdit, self).get_context_data(**kwargs)
pk = self.kwargs.get('pk')
ec = models.EventChecklist.objects.get(pk=pk)
context['event'] = ec.event
context['edit'] = True
context['page_title'] = 'Edit Event Checklist for Event {}'.format(ec.event.display_id)
form = context['form']
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
for field, model in form.related_models.items():
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
return context
class EventChecklistCreate(generic.CreateView):
model = models.EventChecklist
template_name = 'event_checklist_form.html'
form_class = forms.EventChecklistForm
# From both business logic and programming POVs, RAs must exist before ECs!
def get(self, *args, **kwargs):
epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
# Check if RA exists
ra = models.RiskAssessment.objects.filter(event=event).first()
if ra is None:
messages.error(self.request, 'A Risk Assessment must exist prior to creating any Event Checklists for {}! Please create one now.'.format(event))
return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
return super(EventChecklistCreate, self).get(self)
def get_form(self, **kwargs):
form = super(EventChecklistCreate, self).get_form(**kwargs)
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
form.instance.event = event
return form
def get_context_data(self, **kwargs):
context = super(EventChecklistCreate, self).get_context_data(**kwargs)
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
context['event'] = event
context['page_title'] = 'Create Event Checklist for Event {}'.format(event.display_id)
return context
def get_success_url(self):
return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
class EventChecklistList(generic.ListView):
paginate_by = 20
model = models.EventChecklist
template_name = 'hs_object_list.html'
def get_context_data(self, **kwargs):
context = super(EventChecklistList, self).get_context_data(**kwargs)
context['title'] = 'Event Checklist'
context['view'] = 'ec_detail'
context['edit'] = 'ec_edit'
context['review'] = 'ec_review'
context['perm'] = 'perms.RIGS.review_eventchecklist'
context['fields'] = [n.name for n in list(self.model._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
return context
class EventChecklistReview(generic.View):
def get(self, *args, **kwargs):
rpk = kwargs.get('pk')
ec = models.EventChecklist.objects.get(pk=rpk)
with reversion.create_revision():
reversion.set_user(self.request.user)
ec.reviewed_by = self.request.user
ec.reviewed_at = timezone.now()
ec.save()
return HttpResponseRedirect(reverse_lazy('ec_list'))
class HSList(generic.ListView):
paginate_by = 20
model = models.Event
template_name = 'hs_list.html'
def get_queryset(self):
return models.Event.objects.all().order_by('-start_date')
def get_context_data(self, **kwargs):
context = super(HSList, self).get_context_data(**kwargs)
context['page_title'] = 'H&S Overview'
return context

View File

@@ -1,12 +1,11 @@
from RIGS import models, forms
from django_ical.views import ICalFeed
from django.db.models import Q
from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.utils import timezone
from django.conf import settings
import datetime import datetime
import pytz import pytz
from django.conf import settings
from django.db.models import Q
from django_ical.views import ICalFeed
from RIGS import models
class CalendarICS(ICalFeed): class CalendarICS(ICalFeed):
@@ -102,7 +101,7 @@ class CalendarICS(ICalFeed):
return item.earliest_time return item.earliest_time
def item_end_datetime(self, item): def item_end_datetime(self, item):
if type(item.latest_time) == datetime.date: # Ical end_datetime is non-inclusive, so add a day if isinstance(item.latest_time, datetime.date): # Ical end_datetime is non-inclusive, so add a day
return item.latest_time + datetime.timedelta(days=1) return item.latest_time + datetime.timedelta(days=1)
return item.latest_time return item.latest_time

View File

@@ -1,5 +1,5 @@
from django.core.management.base import BaseCommand, CommandError
from django.core.management import call_command from django.core.management import call_command
from django.core.management.base import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):

View File

@@ -1,11 +1,11 @@
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group, Permission
from django.db import transaction
from reversion import revisions as reversion
import datetime import datetime
import random import random
from django.contrib.auth.models import Group, Permission
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from reversion import revisions as reversion
from RIGS import models from RIGS import models
@@ -20,6 +20,7 @@ class Command(BaseCommand):
keyholder_group = None keyholder_group = None
finance_group = None finance_group = None
hs_group = None
def handle(self, *args, **options): def handle(self, *args, **options):
from django.conf import settings from django.conf import settings
@@ -165,6 +166,7 @@ class Command(BaseCommand):
def setupGroups(self): def setupGroups(self):
self.keyholder_group = Group.objects.create(name='Keyholders') self.keyholder_group = Group.objects.create(name='Keyholders')
self.finance_group = Group.objects.create(name='Finance') self.finance_group = Group.objects.create(name='Finance')
self.hs_group = Group.objects.create(name='H&S')
keyholderPerms = ["add_event", "change_event", "view_event", keyholderPerms = ["add_event", "change_event", "view_event",
"add_eventitem", "change_eventitem", "delete_eventitem", "add_eventitem", "change_eventitem", "delete_eventitem",
@@ -172,10 +174,17 @@ class Command(BaseCommand):
"add_person", "change_person", "view_person", "view_profile", "add_person", "change_person", "view_person", "view_profile",
"add_venue", "change_venue", "view_venue", "add_venue", "change_venue", "view_venue",
"add_asset", "change_asset", "delete_asset", "add_asset", "change_asset", "delete_asset",
"asset_finance", "view_asset", "view_supplier", "asset_finance", "view_asset", "view_supplier", "change_supplier", "asset_finance",
"add_supplier"] "add_supplier", "view_cabletype", "change_cabletype",
"add_cabletype", "view_eventchecklist", "change_eventchecklist",
"add_eventchecklist", "view_riskassessment", "change_riskassessment",
"add_riskassessment", "add_eventchecklistcrew", "change_eventchecklistcrew",
"delete_eventchecklistcrew", "view_eventchecklistcrew", "add_eventchecklistvehicle",
"change_eventchecklistvehicle",
"delete_eventchecklistvehicle", "view_eventchecklistvehicle", ]
financePerms = keyholderPerms + ["add_invoice", "change_invoice", "view_invoice", financePerms = keyholderPerms + ["add_invoice", "change_invoice", "view_invoice",
"add_payment", "change_payment", "delete_payment"] "add_payment", "change_payment", "delete_payment"]
hsPerms = keyholderPerms + ["review_riskassessment", "review_eventchecklist"]
for permId in keyholderPerms: for permId in keyholderPerms:
self.keyholder_group.permissions.add(Permission.objects.get(codename=permId)) self.keyholder_group.permissions.add(Permission.objects.get(codename=permId))
@@ -183,6 +192,9 @@ class Command(BaseCommand):
for permId in financePerms: for permId in financePerms:
self.finance_group.permissions.add(Permission.objects.get(codename=permId)) self.finance_group.permissions.add(Permission.objects.get(codename=permId))
for permId in hsPerms:
self.hs_group.permissions.add(Permission.objects.get(codename=permId))
def setupGenericProfiles(self): def setupGenericProfiles(self):
names = ["Clara Oswin Oswald", "Rory Williams", "Amy Pond", "River Song", "Martha Jones", "Donna Noble", names = ["Clara Oswin Oswald", "Rory Williams", "Amy Pond", "River Song", "Martha Jones", "Donna Noble",
"Jack Harkness", "Mickey Smith", "Rose Tyler"] "Jack Harkness", "Mickey Smith", "Rose Tyler"]
@@ -207,21 +219,29 @@ class Command(BaseCommand):
financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User", financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User",
initials="FU", initials="FU",
email="financeuser@example.com", is_active=True) email="financeuser@example.com", is_active=True, is_approved=True)
financeUser.groups.add(self.finance_group) financeUser.groups.add(self.finance_group)
financeUser.groups.add(self.keyholder_group) financeUser.groups.add(self.keyholder_group)
financeUser.set_password('finance') financeUser.set_password('finance')
financeUser.save() financeUser.save()
hsUser = models.Profile.objects.create(username="hs", first_name="HS", last_name="User",
initials="HSU",
email="hsuser@example.com", is_active=True, is_approved=True)
hsUser.groups.add(self.hs_group)
hsUser.groups.add(self.keyholder_group)
hsUser.set_password('hs')
hsUser.save()
keyholderUser = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User", keyholderUser = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User",
initials="KU", initials="KU",
email="keyholderuser@example.com", is_active=True) email="keyholderuser@example.com", is_active=True, is_approved=True)
keyholderUser.groups.add(self.keyholder_group) keyholderUser.groups.add(self.keyholder_group)
keyholderUser.set_password('keyholder') keyholderUser.set_password('keyholder')
keyholderUser.save() keyholderUser.save()
basicUser = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User", initials="BU", basicUser = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User", initials="BU",
email="basicuser@example.com", is_active=True) email="basicuser@example.com", is_active=True, is_approved=True)
basicUser.set_password('basic') basicUser.set_password('basic')
basicUser.save() basicUser.save()

View File

@@ -0,0 +1,190 @@
# Generated by Django 3.1.2 on 2021-01-23 19:10
import RIGS.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0038_auto_20200306_2000'),
]
operations = [
migrations.CreateModel(
name='EventChecklist',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField()),
('safe_parking', models.BooleanField(blank=True, help_text='Vehicles parked safely?<br><small>(does not obstruct venue access)</small>', null=True)),
('safe_packing', models.BooleanField(blank=True, help_text='Equipment packed away safely?<br><small>(including flightcases)</small>', null=True)),
('exits', models.BooleanField(blank=True, help_text='Emergency exits clear?', null=True)),
('trip_hazard', models.BooleanField(blank=True, help_text='Appropriate barriers around kit and cabling secured?', null=True)),
('warning_signs', models.BooleanField(blank=True, help_text='Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>')),
('ear_plugs', models.BooleanField(blank=True, help_text='Ear plugs issued to crew where needed?', null=True)),
('hs_location', models.CharField(blank=True, help_text='Location of Safety Bag/Box', max_length=255, null=True)),
('extinguishers_location', models.CharField(blank=True, help_text='Location of fire extinguishers', max_length=255, null=True)),
('rcds', models.BooleanField(blank=True, help_text='RCDs installed where needed and tested?', null=True)),
('supply_test', models.BooleanField(blank=True, help_text='Electrical supplies tested?<br><small>(using socket tester)</small>', null=True)),
('earthing', models.BooleanField(blank=True, help_text='Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>', null=True)),
('pat', models.BooleanField(blank=True, help_text='All equipment in PAT period?', null=True)),
('source_rcd', models.BooleanField(blank=True, help_text='Source RCD protected?<br><small>(if cable is more than 3m long) </small>', null=True)),
('labelling', models.BooleanField(blank=True, help_text='Appropriate and clear labelling on distribution and cabling?', null=True)),
('fd_voltage_l1', models.IntegerField(blank=True, help_text='L1 - N', null=True, verbose_name='First Distro Voltage L1-N')),
('fd_voltage_l2', models.IntegerField(blank=True, help_text='L2 - N', null=True, verbose_name='First Distro Voltage L2-N')),
('fd_voltage_l3', models.IntegerField(blank=True, help_text='L3 - N', null=True, verbose_name='First Distro Voltage L3-N')),
('fd_phase_rotation', models.BooleanField(blank=True, help_text='Phase Rotation<br><small>(if required)</small>', null=True, verbose_name='Phase Rotation')),
('fd_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True, verbose_name='Earth Fault Loop Impedance')),
('fd_pssc', models.IntegerField(blank=True, help_text='Prospective Short Circuit Current', null=True, verbose_name='PSCC')),
('w1_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
('w1_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
('w1_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
('w1_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
('w2_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
('w2_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
('w2_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
('w2_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
('w3_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
('w3_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
('w3_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
('w3_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
('all_rcds_tested', models.BooleanField(blank=True, help_text='All circuit RCDs tested?<br><small>(using test button)</small>', null=True)),
('public_sockets_tested', models.BooleanField(blank=True, help_text='Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>', null=True)),
('reviewed_at', models.DateTimeField(null=True)),
],
options={
'ordering': ['event'],
'permissions': [('review_eventchecklist', 'Can review Event Checklists')],
},
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.CreateModel(
name='EventChecklistCrew',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(max_length=255)),
('start', models.DateTimeField()),
('end', models.DateTimeField()),
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.eventchecklist')),
],
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.CreateModel(
name='EventChecklistVehicle',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('vehicle', models.CharField(max_length=255)),
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to='RIGS.eventchecklist')),
],
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.CreateModel(
name='RiskAssessment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nonstandard_equipment', models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by <a href='https://nottinghamtec.sharepoint.com/:f:/g/HealthAndSafety/Eo4xED_DrqFFsfYIjKzMZIIB6Gm_ZfR-a8l84RnzxtBjrA?e=Bf0Haw'>TEC's standard risk assessments and method statements?</a>")),
('nonstandard_use', models.BooleanField(help_text='Are TEC using their equipment in a way that is abnormal?<br><small>i.e. Not covered by TECs standard health and safety documentation</small>')),
('contractors', models.BooleanField(help_text='Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>')),
('other_companies', models.BooleanField(help_text='Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>')),
('crew_fatigue', models.BooleanField(help_text='Is crew fatigue likely to be a risk at any point during this event?')),
('general_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
('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?')),
('outside', models.BooleanField(help_text='Is the event outdoors?')),
('generators', models.BooleanField(help_text='Will generators be used?')),
('other_companies_power', models.BooleanField(help_text='Will TEC be supplying power to any other companies?')),
('nonstandard_equipment_power', models.BooleanField(help_text='Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?')),
('multiple_electrical_environments', models.BooleanField(help_text='Will the electrical installation occupy more than one electrical environment?')),
('power_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
('power_plan', models.URLField(blank=True, help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", null=True, validators=[RIGS.models.validate_url])),
('noise_monitoring', models.BooleanField(help_text='Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?')),
('sound_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
('known_venue', models.BooleanField(help_text='Is this venue new to you (the MIC) or new to TEC?')),
('safe_loading', models.BooleanField(help_text='Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)')),
('safe_storage', models.BooleanField(help_text='Are there any problems with safe and secure equipment storage?')),
('area_outside_of_control', models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")),
('barrier_required', models.BooleanField(help_text='Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?')),
('nonstandard_emergency_procedure', models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")),
('special_structures', models.BooleanField(help_text='Does the event require use of winch stands, motors, MPT Towers, or staging?')),
('suspended_structures', models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")),
('persons_responsible_structures', models.TextField(blank=True, help_text='Who are the persons on site responsible for their use?', null=True)),
('rigging_plan', models.URLField(blank=True, help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", null=True, validators=[RIGS.models.validate_url])),
('reviewed_at', models.DateTimeField(null=True)),
('supervisor_consulted', models.BooleanField(null=True)),
],
options={
'ordering': ['event'],
'permissions': [('review_riskassessment', 'Can review Risk Assessments')],
},
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.RemoveField(
model_name='eventcrew',
name='event',
),
migrations.RemoveField(
model_name='eventcrew',
name='user',
),
migrations.DeleteModel(
name='RIGSVersion',
),
migrations.RemoveField(
model_name='event',
name='risk_assessment_edit_url',
),
migrations.AlterField(
model_name='profile',
name='first_name',
field=models.CharField(blank=True, max_length=150, verbose_name='first name'),
),
migrations.DeleteModel(
name='EventCrew',
),
migrations.AddField(
model_name='riskassessment',
name='event',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='RIGS.event'),
),
migrations.AddField(
model_name='riskassessment',
name='power_mic',
field=models.ForeignKey(blank=True, 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)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='power_mic', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
),
migrations.AddField(
model_name='riskassessment',
name='reviewed_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer'),
),
migrations.AddField(
model_name='eventchecklistvehicle',
name='driver',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='eventchecklistcrew',
name='crewmember',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crewed', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='eventchecklist',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to='RIGS.event'),
),
migrations.AddField(
model_name='eventchecklist',
name='power_mic',
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC?', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
),
migrations.AddField(
model_name='eventchecklist',
name='reviewed_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer'),
),
migrations.AddField(
model_name='eventchecklist',
name='venue',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.venue'),
),
]

View File

@@ -1,16 +0,0 @@
# Generated by Django 3.0.3 on 2020-03-18 00:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0038_auto_20200306_2000'),
]
operations = [
migrations.DeleteModel(
name='EventCrew',
),
]

View File

@@ -1,16 +0,0 @@
# Generated by Django 3.0.3 on 2020-04-15 18:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0039_delete_eventcrew'),
]
operations = [
migrations.DeleteModel(
name='RIGSVersion',
),
]

View File

@@ -1,23 +1,22 @@
import datetime import datetime
import hashlib import hashlib
import datetime import random
import pytz import string
from collections import Counter
from decimal import Decimal
from urllib.parse import urlparse
from django.db import models import pytz
from django.contrib.auth.models import AbstractUser from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from reversion import revisions as reversion from reversion import revisions as reversion
from reversion.models import Version from reversion.models import Version
import string
import random
from collections import Counter
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.urls import reverse_lazy
class Profile(AbstractUser): class Profile(AbstractUser):
@@ -65,8 +64,15 @@ 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(object): class RevisionMixin(object):
@property
def is_first_version(self):
versions = Version.objects.get_for_object(self)
return len(versions) == 1
@property @property
def current_version(self): def current_version(self):
version = Version.objects.get_for_object(self).select_related('revision').first() version = Version.objects.get_for_object(self).select_related('revision').first()
@@ -189,6 +195,8 @@ class VatRate(models.Model, RevisionMixin):
objects = VatManager() objects = VatManager()
reversion_hide = True
@property @property
def as_percent(self): def as_percent(self):
return self.rate * 100 return self.rate * 100
@@ -267,9 +275,7 @@ class EventManager(models.Manager):
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False, is_rig=True) & ~models.Q( (models.Q(end_date__gte=timezone.now().date(), dry_hire=False, is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Ends after status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=timezone.now().date(), is_rig=True) & ~models.Q( (models.Q(dry_hire=True, start_date__gte=timezone.now().date(), is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Active dry hire status=Event.CANCELLED)) # Active dry hire
(models.Q(dry_hire=True, checked_in_by__isnull=True, is_rig=True) & (
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) # Active dry hire GT
).count() ).count()
return event_count return event_count
@@ -326,8 +332,12 @@ class Event(models.Model, RevisionMixin):
auth_request_at = models.DateTimeField(null=True, blank=True) auth_request_at = models.DateTimeField(null=True, blank=True)
auth_request_to = models.EmailField(null=True, blank=True) auth_request_to = models.EmailField(null=True, blank=True)
# Risk assessment info @property
risk_assessment_edit_url = models.CharField(verbose_name="risk assessment", max_length=255, blank=True, null=True) def display_id(self):
if self.is_rig:
return str("N%05d" % self.pk)
else:
return self.pk
# Calculated values # Calculated values
""" """
@@ -336,17 +346,6 @@ class Event(models.Model, RevisionMixin):
@property @property
def sum_total(self): def sum_total(self):
# Manual querying is required for efficiency whilst maintaining floating point arithmetic
# if connection.vendor == 'postgresql':
# sql = "SELECT SUM(quantity * cost) AS sum_total FROM \"RIGS_eventitem\" WHERE event_id=%i" % self.id
# else:
# sql = "SELECT id, SUM(quantity * cost) AS sum_total FROM RIGS_eventitem WHERE event_id=%i" % self.id
# total = self.items.raw(sql)[0]
# if total.sum_total:
# return total.sum_total
# total = 0.0
# for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"):
# total += item.sum
total = EventItem.objects.filter(event=self).aggregate( total = EventItem.objects.filter(event=self).aggregate(
sum_total=models.Sum(models.F('cost') * models.F('quantity'), sum_total=models.Sum(models.F('cost') * models.F('quantity'),
output_field=models.DecimalField(max_digits=10, decimal_places=2)) output_field=models.DecimalField(max_digits=10, decimal_places=2))
@@ -380,8 +379,8 @@ class Event(models.Model, RevisionMixin):
return (self.status == self.BOOKED or self.status == self.CONFIRMED) return (self.status == self.BOOKED or self.status == self.CONFIRMED)
@property @property
def authorised(self): def hs_done(self):
return not self.internal and self.purchase_order or self.authorisation.amount == self.total return self.riskassessment is not None and len(self.checklists.all()) > 0
@property @property
def has_start_time(self): def has_start_time(self):
@@ -445,7 +444,14 @@ class Event(models.Model, RevisionMixin):
@property @property
def internal(self): def internal(self):
return self.organisation and self.organisation.union_account return bool(self.organisation and self.organisation.union_account)
@property
def authorised(self):
if self.internal:
return self.authorisation.amount == self.total
else:
return bool(self.purchase_order)
objects = EventManager() objects = EventManager()
@@ -453,22 +459,26 @@ class Event(models.Model, RevisionMixin):
return reverse_lazy('event_detail', kwargs={'pk': self.pk}) return reverse_lazy('event_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return str(self.pk) + ": " + self.name return "{}: {}".format(self.display_id, self.name)
def clean(self): def clean(self):
errdict = {}
if self.end_date and self.start_date > self.end_date: if self.end_date and self.start_date > self.end_date:
raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.') errdict['end_date'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
startEndSameDay = not self.end_date or self.end_date == self.start_date startEndSameDay = not self.end_date or self.end_date == self.start_date
hasStartAndEnd = self.has_start_time and self.has_end_time hasStartAndEnd = self.has_start_time and self.has_end_time
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time: if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.') errdict['end_time'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
if self.access_at is not None: if self.access_at is not None:
if self.access_at.date() > self.start_date: if self.access_at.date() > self.start_date:
raise ValidationError('Regardless of what some clients might think, access time cannot be after the event has started.') errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
elif self.start_time is not None and self.start_date == self.access_at.date() and self.access_at.time() > self.start_time: elif self.start_time is not None and self.start_date == self.access_at.date() and self.access_at.time() > self.start_time:
raise ValidationError('Regardless of what some clients might think, access time cannot be after the event has started.') errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
if errdict != {}: # If there was an error when validation
raise ValidationError(errdict)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Call :meth:`full_clean` before saving.""" """Call :meth:`full_clean` before saving."""
@@ -476,7 +486,8 @@ class Event(models.Model, RevisionMixin):
super(Event, self).save(*args, **kwargs) super(Event, self).save(*args, **kwargs)
class EventItem(models.Model): @reversion.register
class EventItem(models.Model, RevisionMixin):
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE) event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
@@ -484,6 +495,8 @@ class EventItem(models.Model):
cost = models.DecimalField(max_digits=10, decimal_places=2) cost = models.DecimalField(max_digits=10, decimal_places=2)
order = models.IntegerField() order = models.IntegerField()
reversion_hide = True
@property @property
def total_cost(self): def total_cost(self):
return self.cost * self.quantity return self.cost * self.quantity
@@ -494,6 +507,10 @@ class EventItem(models.Model):
def __str__(self): def __str__(self):
return str(self.event.pk) + "." + str(self.order) + ": " + self.event.name + " | " + self.name return str(self.event.pk) + "." + str(self.order) + ": " + self.event.name + " | " + self.name
@property
def activity_feed_string(self):
return str("item {}".format(self.name))
@reversion.register @reversion.register
class EventAuthorisation(models.Model, RevisionMixin): class EventAuthorisation(models.Model, RevisionMixin):
@@ -510,15 +527,17 @@ class EventAuthorisation(models.Model, RevisionMixin):
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return str("N%05d" % self.event.pk + ' (requested by ' + self.sent_by.initials + ')') return "{} (requested by {})".format(self.event.display_id, self.sent_by.initials)
@reversion.register(follow=['payment_set']) @reversion.register(follow=['payment_set'])
class Invoice(models.Model): class Invoice(models.Model, RevisionMixin):
event = models.OneToOneField('Event', on_delete=models.CASCADE) event = models.OneToOneField('Event', on_delete=models.CASCADE)
invoice_date = models.DateField(auto_now_add=True) invoice_date = models.DateField(auto_now_add=True)
void = models.BooleanField(default=False) void = models.BooleanField(default=False)
reversion_perm = 'RIGS.view_invoice'
@property @property
def sum_total(self): def sum_total(self):
return self.event.sum_total return self.event.sum_total
@@ -542,14 +561,26 @@ class Invoice(models.Model):
def is_closed(self): def is_closed(self):
return self.balance == 0 or self.void return self.balance == 0 or self.void
def get_absolute_url(self):
return reverse_lazy('invoice_detail', kwargs={'pk': self.pk})
@property
def activity_feed_string(self):
return "#{} for Event {}".format(self.display_id, "N%05d" % self.event.pk)
def __str__(self): def __str__(self):
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance) return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
@property
def display_id(self):
return "{:05d}".format(self.pk)
class Meta: class Meta:
ordering = ['-invoice_date'] ordering = ['-invoice_date']
class Payment(models.Model): @reversion.register
class Payment(models.Model, RevisionMixin):
CASH = 'C' CASH = 'C'
INTERNAL = 'I' INTERNAL = 'I'
EXTERNAL = 'E' EXTERNAL = 'E'
@@ -568,5 +599,239 @@ class Payment(models.Model):
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT') amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True) method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True)
reversion_hide = True
def __str__(self): def __str__(self):
return "%s: %d" % (self.get_method_display(), self.amount) return "%s: %d" % (self.get_method_display(), self.amount)
@property
def activity_feed_string(self):
return str("payment of £{}".format(self.amount))
def validate_url(value):
if not value:
return # Required error is done the field
obj = urlparse(value)
if obj.hostname not in ('nottinghamtec.sharepoint.com'):
raise ValidationError('URL must point to a location on the TEC Sharepoint')
@reversion.register
class RiskAssessment(models.Model, RevisionMixin):
SMALL = (0, 'Small')
MEDIUM = (1, 'Medium')
LARGE = (2, 'Large')
SIZES = (SMALL, MEDIUM, LARGE)
event = models.OneToOneField('Event', on_delete=models.CASCADE)
# General
nonstandard_equipment = models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by <a href='https://nottinghamtec.sharepoint.com/:f:/g/HealthAndSafety/Eo4xED_DrqFFsfYIjKzMZIIB6Gm_ZfR-a8l84RnzxtBjrA?e=Bf0Haw'>"
"TEC's standard risk assessments and method statements?</a>")
nonstandard_use = models.BooleanField(help_text="Are TEC using their equipment in a way that is abnormal?<br><small>i.e. Not covered by TECs standard health and safety documentation</small>")
contractors = models.BooleanField(help_text="Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>")
other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>")
crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?")
general_notes = models.TextField(blank=True, null=True, help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
# Power
# event_size = models.IntegerField(blank=True, null=True, choices=SIZES)
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,
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)")
outside = models.BooleanField(help_text="Is the event outdoors?")
generators = models.BooleanField(help_text="Will generators be used?")
other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?")
nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?")
multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?")
power_notes = models.TextField(blank=True, null=True, help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
power_plan = models.URLField(blank=True, null=True, help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
# Sound
noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?")
sound_notes = models.TextField(blank=True, null=True, help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
# Site
known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
safe_loading = models.BooleanField(help_text="Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)")
safe_storage = models.BooleanField(help_text="Are there any problems with safe and secure equipment storage?")
area_outside_of_control = models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")
barrier_required = models.BooleanField(help_text="Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?")
nonstandard_emergency_procedure = models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")
# Structures
special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?")
suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")
persons_responsible_structures = models.TextField(blank=True, null=True, help_text="Who are the persons on site responsible for their use?")
rigging_plan = models.URLField(blank=True, null=True, help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
# Blimey that was a lot of options
reviewed_at = models.DateTimeField(null=True)
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
verbose_name="Reviewer", on_delete=models.CASCADE)
supervisor_consulted = models.BooleanField(null=True)
expected_values = {
'nonstandard_equipment': False,
'nonstandard_use': False,
'contractors': False,
'other_companies': False,
'crew_fatigue': False,
'big_power': False,
'generators': False,
'other_companies_power': False,
'nonstandard_equipment_power': False,
'multiple_electrical_environments': False,
'noise_monitoring': False,
'known_venue': False,
'safe_loading': False,
'safe_storage': False,
'area_outside_of_control': False,
'barrier_required': False,
'nonstandard_emergency_procedure': False,
'special_structures': False,
'suspended_structures': False,
}
inverted_fields = {key: value for (key, value) in expected_values.items() if not value}.keys()
def clean(self):
# Check for idiots
if not self.outside and self.generators:
raise forms.ValidationError("Engage brain, please. <strong>No generators indoors!(!)</strong>")
class Meta:
ordering = ['event']
permissions = [
('review_riskassessment', 'Can review Risk Assessments')
]
@property
def event_size(self):
# Confirm event size. Check all except generators, since generators entails outside
if self.outside or self.other_companies_power or self.nonstandard_equipment_power or self.multiple_electrical_environments:
return self.LARGE[0]
elif self.big_power:
return self.MEDIUM[0]
else:
return self.SMALL[0]
@property
def activity_feed_string(self):
return str(self.event)
def get_absolute_url(self):
return reverse_lazy('ra_detail', kwargs={'pk': self.pk})
def __str__(self):
return "%i - %s" % (self.pk, self.event)
@reversion.register(follow=['vehicles', 'crew'])
class EventChecklist(models.Model, RevisionMixin):
event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
# General
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists',
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?")
venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
date = models.DateField()
# Safety Checks
safe_parking = models.BooleanField(blank=True, null=True, help_text="Vehicles parked safely?<br><small>(does not obstruct venue access)</small>")
safe_packing = models.BooleanField(blank=True, null=True, help_text="Equipment packed away safely?<br><small>(including flightcases)</small>")
exits = models.BooleanField(blank=True, null=True, help_text="Emergency exits clear?")
trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?")
warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>")
ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?")
hs_location = models.CharField(blank=True, null=True, max_length=255, help_text="Location of Safety Bag/Box")
extinguishers_location = models.CharField(blank=True, null=True, max_length=255, help_text="Location of fire extinguishers")
# Small Electrical Checks
rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested?<br><small>(using socket tester)</small>")
# Shared electrical checks
earthing = models.BooleanField(blank=True, null=True, help_text="Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>")
pat = models.BooleanField(blank=True, null=True, help_text="All equipment in PAT period?")
# Medium Electrical Checks
source_rcd = models.BooleanField(blank=True, null=True, help_text="Source RCD protected?<br><small>(if cable is more than 3m long) </small>")
labelling = models.BooleanField(blank=True, null=True, help_text="Appropriate and clear labelling on distribution and cabling?")
# First Distro
fd_voltage_l1 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L1-N", help_text="L1 - N")
fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N")
fd_voltage_l3 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N")
fd_phase_rotation = models.BooleanField(blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation<br><small>(if required)</small>")
fd_earth_fault = models.IntegerField(blank=True, null=True, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
# Worst case points
w1_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
w1_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
w2_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
w2_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
w3_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
w3_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</small>")
public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
reviewed_at = models.DateTimeField(null=True)
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
verbose_name="Reviewer", on_delete=models.CASCADE)
inverted_fields = []
class Meta:
ordering = ['event']
permissions = [
('review_eventchecklist', 'Can review Event Checklists')
]
@property
def activity_feed_string(self):
return str(self.event)
def get_absolute_url(self):
return reverse_lazy('ec_detail', kwargs={'pk': self.pk})
def __str__(self):
return "%i - %s" % (self.pk, self.event)
@reversion.register
class EventChecklistVehicle(models.Model, RevisionMixin):
checklist = models.ForeignKey('EventChecklist', related_name='vehicles', blank=True, on_delete=models.CASCADE)
vehicle = models.CharField(max_length=255)
driver = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='vehicles', on_delete=models.CASCADE)
reversion_hide = True
def __str__(self):
return "{} driven by {}".format(self.vehicle, str(self.driver))
@reversion.register
class EventChecklistCrew(models.Model, RevisionMixin):
checklist = models.ForeignKey('EventChecklist', related_name='crew', blank=True, on_delete=models.CASCADE)
crewmember = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='crewed', on_delete=models.CASCADE)
role = models.CharField(max_length=255)
start = models.DateTimeField()
end = models.DateTimeField()
reversion_hide = True
def clean(self):
if self.start > self.end:
raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.')
def __str__(self):
return "{} ({})".format(str(self.crewmember), self.role)

View File

@@ -1,34 +1,33 @@
from io import BytesIO import copy
import urllib.request
import urllib.error
import urllib.parse
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.views import generic
from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.template.loader import get_template
from django.conf import settings
from django.urls import reverse
from django.core import signing
from django.http import HttpResponse
from django.core.exceptions import SuspiciousOperation
from django.db.models import Q
from django.contrib import messages
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from z3c.rml import rml2pdf
from PyPDF2 import PdfFileMerger, PdfFileReader
import simplejson
import premailer
from RIGS import models, forms
from PyRIGS import decorators
import datetime import datetime
import re import re
import copy import urllib.error
import urllib.parse
import urllib.request
from io import BytesIO
import premailer
import simplejson
from PyPDF2 import PdfFileMerger, PdfFileReader
from django.conf import settings
from django.contrib import messages
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core import signing
from django.core.exceptions import SuspiciousOperation
from django.core.mail import EmailMultiAlternatives
from django.db.models import Q
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from django.urls import reverse
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import generic
from z3c.rml import rml2pdf
from PyRIGS import decorators
from RIGS import models, forms
__author__ = 'ghost' __author__ = 'ghost'
@@ -42,6 +41,7 @@ class RigboardIndex(generic.TemplateView):
# call out method to get current events # call out method to get current events
context['events'] = models.Event.objects.current_events() context['events'] = models.Event.objects.current_events()
context['page_title'] = "Rigboard"
return context return context
@@ -82,26 +82,6 @@ class EventEmbed(EventDetail):
template_name = 'event_embed.html' template_name = 'event_embed.html'
class EventRA(generic.base.RedirectView):
permanent = False
def get_redirect_url(self, *args, **kwargs):
event = get_object_or_404(models.Event, pk=kwargs['pk'])
if event.risk_assessment_edit_url:
return event.risk_assessment_edit_url
params = {
'entry.708610078': f'N{event.pk:05}',
'entry.905899507': event.name,
'entry.139491562': event.venue.name if event.venue else '',
'entry.1689826056': event.start_date.strftime('%Y-%m-%d') + (
(' - ' + event.end_date.strftime('%Y-%m-%d')) if event.end_date else ''),
'entry.902421165': event.mic.name if event.mic else ''
}
return settings.RISK_ASSESSMENT_URL + "?" + urllib.parse.urlencode(params)
class EventCreate(generic.CreateView): class EventCreate(generic.CreateView):
model = models.Event model = models.Event
form_class = forms.EventForm form_class = forms.EventForm
@@ -109,11 +89,12 @@ class EventCreate(generic.CreateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventCreate, self).get_context_data(**kwargs) context = super(EventCreate, self).get_context_data(**kwargs)
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()
form = context['form'] form = context['form']
if re.search(r'"-\d+"', form['items_json'].value()): if hasattr(form, 'items_json') and re.search(r'"-\d+"', form['items_json'].value()):
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.") messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
# Get some other objects to include in the form. Used when there are errors but also nice and quick. # Get some other objects to include in the form. Used when there are errors but also nice and quick.
@@ -134,6 +115,7 @@ class EventUpdate(generic.UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventUpdate, self).get_context_data(**kwargs) context = super(EventUpdate, self).get_context_data(**kwargs)
context['page_title'] = "Event {}".format(self.object.display_id)
context['edit'] = True context['edit'] = True
form = context['form'] form = context['form']
@@ -147,7 +129,7 @@ class EventUpdate(generic.UpdateView):
return context return context
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
if not hasattr(context, 'duplicate'): if hasattr(context, 'duplicate') and not context['duplicate']:
# If this event has already been emailed to a client, show a warning # If this event has already been emailed to a client, show a warning
if self.object.auth_request_at is not None: if self.object.auth_request_at is not None:
messages.info(self.request, messages.info(self.request,
@@ -155,7 +137,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 client, any changes to 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(EventUpdate, self).render_to_response(context, **response_kwargs)
def get_success_url(self): def get_success_url(self):
@@ -168,6 +150,7 @@ class EventDuplicate(EventUpdate):
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
new.status = new.PROVISIONAL # Return status to provisional
# 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:
@@ -188,6 +171,7 @@ class EventDuplicate(EventUpdate):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventDuplicate, self).get_context_data(**kwargs) context = super(EventDuplicate, self).get_context_data(**kwargs)
context['page_title'] = "Duplicate of Event {}".format(self.object.display_id)
context["duplicate"] = True context["duplicate"] = True
return context return context
@@ -209,6 +193,7 @@ class EventPrint(generic.View):
}, },
'quote': True, 'quote': True,
'current_user': request.user, 'current_user': request.user,
'filename': 'Event {} {} {}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
} }
rml = template.render(context) rml = template.render(context)
@@ -223,10 +208,7 @@ class EventPrint(generic.View):
merger.write(merged) merger.write(merged)
response = HttpResponse(content_type='application/pdf') response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
response['Content-Disposition'] = "filename=N%05d | %s.pdf" % (object.pk, escapedEventName)
response.write(merged.getvalue()) response.write(merged.getvalue())
return response return response
@@ -242,6 +224,8 @@ class EventArchive(generic.ListView):
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'))
context['statuses'] = models.Event.EVENT_STATUS_CHOICES
context['page_title'] = 'Event Archive'
return context return context
def get_queryset(self): def get_queryset(self):
@@ -281,6 +265,11 @@ class EventArchive(generic.ListView):
filter &= qfilter filter &= qfilter
status = self.request.GET.getlist('status', "")
if len(status) > 0:
filter &= Q(status__in=status)
qs = self.model.objects.filter(filter).order_by('-start_date') qs = self.model.objects.filter(filter).order_by('-start_date')
# Preselect related for efficiency # Preselect related for efficiency
@@ -302,7 +291,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)) 'You will also receive email confirmation to %s.' % self.object.email)
return self.render_to_response(self.get_context_data()) return self.render_to_response(self.get_context_data())
@property @property
@@ -318,8 +307,10 @@ class EventAuthorise(generic.UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventAuthorise, self).get_context_data(**kwargs) context = super(EventAuthorise, self).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)
if self.event.dry_hire:
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
return context return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@@ -382,7 +373,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
email = form.cleaned_data['email'] email = form.cleaned_data['email']
event = self.object event = self.object
event.auth_request_by = self.request.user event.auth_request_by = self.request.user
event.auth_request_at = datetime.datetime.now() event.auth_request_at = timezone.now()
event.auth_request_to = email event.auth_request_to = email
event.save() event.save()
@@ -437,27 +428,3 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
}) })
context['to_name'] = self.request.GET.get('to_name', None) context['to_name'] = self.request.GET.get('to_name', None)
return context return context
@method_decorator(csrf_exempt, name='dispatch')
class LogRiskAssessment(generic.View):
http_method_names = ["post"]
def post(self, request, **kwargs):
data = request.POST
shared_secret = data.get("secret")
edit_url = data.get("editUrl")
rig_number = data.get("rigNum")
if shared_secret is None or edit_url is None or rig_number is None:
return HttpResponse(status=422)
if shared_secret != settings.RISK_ASSESSMENT_SECRET:
return HttpResponse(status=403)
rig_number = int(re.sub("[^0-9]", "", rig_number))
event = get_object_or_404(models.Event, pk=rig_number)
event.risk_assessment_edit_url = edit_url
event.save()
return HttpResponse(status=200)

View File

@@ -1,20 +1,21 @@
import datetime
import re import re
import urllib.request
import urllib.error import urllib.error
import urllib.parse import urllib.parse
import urllib.request
from io import BytesIO from io import BytesIO
from django.db.models.signals import post_save
from PyPDF2 import PdfFileReader, PdfFileMerger from PyPDF2 import PdfFileReader, PdfFileMerger
from django.conf import settings from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.cache import cache
from django.core.mail import EmailMessage, EmailMultiAlternatives from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.db.models.signals import post_save
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from registration.signals import user_activated
from premailer import Premailer from premailer import Premailer
from registration.signals import user_activated
from reversion import revisions as reversion
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
from RIGS import models from RIGS import models
@@ -138,3 +139,11 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
user_activated.connect(send_admin_awaiting_approval_email) user_activated.connect(send_admin_awaiting_approval_email)
def update_cache(sender, instance, created, **kwargs):
cache.clear()
for model in reversion.get_registered_models():
post_save.connect(update_cache, sender=model)

View File

@@ -1,28 +0,0 @@
/*!
* Ajax Bootstrap Select
*
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
*
* @version 1.4.5
* @author Adam Heim - https://github.com/truckingsim
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
* @copyright 2019 Adam Heim
* @license Released under the MIT license.
*
* Contributors:
* Mark Carver - https://github.com/markcarver
*
* Last build: 2019-04-23 12:18:56 PM EDT
*/
.bootstrap-select .status {
background: #f0f0f0;
clear: both;
color: #999;
font-size: 11px;
font-style: italic;
font-weight: 500;
line-height: 1;
margin-bottom: -5px;
padding: 10px 20px; }
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7OztFQWVFO0FBQ0Y7RUFDRSxtQkFBbUI7RUFDbkIsV0FBVztFQUNYLFdBQVc7RUFDWCxlQUFlO0VBQ2Ysa0JBQWtCO0VBQ2xCLGdCQUFnQjtFQUNoQixjQUFjO0VBQ2QsbUJBQW1CO0VBQ25CLGtCQUFrQixFQUFBIiwiZmlsZSI6ImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyIvKiFcbiAqIEFqYXggQm9vdHN0cmFwIFNlbGVjdFxuICpcbiAqIEV4dGVuZHMgZXhpc3RpbmcgW0Jvb3RzdHJhcCBTZWxlY3RdIGltcGxlbWVudGF0aW9ucyBieSBhZGRpbmcgdGhlIGFiaWxpdHkgdG8gc2VhcmNoIHZpYSBBSkFYIHJlcXVlc3RzIGFzIHlvdSB0eXBlLiBPcmlnaW5hbGx5IGZvciBDUk9TQ09OLlxuICpcbiAqIEB2ZXJzaW9uIDEuNC41XG4gKiBAYXV0aG9yIEFkYW0gSGVpbSAtIGh0dHBzOi8vZ2l0aHViLmNvbS90cnVja2luZ3NpbVxuICogQGxpbmsgaHR0cHM6Ly9naXRodWIuY29tL3RydWNraW5nc2ltL0FqYXgtQm9vdHN0cmFwLVNlbGVjdFxuICogQGNvcHlyaWdodCAyMDE5IEFkYW0gSGVpbVxuICogQGxpY2Vuc2UgUmVsZWFzZWQgdW5kZXIgdGhlIE1JVCBsaWNlbnNlLlxuICpcbiAqIENvbnRyaWJ1dG9yczpcbiAqICAgTWFyayBDYXJ2ZXIgLSBodHRwczovL2dpdGh1Yi5jb20vbWFya2NhcnZlclxuICpcbiAqIExhc3QgYnVpbGQ6IDIwMTktMDQtMjMgMTI6MTg6NTYgUE0gRURUXG4gKi9cbi5ib290c3RyYXAtc2VsZWN0IC5zdGF0dXMge1xuICBiYWNrZ3JvdW5kOiAjZjBmMGYwO1xuICBjbGVhcjogYm90aDtcbiAgY29sb3I6ICM5OTk7XG4gIGZvbnQtc2l6ZTogMTFweDtcbiAgZm9udC1zdHlsZTogaXRhbGljO1xuICBmb250LXdlaWdodDogNTAwO1xuICBsaW5lLWhlaWdodDogMTtcbiAgbWFyZ2luLWJvdHRvbTogLTVweDtcbiAgcGFkZGluZzogMTBweCAyMHB4O1xufVxuIl19 */

View File

@@ -1,28 +0,0 @@
/*!
* Ajax Bootstrap Select
*
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
*
* @version 1.4.5
* @author Adam Heim - https://github.com/truckingsim
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
* @copyright 2019 Adam Heim
* @license Released under the MIT license.
*
* Contributors:
* Mark Carver - https://github.com/markcarver
*
* Last build: 2019-04-23 12:18:56 PM EDT
*/
.bootstrap-select .status {
background: #f0f0f0;
clear: both;
color: #999;
font-size: 11px;
font-style: italic;
font-weight: 500;
line-height: 1;
margin-bottom: -5px;
padding: 10px 20px; }
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5taW4uY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7RUFlRTtBQUFDO0VBQTBCLG1CQUFrQjtFQUFDLFdBQVU7RUFBQyxXQUFVO0VBQUMsZUFBYztFQUFDLGtCQUFpQjtFQUFDLGdCQUFlO0VBQUMsY0FBYTtFQUFDLG1CQUFrQjtFQUFDLGtCQUFpQixFQUFBIiwiZmlsZSI6ImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5taW4uY3NzIiwic291cmNlc0NvbnRlbnQiOlsiLyohXG4gKiBBamF4IEJvb3RzdHJhcCBTZWxlY3RcbiAqXG4gKiBFeHRlbmRzIGV4aXN0aW5nIFtCb290c3RyYXAgU2VsZWN0XSBpbXBsZW1lbnRhdGlvbnMgYnkgYWRkaW5nIHRoZSBhYmlsaXR5IHRvIHNlYXJjaCB2aWEgQUpBWCByZXF1ZXN0cyBhcyB5b3UgdHlwZS4gT3JpZ2luYWxseSBmb3IgQ1JPU0NPTi5cbiAqXG4gKiBAdmVyc2lvbiAxLjQuNVxuICogQGF1dGhvciBBZGFtIEhlaW0gLSBodHRwczovL2dpdGh1Yi5jb20vdHJ1Y2tpbmdzaW1cbiAqIEBsaW5rIGh0dHBzOi8vZ2l0aHViLmNvbS90cnVja2luZ3NpbS9BamF4LUJvb3RzdHJhcC1TZWxlY3RcbiAqIEBjb3B5cmlnaHQgMjAxOSBBZGFtIEhlaW1cbiAqIEBsaWNlbnNlIFJlbGVhc2VkIHVuZGVyIHRoZSBNSVQgbGljZW5zZS5cbiAqXG4gKiBDb250cmlidXRvcnM6XG4gKiAgIE1hcmsgQ2FydmVyIC0gaHR0cHM6Ly9naXRodWIuY29tL21hcmtjYXJ2ZXJcbiAqXG4gKiBMYXN0IGJ1aWxkOiAyMDE5LTA0LTIzIDEyOjE4OjU2IFBNIEVEVFxuICovLmJvb3RzdHJhcC1zZWxlY3QgLnN0YXR1c3tiYWNrZ3JvdW5kOiNmMGYwZjA7Y2xlYXI6Ym90aDtjb2xvcjojOTk5O2ZvbnQtc2l6ZToxMXB4O2ZvbnQtc3R5bGU6aXRhbGljO2ZvbnQtd2VpZ2h0OjUwMDtsaW5lLWhlaWdodDoxO21hcmdpbi1ib3R0b206LTVweDtwYWRkaW5nOjEwcHggMjBweH0iXX0= */

View File

@@ -1,23 +0,0 @@
.autocomplete {
background: white;
z-index: 1000;
font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
overflow: auto;
box-sizing: border-box;
border: 1px solid rgba(50, 50, 50, 0.6); }
.autocomplete * {
font: inherit; }
.autocomplete > div {
padding: 0 4px; }
.autocomplete .group {
background: #eee; }
.autocomplete > div:hover:not(.group),
.autocomplete > div.selected {
background: #81ca91;
cursor: pointer; }
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImF1dG9jb21wbGV0ZS5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0E7RUFDSSxpQkFBaUI7RUFDakIsYUFBYTtFQUNiLDRHQUE0RztFQUM1RyxjQUFjO0VBQ2Qsc0JBQXNCO0VBQ3RCLHVDQUF1QyxFQUFBOztBQUczQztFQUNJLGFBQWEsRUFBQTs7QUFHakI7RUFDSSxjQUFjLEVBQUE7O0FBR2xCO0VBQ0ksZ0JBQWdCLEVBQUE7O0FBR3BCOztFQUVJLG1CQUFtQjtFQUNuQixlQUFlLEVBQUEiLCJmaWxlIjoiYXV0b2NvbXBsZXRlLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIlxyXG4uYXV0b2NvbXBsZXRlIHtcclxuICAgIGJhY2tncm91bmQ6IHdoaXRlO1xyXG4gICAgei1pbmRleDogMTAwMDtcclxuICAgIGZvbnQ6IDE0cHgvMjJweCBcIi1hcHBsZS1zeXN0ZW1cIiwgQmxpbmtNYWNTeXN0ZW1Gb250LCBcIlNlZ29lIFVJXCIsIFJvYm90bywgXCJIZWx2ZXRpY2EgTmV1ZVwiLCBBcmlhbCwgc2Fucy1zZXJpZjtcclxuICAgIG92ZXJmbG93OiBhdXRvO1xyXG4gICAgYm94LXNpemluZzogYm9yZGVyLWJveDtcclxuICAgIGJvcmRlcjogMXB4IHNvbGlkIHJnYmEoNTAsIDUwLCA1MCwgMC42KTtcclxufVxyXG5cclxuLmF1dG9jb21wbGV0ZSAqIHtcclxuICAgIGZvbnQ6IGluaGVyaXQ7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgPiBkaXYge1xyXG4gICAgcGFkZGluZzogMCA0cHg7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgLmdyb3VwIHtcclxuICAgIGJhY2tncm91bmQ6ICNlZWU7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgPiBkaXY6aG92ZXI6bm90KC5ncm91cCksXHJcbi5hdXRvY29tcGxldGUgPiBkaXYuc2VsZWN0ZWQge1xyXG4gICAgYmFja2dyb3VuZDogIzgxY2E5MTtcclxuICAgIGN1cnNvcjogcG9pbnRlcjtcclxufVxyXG5cclxuIl19 */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,39 +0,0 @@
body {
margin: 0px; }
.main-table {
width: 100%;
border-collapse: collapse; }
.client-header {
background-image: url("https://www.nottinghamtec.co.uk/imgs/wof2014-1.jpg");
background-color: #222;
background-repeat: no-repeat;
background-position: center;
width: 100%;
margin-bottom: 28px; }
.client-header .logos {
width: 100%;
max-width: 640px; }
.client-header img {
height: 110px; }
.content-container {
width: 100%; }
.content-container .content {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
width: 100%;
max-width: 600px;
padding: 10px;
text-align: left; }
.content-container .content .button-container {
width: 100%; }
.content-container .content .button-container .button {
padding: 6px 12px;
background-color: #357ebf;
border-radius: 4px; }
.content-container .content .button-container .button a {
color: #fff;
text-decoration: none; }
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImVtYWlsLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUE7RUFDRSxXQUFXLEVBQUE7O0FBR2I7RUFDRSxXQUFXO0VBQ1gseUJBQXlCLEVBQUE7O0FBSTNCO0VBQ0UsMkVBQTJFO0VBQzNFLHNCQUFzQjtFQUN0Qiw0QkFBNEI7RUFDNUIsMkJBQTJCO0VBRTNCLFdBQVc7RUFFWCxtQkFBbUIsRUFBQTtFQVJyQjtJQVdJLFdBQVc7SUFDWCxnQkFBZ0IsRUFBQTtFQVpwQjtJQWdCSSxhQUFhLEVBQUE7O0FBSWpCO0VBQ0UsV0FBVyxFQUFBO0VBRGI7SUFJSSx3RUFBd0U7SUFFeEUsV0FBVztJQUNYLGdCQUFnQjtJQUNoQixhQUFhO0lBQ2IsZ0JBQWdCLEVBQUE7SUFUcEI7TUFZTSxXQUFXLEVBQUE7TUFaakI7UUFlUSxpQkFBaUI7UUFDakIseUJBaERjO1FBaURkLGtCQUFrQixFQUFBO1FBakIxQjtVQW9CVSxXQUFXO1VBQ1gscUJBQXFCLEVBQUEiLCJmaWxlIjoiZW1haWwuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiJGJ1dHRvbl9jb2xvcjogIzM1N2ViZjtcblxuYm9keXtcbiAgbWFyZ2luOiAwcHg7XG59XG5cbi5tYWluLXRhYmxle1xuICB3aWR0aDogMTAwJTtcbiAgYm9yZGVyLWNvbGxhcHNlOiBjb2xsYXBzZTtcblxufVxuXG4uY2xpZW50LWhlYWRlciB7XG4gIGJhY2tncm91bmQtaW1hZ2U6IHVybChcImh0dHBzOi8vd3d3Lm5vdHRpbmdoYW10ZWMuY28udWsvaW1ncy93b2YyMDE0LTEuanBnXCIpO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMjIyO1xuICBiYWNrZ3JvdW5kLXJlcGVhdDogbm8tcmVwZWF0O1xuICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBjZW50ZXI7XG5cbiAgd2lkdGg6IDEwMCU7XG5cbiAgbWFyZ2luLWJvdHRvbTogMjhweDtcblxuICAubG9nb3N7XG4gICAgd2lkdGg6IDEwMCU7XG4gICAgbWF4LXdpZHRoOiA2NDBweDtcbiAgfVxuXG4gIGltZyB7XG4gICAgaGVpZ2h0OiAxMTBweDtcbiAgfVxufVxuXG4uY29udGVudC1jb250YWluZXJ7XG4gIHdpZHRoOiAxMDAlO1xuXG4gIC5jb250ZW50IHtcbiAgICBmb250LWZhbWlseTogXCJPcGVuIFNhbnNcIiwgXCJIZWx2ZXRpY2EgTmV1ZVwiLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmO1xuXG4gICAgd2lkdGg6IDEwMCU7XG4gICAgbWF4LXdpZHRoOiA2MDBweDtcbiAgICBwYWRkaW5nOiAxMHB4O1xuICAgIHRleHQtYWxpZ246IGxlZnQ7XG5cbiAgICAuYnV0dG9uLWNvbnRhaW5lcntcbiAgICAgIHdpZHRoOiAxMDAlO1xuXG4gICAgICAuYnV0dG9uIHtcbiAgICAgICAgcGFkZGluZzogNnB4IDEycHg7XG4gICAgICAgIGJhY2tncm91bmQtY29sb3I6ICRidXR0b25fY29sb3I7XG4gICAgICAgIGJvcmRlci1yYWRpdXM6IDRweDtcblxuICAgICAgICBhIHtcbiAgICAgICAgICBjb2xvcjogI2ZmZjtcbiAgICAgICAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7XG4gICAgICAgIH1cblxuICAgICAgfVxuXG4gICAgfVxuXG4gIH1cbn1cblxuIl19 */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJpZS5jc3MiLCJzb3VyY2VzQ29udGVudCI6W119 */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +0,0 @@
/*!
* Bootstrap alert.js v4.4.1 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("jquery"),require("./util.js")):"function"==typeof define&&define.amd?define(["jquery","./util.js"],t):(e=e||self).Alert=t(e.jQuery,e.Util)}(this,(function(e,t){"use strict";function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}e=e&&e.hasOwnProperty("default")?e.default:e,t=t&&t.hasOwnProperty("default")?t.default:t;var r=e.fn.alert,o={CLOSE:"close.bs.alert",CLOSED:"closed.bs.alert",CLICK_DATA_API:"click.bs.alert.data-api"},i="alert",l="fade",s="show",a=function(){function r(e){this._element=e}var a,u,f,c=r.prototype;return c.close=function(e){var t=this._element;e&&(t=this._getRootElement(e)),this._triggerCloseEvent(t).isDefaultPrevented()||this._removeElement(t)},c.dispose=function(){e.removeData(this._element,"bs.alert"),this._element=null},c._getRootElement=function(n){var r=t.getSelectorFromElement(n),o=!1;return r&&(o=document.querySelector(r)),o||(o=e(n).closest("."+i)[0]),o},c._triggerCloseEvent=function(t){var n=e.Event(o.CLOSE);return e(t).trigger(n),n},c._removeElement=function(n){var r=this;if(e(n).removeClass(s),e(n).hasClass(l)){var o=t.getTransitionDurationFromElement(n);e(n).one(t.TRANSITION_END,(function(e){return r._destroyElement(n,e)})).emulateTransitionEnd(o)}else this._destroyElement(n)},c._destroyElement=function(t){e(t).detach().trigger(o.CLOSED).remove()},r._jQueryInterface=function(t){return this.each((function(){var n=e(this),o=n.data("bs.alert");o||(o=new r(this),n.data("bs.alert",o)),"close"===t&&o[t](this)}))},r._handleDismiss=function(e){return function(t){t&&t.preventDefault(),e.close(this)}},a=r,f=[{key:"VERSION",get:function(){return"4.4.1"}}],(u=null)&&n(a.prototype,u),f&&n(a,f),r}();return e(document).on(o.CLICK_DATA_API,'[data-dismiss="alert"]',a._handleDismiss(new a)),e.fn.alert=a._jQueryInterface,e.fn.alert.Constructor=a,e.fn.alert.noConflict=function(){return e.fn.alert=r,a._jQueryInterface},a}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
$(document).ready((function(){function e(e){targetObject=$("#"+e.attr("id")+"-update"),update_url=$("option:selected",e).data("update_url"),""==update_url?targetObject.attr("disabled",!0):(targetObject.attr("href",update_url),targetObject.attr("disabled",!1))}clearSelectionLabel="(no selection)",$(".selectpicker").each((function(){var t={ajax:{url:$(this).data("sourceurl"),type:"GET",dataType:"json",data:{term:"{{{q}}}"}},locale:{emptyTitle:""},clearOnEmpty:!1,preprocessData:function(e){var t,a=e.length,l=[];if(l.push({text:clearSelectionLabel,value:"",data:{update_url:"",subtext:""}}),a)for(t=0;t<a;t++)l.push($.extend(!0,e[t],{text:e[t].label,value:e[t].pk,data:{update_url:e[t].update,subtext:""}}));return l}};$(this).prepend($("<option></option>").attr("value","").text(clearSelectionLabel).data("update_url","")),$(this).selectpicker().ajaxSelectPicker(t),$(this).change((function(){e($(this))})),e($(this))})),$("#modal").on("hide.bs.modal",(function(e){null!=modaltarget&&""!=modalobject&&function(e,t,a,l){e.find("option").remove(),e.append($("<option></option>").attr("value",t).text(a).data("update_url",l)),e.selectpicker("render"),e.selectpicker("refresh"),e.selectpicker("val",t),e.change()}($(modaltarget),modalobject[0].pk,modalobject[0].fields.name,modalobject[0].update_url)}))}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
function setupItemTable(t){objectitems=JSON.parse(t),$.each(objectitems,(function(t,e){objectitems[t]=JSON.parse(e)})),newitem=-1}function nl2br(t,e){return(t+"").replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g,"$1"+(e||void 0===e?"<br />":"<br>")+"$2")}function escapeHtml(t){return $("<div/>").text(t).html()}function updatePrices(){var t=0;for(var e in objectitems){var i=objectitems[e].fields,a=i.cost*i.quantity;$("#item-"+e+" .sub-total").html(parseFloat(a).toFixed(2)).data("subtotal",a),t+=Number(a)}$("#sumtotal").text(parseFloat(t).toFixed(2));var o=t*Number($("#vat-rate").data("rate"));$("#vat").text(parseFloat(o).toFixed(2)),$("#total").text(parseFloat(t+o).toFixed(2))}$("#item-table").on("click",".item-delete",(function(){delete objectitems[$(this).data("pk")],$("#item-"+$(this).data("pk")).remove(),updatePrices()})),$("#item-table").on("click",".item-add",(function(){$("#item-form").data("pk",newitem),$("#item_name").val(""),$("#item_description").val(""),$("#item_quantity").val(""),$("#item_cost").val(""),$($(this).data("target")).modal("show")})),$("#item-table").on("click",".item-edit",(function(){var t=$(this).data("pk");$("#item-form").data("pk",t);var e=objectitems[t].fields;$("#item_name").val(e.name),$("#item_description").val(e.description),$("#item_quantity").val(e.quantity),$("#item_cost").val(e.cost),$($(this).data("target")).modal("show")})),$("body").on("submit","#item-form",(function(t){t.preventDefault();var e,i=$(this).data("pk");if($("#itemModal").modal("hide"),i==newitem--){(e=new Object).name=$("#item_name").val(),e.description=$("#item_description").val(),e.cost=$("#item_cost").val(),e.quantity=$("#item_quantity").val();var a=0;for(item in objectitems)a++;e.order=a,objectitems[i]=new Object,objectitems[i].fields=e,$("#new-item-row").clone().attr("id","item-"+i).data("pk",i).appendTo("#item-table-body"),$("#item-"+i+" .item-delete, #item-"+i+" .item-edit").data("pk",i)}else(e=objectitems[i].fields).name=$("#item_name").val(),e.description=$("#item_description").val(),e.cost=$("#item_cost").val(),e.quantity=$("#item_quantity").val(),objectitems[i].fields=e;$row=$("#item-"+i),$row.find(".name").html(escapeHtml(e.name)),$row.find(".description").html(nl2br(escapeHtml(e.description))),$row.find(".cost").html(parseFloat(e.cost).toFixed(2)),$row.find(".quantity").html(e.quantity),updatePrices()})),$("body").on("submit",".itemised_form",(function(t){$("#id_items_json").val(JSON.stringify(objectitems))}));var fixHelper=function(t,e){return e.children().each((function(){$(this).width($(this).width())})),e};$("#item-table tbody").sortable({helper:fixHelper,update:function(t,e){info=$(this).sortable("toArray"),itemorder=new Array,$.each(info,(function(t,e){pk=$("#"+e).data("pk"),objectitems[pk].fields.order=t}))}});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
var Konami=function(t){var e={addEvent:function(t,e,n,o){t.addEventListener?t.addEventListener(e,n,!1):t.attachEvent&&(t["e"+e+n]=n,t[e+n]=function(){t["e"+e+n](window.event,o)},t.attachEvent("on"+e,t[e+n]))},removeEvent:function(t,e,n){t.removeEventListener?t.removeEventListener(e,n):t.attachEvent&&t.detachEvent(e)},input:"",pattern:"38384040373937396665",keydownHandler:function(t,n){if(n&&(e=n),e.input+=t?t.keyCode:event.keyCode,e.input.length>e.pattern.length&&(e.input=e.input.substr(e.input.length-e.pattern.length)),e.input===e.pattern)return e.code(e._currentLink),e.input="",t.preventDefault(),!1},load:function(t){this._currentLink=t,this.addEvent(document,"keydown",this.keydownHandler,this),this.iphone.load(t)},unload:function(){this.removeEvent(document,"keydown",this.keydownHandler),this.iphone.unload()},code:function(t){window.location=t},iphone:{start_x:0,start_y:0,stop_x:0,stop_y:0,tap:!1,capture:!1,orig_keys:"",keys:["UP","UP","DOWN","DOWN","LEFT","RIGHT","LEFT","RIGHT","TAP","TAP"],input:[],code:function(t){e.code(t)},touchmoveHandler:function(t){if(1===t.touches.length&&!0===e.iphone.capture){var n=t.touches[0];e.iphone.stop_x=n.pageX,e.iphone.stop_y=n.pageY,e.iphone.tap=!1,e.iphone.capture=!1,e.iphone.check_direction()}},touchendHandler:function(){if(e.iphone.input.push(e.iphone.check_direction()),e.iphone.input.length>e.iphone.keys.length&&e.iphone.input.shift(),e.iphone.input.length===e.iphone.keys.length){for(var t=!0,n=0;n<e.iphone.keys.length;n++)e.iphone.input[n]!==e.iphone.keys[n]&&(t=!1);t&&e.iphone.code(e._currentLink)}},touchstartHandler:function(t){e.iphone.start_x=t.changedTouches[0].pageX,e.iphone.start_y=t.changedTouches[0].pageY,e.iphone.tap=!0,e.iphone.capture=!0},load:function(t){this.orig_keys=this.keys,e.addEvent(document,"touchmove",this.touchmoveHandler),e.addEvent(document,"touchend",this.touchendHandler,!1),e.addEvent(document,"touchstart",this.touchstartHandler)},unload:function(){e.removeEvent(document,"touchmove",this.touchmoveHandler),e.removeEvent(document,"touchend",this.touchendHandler),e.removeEvent(document,"touchstart",this.touchstartHandler)},check_direction:function(){return x_magnitude=Math.abs(this.start_x-this.stop_x),y_magnitude=Math.abs(this.start_y-this.stop_y),x=this.start_x-this.stop_x<0?"RIGHT":"LEFT",y=this.start_y-this.stop_y<0?"DOWN":"UP",result=x_magnitude>y_magnitude?x:y,result=!0===this.tap?"TAP":result,result}}};return"string"==typeof t&&e.load(t),"function"==typeof t&&(e.code=t,e.load()),e};"undefined"!=typeof module&&void 0!==module.exports?module.exports=Konami:"function"==typeof define&&define.amd?define([],(function(){return Konami})):window.Konami=Konami;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +0,0 @@
/*!
* Bootstrap popover.js v4.4.1 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("jquery"),require("./tooltip.js")):"function"==typeof define&&define.amd?define(["jquery","./tooltip.js"],t):(e=e||self).Popover=t(e.jQuery,e.Tooltip)}(this,(function(e,t){"use strict";function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?o(Object(n),!0).forEach((function(t){r(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):o(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}e=e&&e.hasOwnProperty("default")?e.default:e,t=t&&t.hasOwnProperty("default")?t.default:t;var u="popover",s=".bs.popover",c=e.fn[u],p=new RegExp("(^|\\s)bs-popover\\S+","g"),f=i({},t.Default,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'}),a=i({},t.DefaultType,{content:"(string|element|function)"}),l="fade",h="show",y=".popover-header",d=".popover-body",v={HIDE:"hide"+s,HIDDEN:"hidden"+s,SHOW:"show"+s,SHOWN:"shown"+s,INSERTED:"inserted"+s,CLICK:"click"+s,FOCUSIN:"focusin"+s,FOCUSOUT:"focusout"+s,MOUSEENTER:"mouseenter"+s,MOUSELEAVE:"mouseleave"+s},g=function(t){var r,o;function i(){return t.apply(this,arguments)||this}o=t,(r=i).prototype=Object.create(o.prototype),r.prototype.constructor=r,r.__proto__=o;var c,g,b,O=i.prototype;return O.isWithContent=function(){return this.getTitle()||this._getContent()},O.addAttachmentClass=function(t){e(this.getTipElement()).addClass("bs-popover-"+t)},O.getTipElement=function(){return this.tip=this.tip||e(this.config.template)[0],this.tip},O.setContent=function(){var t=e(this.getTipElement());this.setElementContent(t.find(y),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(t.find(d),n),t.removeClass(l+" "+h)},O._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},O._cleanTipClass=function(){var t=e(this.getTipElement()),n=t.attr("class").match(p);null!==n&&n.length>0&&t.removeClass(n.join(""))},i._jQueryInterface=function(t){return this.each((function(){var n=e(this).data("bs.popover"),r="object"==typeof t?t:null;if((n||!/dispose|hide/.test(t))&&(n||(n=new i(this,r),e(this).data("bs.popover",n)),"string"==typeof t)){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},c=i,b=[{key:"VERSION",get:function(){return"4.4.1"}},{key:"Default",get:function(){return f}},{key:"NAME",get:function(){return u}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return v}},{key:"EVENT_KEY",get:function(){return s}},{key:"DefaultType",get:function(){return a}}],(g=null)&&n(c.prototype,g),b&&n(c,b),i}(t);return e.fn[u]=g._jQueryInterface,e.fn[u].Constructor=g,e.fn[u].noConflict=function(){return e.fn[u]=c,g._jQueryInterface},g}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +0,0 @@
/*!
* Bootstrap util.js v4.4.1 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):(t=t||self).Util=e(t.jQuery)}(this,(function(t){"use strict";t=t&&t.hasOwnProperty("default")?t.default:t;function e(e){var r=this,o=!1;return t(this).one(n.TRANSITION_END,(function(){o=!0})),setTimeout((function(){o||n.triggerTransitionEnd(r)}),e),this}var n={TRANSITION_END:"bsTransitionEnd",getUID:function(t){do{t+=~~(1e6*Math.random())}while(document.getElementById(t));return t},getSelectorFromElement:function(t){var e=t.getAttribute("data-target");if(!e||"#"===e){var n=t.getAttribute("href");e=n&&"#"!==n?n.trim():""}try{return document.querySelector(e)?e:null}catch(t){return null}},getTransitionDurationFromElement:function(e){if(!e)return 0;var n=t(e).css("transition-duration"),r=t(e).css("transition-delay"),o=parseFloat(n),i=parseFloat(r);return o||i?(n=n.split(",")[0],r=r.split(",")[0],1e3*(parseFloat(n)+parseFloat(r))):0},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(e){t(e).trigger("transitionend")},supportsTransitionEnd:function(){return Boolean("transitionend")},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,r){for(var o in r)if(Object.prototype.hasOwnProperty.call(r,o)){var i=r[o],a=e[o],u=a&&n.isElement(a)?"element":(s=a,{}.toString.call(s).match(/\s([a-z]+)/i)[1].toLowerCase());if(!new RegExp(i).test(u))throw new Error(t.toUpperCase()+': Option "'+o+'" provided type "'+u+'" but expected type "'+i+'".')}var s},findShadowRoot:function(t){if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){var e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?n.findShadowRoot(t.parentNode):null},jQueryDetection:function(){if(void 0===t)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1===e[0]&&9===e[1]&&e[2]<1||e[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};return n.jQueryDetection(),t.fn.emulateTransitionEnd=e,t.event.special[n.TRANSITION_END]={bindType:"transitionend",delegateType:"transitionend",handle:function(e){if(t(e.target).is(this))return e.handleObj.handler.apply(this,arguments)}},n}));

View File

@@ -1,11 +0,0 @@
$font-family-sans-serif: "Open Sans", sans-serif;
//Make it look less primary school
$font-size-base: 0.875rem;
$theme-colors: (
"yellow": #ffd351,
"success": #3b7743,
"warning": #D3963B,
"danger": #A94447,
"info": #B8FAFF,
"primary": #4CB8F1
);

View File

@@ -30,6 +30,18 @@
{% endif %} {% endif %}
</div> </div>
</li> </li>
{% if perms.RIGS.view_riskassessment %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownHS" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
H&S
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownHS">
<a class="dropdown-item" href="{% url 'hs_list' %}"><span class="fas fa-eye"></span> Overview</a>
<a class="dropdown-item" href="{% url 'ra_list' %}"><span class="fas fa-file-medical"></span> Risk Assessments</a>
<a class="dropdown-item" href="{% url 'ec_list' %}"><span class="fas fa-tasks"></span> Event Checklists</a>
</div>
</li>
{% endif %}
{% if perms.RIGS.view_invoice %} {% if perms.RIGS.view_invoice %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<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">
@@ -53,51 +65,14 @@
{% if perms.RIGS.view_venue %} {% if perms.RIGS.view_venue %}
<li class="nav-item"><a class="nav-link" href="{% url 'venue_list' %}">Venues</a></li> <li class="nav-item"><a class="nav-link" href="{% url 'venue_list' %}">Venues</a></li>
{% endif %} {% endif %}
<form id="searchForm" class="form-inline flex-nowrap mx-3" role="form" method="GET">
<div class="input-group input-group-sm flex-nowrap">
<div class="input-group-prepend">
<input id="id_search_input" type="search" name="q" class="form-control form-control-sm" placeholder="Search..." />
</div>
<select id="search-options" class="custom-select form-control">
<option selected data-action="{% url 'event_archive' %}" href="#">Events</option>
<option data-action="{% url 'person_list' %}" href="#">People</option>
<option data-action="{% url 'organisation_list' %}" href="#">Organisations</option>
<option data-action="{% url 'venue_list' %}" href="#">Venues</option>
{% if perms.RIGS.view_invoice %}
<option data-action="{% url 'invoice_archive' %}" href="#">Invoices</option>
{% endif %}
</select>
</div>
<button class="btn btn-primary form-control form-control-sm btn-sm">Search</button>
<a href="{% url 'search_help' %}" class="nav-link modal-href btn-sm"><span class="fas fa-question-circle"></span></a>
</form>
{% endif %} {% endif %}
<li class="nav-item dropdown" id="user">
{% if user.is_authenticated %}
<a class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Hi {{ user.first_name }}
</a>
<ul class="dropdown-menu p-3" id="userdropdown">
<li class="media">
<a href="{% url 'profile_detail' %}">
<img src="{{ request.user.profile_picture }}" class="media-object"/>
<div class="media-body">
<b>{{ request.user.first_name }} {{ request.user.last_name }}</b>
<p class="muted">{{ request.user.email }}</p>
</div>
</a>
</li>
<li>
<a href="{% url 'logout' %}" class="btn btn-primary align"><i class="fas fa-sign-out-alt"></i> Logout</a>
</li>
</ul>
{% else %}
<a class="nav-link" href="{% url 'login' %}">
Login
</a>
{% endif %}
</li>
{% endblock %} {% endblock %}
{% block titleelements_right %}
{% include 'partials/search.html' %}
{% include 'partials/navbar_user.html' %}
{% endblock %}
{% block js %} {% block js %}
<script src="{% static 'js/tooltip.js' %}"></script> <script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/popover.js' %}"></script> <script src="{% static 'js/popover.js' %}"></script>
@@ -106,17 +81,4 @@
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
}) })
</script> </script>
<script>
$(document).ready(function(){
$('#search-options option').click(function(){
$('#searchForm').attr('action', $(this).data('action')).submit();
});
$('#id_search_input').keypress(function (e) {
if (e.which == 13) {
$('#searchForm').attr('action', $('#search-options option').first().data('action')).submit();
return false;
}
});
});
</script>
{% endblock %} {% endblock %}

View File

@@ -1,77 +1,60 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% load static %} {% load static %}
{% block title %}Calendar{% endblock %} {% block title %}Calendar{% endblock %}
{% block css %} {% block css %}
<link href="{% static "css/fullcalendar.css" %}" rel='stylesheet' /> <link href="{% static 'css/main.min.css' %}" rel='stylesheet' />
<link href="{% static "css/fullcalendar.print.css" %}" rel='stylesheet' media='print' />
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{% static 'js/moment.js' %}"></script> <script src="{% static 'js/moment.js' %}"></script>
<script src="{% static 'js/fullcalendar.js' %}"></script> <script src="{% static 'js/main.min.js' %}"></script>
<script> <script>
function getUrlVars() { viewToUrl = {
var vars = {}; 'timeGridWeek':'week',
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) { 'timeGridDay':'day',
vars[key] = value; 'dayGridMonth':'month'
});
return vars;
} }
viewFromUrl = {
'week':'timeGridWeek',
'day':'timeGridDay',
'month':'dayGridMonth'
}
var calendar; //Need to access it from jquery ready
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
$(document).ready(function() { calendar = new FullCalendar.Calendar(calendarEl, {
themeSystem: 'bootstrap',
viewToUrl = { //defaultView: 'dayGridMonth', This is now default
'agendaWeek':'week', aspectRatio: 1.5,
'agendaDay':'day', eventTimeFormat: {
'month':'month' 'hour': '2-digit',
} 'minute': '2-digit',
viewFromUrl = { 'hour12': false
'week':'agendaWeek', },
'day':'agendaDay', //nowIndicator: true,
'month':'month' //firstDay: 1,
} headerToolbar: false,
editable: false,
$('#calendar').fullCalendar({ dayMaxEventRows: true, // allow "more" link when too many events
editable: false, events: function(fetchInfo, successCallback, failureCallback) {
eventLimit: true, // allow "more" link when too many events
firstDay: 1,
aspectRatio: 1.5,
timeFormat: 'HH:mm',
views: {
basic: {
// options apply to basicWeek and basicDay views
},
agenda: {
// options apply to agendaWeek and agendaDay views
},
week: {
columnFormat:'ddd D/M'
},
day: {
// options apply to basicDay and agendaDay views
}
},
header:false,
events: function(start_moment, end_moment, timezone, callback) {
$.ajax({ $.ajax({
url: '/api/event', url: '/api/event',
dataType: 'json', dataType: 'json',
data: { data: {
start: moment(start_moment).format("YYYY-MM-DD[T]HH:mm:ss"), start: moment(fetchInfo.startStr).format("YYYY-MM-DD[T]HH:mm:ss"),
end: moment(end_moment).format("YYYY-MM-DD[T]HH:mm:ss") end: moment(fetchInfo.endStr).format("YYYY-MM-DD[T]HH:mm:ss")
}, },
success: function(doc) { success: function(doc) {
var events = []; var events = [];
colours = {'Provisional': '#f0ad4e', colours = {
'Confirmed': '#5cb85c' , 'Provisional': '#FFE89B',
'Booked': '#5cb85c' , 'Confirmed': '#3AB54A' ,
'Booked': '#3AB54A' ,
'Cancelled': 'grey' , 'Cancelled': 'grey' ,
'non-rig': '#5bc0de' 'non-rig': '#25AAE2'
}; };
$(doc).each(function() { $(doc).each(function() {
end = $(this).attr('latest') end = $(this).attr('latest')
@@ -87,100 +70,94 @@
'url': $(this).attr('url') 'url': $(this).attr('url')
} }
if($(this).attr('is_rig')==true || $(this).attr('status') == "Cancelled"){ if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){
thisEvent['color'] = colours[$(this).attr('status')]; thisEvent['color'] = colours[$(this).attr('status')];
}else{ }else{
thisEvent['color'] = colours['non-rig']; thisEvent['color'] = colours['non-rig'];
} }
events.push(thisEvent); events.push(thisEvent);
}); });
callback(events); successCallback(events);
} }
}); });
}, },
datesSet: function(info) {
viewRender: function(view, element){ var view = info.view;
// Set the title of the view // Set the title of the view
$('#calendar-header').text(view.title); $('#calendar-header').text(view.title);
// Enable/Disable "Today" button as required // Enable/Disable "Today" button as required
if(moment().isBetween(view.intervalStart, view.intervalEnd)){ let $today = $('#today-button');
if(moment().isBetween(view.currentStart, view.currentEnd)){
//Today is within the current view //Today is within the current view
$('#today-button').prop('disabled', true); $today.prop('disabled', true);
}else{ }else{
$('#today-button').prop('disabled', false); $today.prop('disabled', false);
} }
// Set active view select button // Set active view select button
switch(view.name){ let $month = $('#month-button');
case 'month': let $week = $('#week-button');
$('#month-button').addClass('active'); let $day = $('#day-button');
$('#week-button').removeClass('active'); switch(view.type){
$('#day-button').removeClass('active'); case 'dayGridMonth':
$month.addClass('active');
$week.removeClass('active');
$day.removeClass('active');
break; break;
case 'agendaWeek': case 'timeGridWeek':
$('#month-button').removeClass('active'); $month.removeClass('active');
$('#week-button').addClass('active'); $week.addClass('active');
$('#day-button').removeClass('active'); $day.removeClass('active');
break; break;
case 'agendaDay': case 'timeGridDay':
$('#month-button').removeClass('active'); $month.removeClass('active');
$('#week-button').removeClass('active'); $week.removeClass('active');
$('#day-button').addClass('active'); $day.addClass('active');
break; break;
} }
history.replaceState(null,null,'{% url 'web_calendar' %}'+viewToUrl[view.name]+'/'+view.intervalStart.format('YYYY-MM-DD')+'/'); history.replaceState(null,null,"{% url 'web_calendar' %}"+viewToUrl[view.type]+'/'+moment(view.currentStart).format('YYYY-MM-DD')+'/');
} }
}); });
calendar.render();
});
$(document).ready(function() {
// set some button listeners // set some button listeners
$('#next-button').click(function(){ calendar.next(); });
$('#next-button').click(function(){ $('#calendar').fullCalendar('next') }); $('#prev-button').click(function(){ calendar.prev(); });
$('#prev-button').click(function(){ $('#calendar').fullCalendar('prev') }); $('#today-button').click(function(){ calendar.today(); });
$('#today-button').click(function(){ $('#calendar').fullCalendar('today') }); $('#month-button').click(function(){ calendar.changeView('dayGridMonth'); });
$('#week-button').click(function(){ calendar.changeView('timeGridWeek'); });
$('#month-button').click(function(){ $('#calendar').fullCalendar('changeView','month') }); $('#day-button').click(function(){ calendar.changeView('timeGridDay'); });
$('#week-button').click(function(){ $('#calendar').fullCalendar('changeView','agendaWeek') });
$('#day-button').click(function(){ $('#calendar').fullCalendar('changeView','agendaDay') });
$('#go-to-date-input').change(function(){ $('#go-to-date-input').change(function(){
if( moment($('#go-to-date-input').val()).isValid() ){ if(moment($('#go-to-date-input').val()).isValid()){
$('#go-to-date-button').prop('disabled', false); $('#go-to-date-button').prop('disabled', false);
}else{ } else{
$('#go-to-date-button').prop('disabled', true); $('#go-to-date-button').prop('disabled', true);
} }
}); });
$('#go-to-date-button').click(function(){ $('#go-to-date-button').click(function(){
day = moment($('#go-to-date-input').val()) ; day = moment($('#go-to-date-input').val());
if(day.isValid()){ if(day.isValid()){
$('#calendar').fullCalendar( 'gotoDate', day); calendar.gotoDate(day.format("YYYY-MM-DD"));
}else{ } else{
alert('Invalid Date'); alert('Invalid Date');
} }
}); });
{% if view and date %}
// Go to the initial settings, if they're valid // Go to the initial settings, if they're valid
view = viewFromUrl['{{view}}']; view = viewFromUrl['{{view}}'];
$('#calendar').fullCalendar( 'changeView', view); calendar.changeView(view);
day = moment('{{date}}'); day = moment('{{date}}');
if(day.isValid()){ if(day.isValid()){
$('#calendar').fullCalendar( 'gotoDate', day); calendar.gotoDate(day.format("YYYY-MM-DD"));
}else{ } else{
console.log('Supplied date is invalid - using default') console.log('Supplied date is invalid - using default')
} }
{% endif %}
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,78 +0,0 @@
<div class="row my-3">
<div class="col-sm-6">
<div class="card mb-3">
<div class="card-header">Contact Details</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-5">Person</dt>
<dd class="col-sm-7">
{% if event.person %}
{{ event.person.name }}
{% endif %}
</dd>
<dt class="col-sm-5">Email</dt>
<dd class="col-sm-7">
<span class="overflow-ellipsis">{{ event.person.email }}</span>
</dd>
<dt class="col-sm-5">Phone Number</dt>
<dd class="col-sm-7">{{ event.person.phone }}</dd>
</dl>
</div>
</div>
{% if event.organisation %}
<div class="card mt-3">
<div class="card-header">Organisation Details</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-5">Organisation</dt>
<dd class="col-sm-7">
{{ event.organisation.name }}
</dd>
<dt class="col-sm-5">Phone Number</dt>
<dd class="col-sm-7">{{ object.organisation.phone }}</dd>
</dl>
</div>
</div>
{% endif %}
</div>
<div class="col-sm-6">
<div class="card border-info">
<div class="card-header">Event Info</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-5">Event Venue</dt>
<dd class="col-sm-7">
{% if object.venue %}
<a href="{% url 'venue_detail' object.venue.pk %}" class="modal-href">
{{ object.venue }}
</a>
{% endif %}
</dd>
<dt class="col-sm-5">Status</dt>
<dd class="col-sm-7">{{ event.get_status_display }}</dd>
<dd class="col-sm-12">&nbsp;</dd>
<dt class="col-sm-5">Access From</dt>
<dd class="col-sm-7">{{ event.access_at|date:"D d M Y H:i"|default:"" }}</dd>
<dt class="col-sm-5">Event Starts</dt>
<dd class="col-sm-7">{{ event.start_date|date:"D d M Y" }} {{ event.start_time|date:"H:i" }}</dd>
<dt class="col-sm-5">Event Ends</dt>
<dd class="col-sm-7">{{ event.end_date|date:"D d M Y" }} {{ event.end_time|date:"H:i" }}</dd>
<dd class="col-sm-12">&nbsp;</dd>
<dt class="col-sm-5">Event Description</dt>
<dd class="col-sm-12">{{ event.description|linebreaksbr }}</dd>
</dl>
</div>
</div>
</div>
</div>

View File

@@ -1,48 +1,56 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% load paginator from filters %} {% load paginator from filters %}
{% load get_list from filters %}
{% load button from filters %}
{% load static %}
{% block title %}Event Archive{% endblock %} {% block css %}
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
{% endblock %}
{% block preload_js %}
<script src="{% static 'js/bootstrap-select.js' %}"></script>
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12 py-2">
<h2>Event Archive</h2> <form class="form-inline" method="GET">
</div> <div class="input-group mx-2">
<div class="col-sm-12 py-2"> <div class="input-group-prepend">
<form class="form-inline"> <span class="input-group-text">Start</span>
<div class="input-group mx-2">
<div class="input-group-prepend">
<span class="input-group-text">Start</span>
</div>
<input type="date" name="start" id="start" value="{{ start|default_if_none:"" }}" placeholder="Start" class="form-control" />
</div> </div>
<div class="input-group mx-2"> <input type="date" name="start" id="start" value="{{ start|default_if_none:'' }}" placeholder="Start" class="form-control" />
<div class="input-group-prepend"> </div>
<span class="input-group-text">End</span> <div class="input-group mx-2">
</div> <div class="input-group-prepend">
<input type="date" name="end" id="end" value="{{ end|default_if_none:"" }}" placeholder="End" class="form-control" /> <span class="input-group-text">End</span>
</div> </div>
<div class="input-group mx-2"> <input type="date" name="end" id="end" value="{{ end|default_if_none:'' }}" placeholder="End" class="form-control" />
<div class="input-group-prepend"> </div>
<span class="input-group-text">Keyword</span> <div class="input-group mx-2">
</div> <div class="input-group-prepend">
<input type="search" name="q" placeholder="Keyword" value="{{ request.GET.q }}" class="form-control" /> <span class="input-group-text">Keyword</span>
</div> </div>
<input type="submit" class="btn btn-primary" value="Search"/> <input type="search" name="q" placeholder="Keyword" value="{{ request.GET.q }}" class="form-control" />
</form> </div>
</div> <select class="selectpicker pr-3" multiple data-actions-box="true" data-none-selected-text="Status" data-actions-box="true" id="status" name="status">
{% for status in statuses %}
<option value="{{status.0}}" {% if status.0|safe in request.GET|get_list:'status' %}selected=""{% endif %}>{{status.1}}</option>
{% endfor %}
</select>
{% button 'search' %}
</form>
</div> </div>
<div class="row"> </div>
<div class="col-sm-12"> <div class="row">
{% with object_list as events %} <div class="col-sm-12">
{% include 'event_table.html' %} {% with object_list as events %}
{% endwith %} {% include 'partials/event_table.html' %}
</div> {% endwith %}
</div> </div>
</div>
{% paginator %}
{% if is_paginated %}
<div class="row justify-content-center">
{% paginator %}
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,247 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load help_text from filters %}
{% load profile_by_index from filters %}
{% load yesnoi from filters %}
{% load button from filters %}
{% block content %}
<div class="row">
<div class="col-12 text-right my-3">
{% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div>
</div>
<div class="row">
<div class="col-md-6 col-sm-12">
<div class="card mb-3">
<div class="card-header">General</div>
<div class="card-body">
<dl class="row">
<dt class="col-6">Date</dt>
<dd class="col-6">
{{ object.date }}
</dd>
<dt class="col-6">Venue</dt>
<dd class="col-6">
{% if object.venue %}
<a href="{% url 'venue_detail' object.venue.pk %}" class="modal-href">
{{ object.venue }}
</a>
{% endif %}
</dd>
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
<dd class="col-6">
<a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a>
</dd>
</dl>
<p>List vehicles and their drivers</p>
<ul>
{% for i in object.vehicles.all %}
<li>{{i}}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="col-md-6 col-sm-12">
<div class="card mb-3">
<div class="card-header">Safety Checks</div>
<div class="card-body">
<dl class="row">
<dt class="col-10">{{ object|help_text:'safe_parking'|safe }}</dt>
<dd class="col-2">
{{ object.safe_parking|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'safe_packing'|safe }}</dt>
<dd class="col-2">
{{ object.safe_packing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'exits'|safe }}</dt>
<dd class="col-2">
{{ object.exits|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'trip_hazard'|safe }}</dt>
<dd class="col-2">
{{ object.trip_hazard|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'warning_signs'|safe }}</dt>
<dd class="col-2">
{{ object.warning_signs|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'ear_plugs'|safe }}</dt>
<dd class="col-2">
{{ object.ear_plugs|yesnoi }}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header">Crew Record</div>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Crewmember</th>
<th scope="col">Start Time</th>
<th scope="col">Role</th>
<th scope="col">End Time</th>
</tr>
</thead>
<tbody id="crewmemberst">
{% for crew in object.crew.all %}
<tr>
<td>{{crew.crewmember}}</td>
<td>{{crew.start}}</td>
<td>{{crew.role}}</td>
<td>{{crew.end}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card mb-3">
<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">
{% if object.event.riskassessment.event_size == 1 %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt>
<dd class="col-2">
{{ object.source_rcd|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'labelling'|safe }}</dt>
<dd class="col-2">
{{ object.labelling|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
<dd class="col-2">
{{ object.earthing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
<dd class="col-2">
{{ object.pat|yesnoi }}
</dd>
</dl>
<hr>
<p>Tests at first distro</p>
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" colspan="3" class="text-center">Value</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small></th>
<th>{{ object|help_text:'fd_voltage_l1' }}</th>
<th>{{ object|help_text:'fd_voltage_l2' }}</th>
<th>{{ object|help_text:'fd_voltage_l3' }}</th>
</tr>
<tr>
<td>{{ object.fd_voltage_l1 }}</td>
<td>{{ object.fd_voltage_l2 }}</td>
<td>{{ object.fd_voltage_l3 }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_phase_rotation'|safe }}</th>
<td colspan="3">{{ object.fd_phase_rotation|yesnoi }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_earth_fault'|safe}}</th>
<td colspan="3">{{ object.fd_earth_fault }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_pssc'}}</th>
<td colspan="3">{{ object.fd_pssc }}</td>
</tr>
</tbody>
</table>
<hr>
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" class="text-center">Point 1</th>
<th scope="col" class="text-center">Point 2</th>
<th scope="col" class="text-center">Point 3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">{{ object|help_text:'w1_description'|safe}}</th>
<td>{{ object.w1_description }}</td>
<td>{{ object.w2_description|default:'' }}</td>
<td>{{ object.w3_description|default:'' }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_polarity'|safe}}</th>
<td>{{ object.w1_polarity|yesnoi }}</td>
<td>{{ object.w2_polarity|default:''|yesnoi }}</td>
<td>{{ object.w3_polarity|default:''|yesnoi }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_voltage'|safe}}</th>
<td>{{ object.w1_voltage }}</td>
<td>{{ object.w2_voltage|default:'' }}</td>
<td>{{ object.w3_voltage|default:'' }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_earth_fault'|safe}}</th>
<td>{{ object.w1_earth_fault }}</td>
<td>{{ object.w2_earth_fault|default:'' }}</td>
<td>{{ object.w3_earth_fault|default:'' }}</td>
</tr>
</tbody>
</table>
<hr>
<dl class="row">
<dt class="col-10">{{ object|help_text:'all_rcds_tested'|safe }}</dt>
<dd class="col-2">
{{ object.all_rcds_tested|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'public_sockets_tested'|safe }}</dt>
<dd class="col-2">
{{ object.public_sockets_tested|yesnoi }}
</dd>
</dl>
<hr>
{% include 'partials/ec_power_info.html' %}
{% 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 %}
</div>
{% endif %}
</div>
</div>
<div class="col-12 text-right">
{% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div>
<div class="col-12 text-right">
{% include 'partials/last_edited.html' with target="eventchecklist_history" %}
</div>
{% endblock %}

View File

@@ -0,0 +1,367 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load help_text from filters %}
{% load profile_by_index from filters %}
{% load button from filters %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/>
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/bootstrap-select.js' %}"></script>
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
{% endblock %}
{% block js %}
{{ block.super }}
<script src="{% static 'js/jquery-ui.js' %}"></script><!--TODO optimise-->
<script src="{% static 'js/interaction.js' %}"></script>
<script src="{% static 'js/modal.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/autocompleter.js' %}"></script>
{% include 'partials/datetime-fix.html' %}
<script>
$(document).ready(function () {
$('button[data-action=add]').on('click', function (event) {
event.preventDefault();
var target = $($(this).attr('data-target'));
var newID = Number(target.attr('data-pk'));
var newRow = $($(this).attr('data-clone'))
.clone().attr('style', "")
.attr('id', function(i, val){
return val.split("_")[0] + '_' + newID;
})
.appendTo(target);
newRow.find('select,input').attr('name', function(i, val){
return val.split("_")[0] + '_' + newID;
})//Disabled is to prevent the hidden row being sent to the form
.removeAttr('disabled');
newRow.find('button[data-action=delete]').attr('data-id', newID);
newRow.find('select').addClass('selectpicker');
newRow.find('.selectpicker').selectpicker('refresh');
$(".selectpicker").each(function(){initPicker($(this))});
initDatetime();
$(target).attr('data-pk', newID - 1);
});
$(document).on('click', 'button[data-action=delete]', function(event) {
event.preventDefault();
$(this).closest('tr').remove();
});
//Somewhat rudimentary way of ensuring people fill in completely (if it hits the database validation the whole table row disappears when the page reloads...)
//the not is to avoid adding it to some of bootstrap-selects extra crap
$('#vehiclest,#crewmemberst').on('change', 'select,input', function () {
$(this).closest('tr').find("select,input").not(':input[type=search]').attr('required', 'true');
});
});
</script>
{% endblock %}
{% block content %}
<div class="col-12">
{% include 'form_errors.html' %}
{% if edit %}
<form role="form" method="POST" action="{% url 'ec_edit' pk=object.pk %}">
{% else %}
<form role="form" method="POST" action="{% url 'event_ec' pk=event.pk %}">
{% endif %}
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
value="{{event.pk}}"/>
{% csrf_token %}
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">Event Information</div>
<div class="card-body">
<dl class="row">
<dt class="col-4">Event Date</dt>
<dd class="col-8">{{ event.start_date}}{%if event.end_date %}-{{ event.end_date}}{%endif%}</dd>
<dt class="col-4">Event Name</dt>
<dd class="col-8">{{ event.name }}</dd>
<dt class="col-4">Client</dt>
<dd class="col-8">{{ event.person }}</dd>
<dt class="col-4">Event Size</dt>
<dd class="col-8">{% include 'partials/event_size.html' with object=event.riskassessment %}</dd>
</dl>
<div class="form-group form-row">
<label for="{{ form.date.id_for_label }}"
class="col-4 col-form-label">{{ form.date.label }}</label>
{% if not form.date.value %}
{% render_field form.date class+="form-control col-8" value=event.start_date %}
{% else %}
{% render_field form.date class+="form-control col-8" %}
{% endif %}
</div>
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
<label for="{{ form.venue.id_for_label }}"
class="col-4 col-form-label">{{ form.venue.label }}</label>
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %}
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
{% elif event.venue %}
<option value="{{event.venue.pk}}" selected="selected">{{ event.venue.name }}</option>
{% endif %}
</select>
</div>
<div class="form-group form-row" id="{{ form.power_mic.id_for_label }}-group">
<label for="{{ form.power_mic.id_for_label }}"
class="col-4 col-form-label">{{ form.power_mic.help_text }}</label>
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true">
{% if power_mic %}
<option value="{{power_mic.pk}}" selected="selected">{{ power_mic.name }}</option>
{% elif event.riskassessment.power_mic %}
<option value="{{event.riskassessment.power_mic.pk}}" selected="selected">{{ event.riskassessment.power_mic.name }}</option>
{% endif %}
</select>
</div>
<p class="pt-3 font-weight-bold">List vehicles and their drivers</p>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Vehicle</th>
<th scope="col">Driver</th>
<th scope="col"></th>
</tr>
</thead>
<tbody id="vehiclest" data-pk="-1">
<tr id="vehicles_new" style="display: none;">
<td><input type="text" class="form-control" name="vehicle_new" disabled="true"/></td>
<td><select class="form-control" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" name="driver_new" disabled="true"></select></td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button</td>
</tr>
{% for i in object.vehicles.all %}
<tr id="vehicles_{{i.pk}}">
<td><input name="vehicle_{{i.pk}}" type="text" class="form-control" value="{{ i.vehicle }}"/></td>
<td>
<select name="driver_{{i.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if i.driver != '' %}
<option value="{{i.driver.pk}}" selected="selected">{{ i.driver.name }}</option>
{% endif %}
</select>
</td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{i.pk}}' data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="text-right">
<button type="button" class="btn btn-secondary" id="vehicle-add" data-action='add' data-target='#vehiclest' data-clone='#vehicles_new'><span class="fas fa-plus"></span> Add Vehicle</button>
</div>
</div>
</div>
</div>
</div>
<div class="row my-3">
<div class="col-12">
<div class="card">
<div class="card-header">Safety Checks</div>
<div class="card-body">
{% include 'partials/checklist_checkbox.html' with formitem=form.safe_parking %}
{% include 'partials/checklist_checkbox.html' with formitem=form.safe_packing %}
{% include 'partials/checklist_checkbox.html' with formitem=form.exits %}
{% include 'partials/checklist_checkbox.html' with formitem=form.trip_hazard %}
{% include 'partials/checklist_checkbox.html' with formitem=form.warning_signs %}
{% include 'partials/checklist_checkbox.html' with formitem=form.ear_plugs %}
<div class="row pt-3">
<label class="col-5" for="{{ form.hs_location.id_for_label }}">{{ form.hs_location.help_text }}</label>
{% render_field form.hs_location class+="form-control col-7 col-md-4" %}
</div>
<div class="row pt-1">
<label class="col-5" for="{{ form.extinguishers_location.id_for_label }}">{{ form.extinguishers_location.help_text }}</label>
{% render_field form.extinguishers_location class+="form-control col-7 col-md-4" %}
</div>
</div>
</div>
</div>
</div>
<div class="row my-3">
<div class="col-12">
<div class="card">
<div class="card-header">Crew Record</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Person</th>
<th scope="col">Start Time</th>
<th scope="col">Role</th>
<th scope="col">End Time</th>
<th scope="col"></th>
</tr>
</thead>
<tbody id="crewmemberst" data-pk="-1">
<tr id="crew_new" style="display: none;">
<td>
<select name="crewmember_new" class="form-control" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" disabled="true"></select>
</td>
<td style="min-width: 15ch"><input name="start_new" type="datetime-local" class="form-control" value="{{ i.start }}" disabled="true"/></td>
<td style="min-width: 15ch"><input name="role_new" type="text" class="form-control" value="{{ i.role }}" disabled="true"/></td>
<td style="min-width: 15ch"><input name="end_new" type="datetime-local" class="form-control" value="{{ i.end }}" disabled="true" /></td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{crew.pk}}' data-action='delete' data-target='#crewmember'><span class="fas fa-times"></span></button</td>
</tr>
{% for crew in object.crew.all %}
<tr id="crew_{{crew.pk}}">
<td>
<select name="crewmember_{{crew.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if crew.crewmember != '' %}
<option value="{{crew.crewmember.pk}}" selected="selected">{{ crew.crewmember.name }}</option>
{% endif %}
</select>
</td>
<td><input name="start_{{crew.pk}}" type="datetime-local" class="form-control" value="{{ crew.start|date:'Y-m-d' }}T{{ crew.start|date:'H:i:s' }}"/></td>
<td><input name="role_{{crew.pk}}" type="text" class="form-control" value="{{ crew.role }}"/></td>
<td><input name="end_{{crew.pk}}" type="datetime-local" class="form-control" value="{{ crew.end|date:'Y-m-d' }}T{{ crew.end|date:'H:i:s' }}"/></td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{crew.pk}}' data-action='delete' data-target='#crewmember'><span class="fas fa-times"></span></button</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card-footer">
<div class="text-right">
<button type="button" class="btn btn-secondary" data-action='add' data-target='#crewmemberst' data-clone='#crew_new'><span class="fas fa-plus"></span> Add Crewmember</button>
</div>
</div>
</div>
</div>
</div>
{% if event.riskassessment.event_size == 0 %}
<div class="row my-3" id="size-0">
<div class="col-12">
<div class="card border-success">
<div class="card-header">Electrical Checks <small>for Small TEC Events <6kVA (aprox. 26A)</small></div>
<div class="card-body">
{% include 'partials/checklist_checkbox.html' with formitem=form.rcds %}
{% include 'partials/checklist_checkbox.html' with formitem=form.supply_test %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
</div>
</div>
</div>
</div>
{% elif event.riskassessment.event_size == 1 %}
<div class="row my-3" id="size-1">
<div class="col-12">
<div class="card border-warning">
<div class="card-header">Electrical Checks <small>for Medium TEC Events </small></div>
<div class="card-body">
{% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %}
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
<hr>
<p>Tests at first distro</p>
<div class="table-responsive">
<table class="table table-bordered table-sm">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" colspan="3" class="text-center">Value</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small></th>
<th class="text-center">{{ form.fd_voltage_l1.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l2.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l3.help_text }}</th>
</tr>
<tr>
<td>{% render_field form.fd_voltage_l1 class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.fd_voltage_l2 class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.fd_voltage_l3 class+="form-control" style="min-width: 5rem;" %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_phase_rotation.help_text|safe}}</th>
<td colspan="3">{% include 'partials/checklist_checkbox.html' with formitem=form.fd_phase_rotation %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_earth_fault.help_text|safe}}</th>
<td colspan="3">{% render_field form.fd_earth_fault class+="form-control" %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_pssc.help_text|safe}}</th>
<td colspan="3">{% render_field form.fd_pssc class+="form-control" %}</td>
</tr>
</tbody>
</table>
</div>
<hr>
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" class="text-center">Point 1</th>
<th scope="col" class="text-center">Point 2</th>
<th scope="col" class="text-center">Point 3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">{{form.w1_description.help_text|safe}}</th>
<td>{% render_field form.w1_description class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.w2_description class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.w3_description class+="form-control" style="min-width: 5rem;" %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_polarity.help_text|safe}}</th>
<td>{% render_field form.w1_polarity %}</td>
<td>{% render_field form.w2_polarity %}</td>
<td>{% render_field form.w3_polarity %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_voltage.help_text|safe}}</th>
<td>{% render_field form.w1_voltage class+="form-control" %}</td>
<td>{% render_field form.w2_voltage class+="form-control" %}</td>
<td>{% render_field form.w3_voltage class+="form-control" %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_earth_fault.help_text|safe}}</th>
<td>{% render_field form.w1_earth_fault class+="form-control" %}</td>
<td>{% render_field form.w2_earth_fault class+="form-control" %}</td>
<td>{% render_field form.w3_earth_fault class+="form-control" %}</td>
</tr>
</tbody>
</table>
</div>
<hr>
{% include 'partials/checklist_checkbox.html' with formitem=form.all_rcds_tested %}
{% include 'partials/checklist_checkbox.html' with formitem=form.public_sockets_tested %}
{% include 'partials/ec_power_info.html' %}
</div>
</div>
</div>
</div>
{% 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 %}
<div class="row mt-3">
<div class="col-sm-12 text-right">
{% button 'submit' %}
</div>
</div>
</form>
</div>
{% endblock %}

View File

@@ -1,4 +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 linkornone from filters %}
{% load namewithnotes from filters %}
{% block title %}{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %} | {{object.name}}{% endblock %} {% block title %}{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %} | {{object.name}}{% endblock %}
{% block content %} {% block content %}
@@ -18,30 +21,28 @@
{% endif %} {% endif %}
{% if object.is_rig and perms.RIGS.view_event %} {% if object.is_rig and perms.RIGS.view_event %}
{# only need contact details for a rig #} {# only need contact details for a rig #}
<div class="col-sm"> <div class="col-md-6">
{% if event.person %}
<div class="card card-default mb-3"> <div class="card card-default 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-6">Person</dt> <dt class="col-sm-6">Person</dt>
<dd class="col-sm-6" <dd class="col-sm-6">
{% if object.person %} {% if object.person %}
<a href="{% url 'person_detail' object.person.pk %}" class="modal-href"> <a href="{% url 'person_detail' object.person.pk %}" class="modal-href">
{{ object.person }} {{ object.person|namewithnotes:'person_detail' }}
</a> </a>
{% endif %} {% endif %}
</dd> </dd>
<dt class="col-sm-6">Email</dt> <dt class="col-sm-6">Email</dt>
<dd class="col-sm-6"> <dd class="col-sm-6">{{ object.person.email|linkornone:'mailto' }}</dd>
<a href="mailto:{{object.person.email}}" target="_blank">
<span class="overflow-ellipsis">{{ object.person.email }}</span>
</a>
</dd>
<dt class="col-sm-6">Phone Number</dt> <dt class="col-sm-6">Phone Number</dt>
<dd class="col-sm-6"><a href="tel:{{object.person.phone}}">{{ object.person.phone }}</a></dd> <dd class="col-sm-6">{{ object.person.phone|linkornone:'tel' }}</a></dd>
</dl> </dl>
</div> </div>
</div> </div>
{% endif %}
{% if event.organisation %} {% if event.organisation %}
<div class="card card-default"> <div class="card card-default">
<div class="card-header">Organisation</div> <div class="card-header">Organisation</div>
@@ -51,16 +52,14 @@
<dd class="col-sm-6"> <dd class="col-sm-6">
{% if object.organisation %} {% if object.organisation %}
<a href="{% url 'organisation_detail' object.organisation.pk %}" class="modal-href"> <a href="{% url 'organisation_detail' object.organisation.pk %}" class="modal-href">
{{ object.organisation }} {{ object.organisation|namewithnotes:'organisation_detail' }}
</a> </a>
{% endif %} {% endif %}
</dd> </dd>
<dt class="col-sm-6">Email</dt>
<dd class="col-sm-6">{{ object.organisation.email|linkornone:'mailto' }}</dd>
<dt class="col-sm-6">Phone Number</dt> <dt class="col-sm-6">Phone Number</dt>
<dd class="col-sm-6"> <dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</a></dd>
<a href="tel:{{object.person.phone}}">
{{ object.organisation.phone }}
</a>
</dd>
<dt class="col-sm-6">Has SU Account</dt> <dt class="col-sm-6">Has SU Account</dt>
<dd class="col-sm-6">{{ event.organisation.union_account|yesno|capfirst }}</dd> <dd class="col-sm-6">{{ event.organisation.union_account|yesno|capfirst }}</dd>
</dl> </dl>
@@ -69,13 +68,20 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<div class="col-sm"> <div class="col-md-6">
{% include 'partials/event_details.html' %} {% include 'partials/event_details.html' %}
</div> </div>
{% if event.is_rig and event.internal and perms.RIGS.view_event %} {% if not event.dry_hire %}
<div class="col-sm-12 py-3"> <div class="col {% if event.is_rig %}py-3{%endif %}">
{ include 'partials/auth_details.html' %} {% include 'partials/hs_details.html' %}
<div> </div>
{% endif %}
{% if event.is_rig %}
{% if event.is_rig and event.internal and perms.RIGS.view_event %}
<div class="col-md-8 py-3">
{% include 'partials/auth_details.html' %}
</div>
{% endif %}
{% endif %} {% endif %}
{% if not request.is_ajax and perms.RIGS.view_event %} {% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">

View File

@@ -1,25 +1,10 @@
{% load button from filters %}
<div class="btn-group py-3"> <div class="btn-group py-3">
<a href="{% url 'event_update' event.pk %}" class="btn btn-secondary"><span {% button 'edit' 'event_update' event.pk %}
class="fas fa-edit"></span> <span
class="hidden-xs">Edit</span></a>
{% if event.is_rig %} {% if event.is_rig %}
{% if not event.dry_hire %} {% button 'print' 'event_print' event.pk %}
<a href="{% url 'event_ra' event.pk %}" class="btn
{% if event.risk_assessment_edit_url %}
btn-success
{% else %}
btn-warning
{% endif %}
"><i class="fas fa-paperclip"></i> <span
class="hidden-xs">RA</span></a>
{% endif %}
<a href="{% url 'event_print' event.pk %}" target="_blank" class="btn btn-primary"><i
class="fas fa-print"></i> <span
class="hidden-xs">Print</span></a>
{% endif %} {% endif %}
<a href="{% url 'event_duplicate' event.pk %}" class="btn btn-secondary" title="Duplicate Rig"><span {% button 'duplicate' 'event_duplicate' event.pk %}
class="fas fa-copy"></span> <span
class="hidden-xs">Duplicate</span></a>
{% if event.is_rig %} {% if event.is_rig %}
{% if event.internal %} {% if event.internal %}
<a class="btn item-add modal-href event-authorise-request <a class="btn item-add modal-href event-authorise-request
@@ -29,11 +14,13 @@
btn-warning btn-warning
{% elif event.auth_request_to %} {% elif event.auth_request_to %}
btn-info btn-info
{% else %}
btn-secondary
{% endif %} {% endif %}
" "
href="{% url 'event_authorise_request' object.pk %}"> href="{% url 'event_authorise_request' object.pk %}">
<i class="fas fa-paper-plane"></i> <i class="fas fa-paper-plane"></i>
<span class="hidden-xs"> <span class="d-none d-sm-inline">
{% if event.authorised %} {% if event.authorised %}
Authorised Authorised
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %} {% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
@@ -58,7 +45,7 @@
{% endif %} {% endif %}
" title="Invoice Rig"><span " title="Invoice Rig"><span
class="fas fa-pound-sign"></span> class="fas fa-pound-sign"></span>
<span class="hidden-xs">Invoice</span></a> <span class="d-none d-sm-inline">Invoice</span></a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>

View File

@@ -13,7 +13,7 @@
<span class="pull-right"> <span class="pull-right">
{% if object.mic %} {% if object.mic %}
<div class="text-center"> <div class="text-center">
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo img-rounded"/> <img src="{{ object.mic.profile_picture }}" class="event-mic-photo rounded"/>
</div> </div>
{% elif object.is_rig %} {% elif object.is_rig %}
<span class="fas fa-exclamation-sign"></span> <span class="fas fa-exclamation-sign"></span>

View File

@@ -3,17 +3,13 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% load static %} {% load static %}
{% load multiply from filters %} {% load multiply from filters %}
{% load button from filters %}
{% block title %}
{% if object.pk %}
Event {% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
{% else %}New Event{% endif %}
{% endblock %}
{% block css %} {% block css %}
{{ block.super }} {{ block.super }}
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/> <link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/> <link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/>
<link rel="stylesheet" href="{% static 'css/flatpickr.css' %}"/>
{% endblock %} {% endblock %}
{% block preload_js %} {% block preload_js %}
@@ -31,92 +27,40 @@
<script src="{% static 'js/autocompleter.js' %}"></script> <script src="{% static 'js/autocompleter.js' %}"></script>
{% include 'partials/datetime-fix.html' %}
<script> <script>
function setTime23Hours() { const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches;
$('#{{ form.end_time.id_for_label }}').val('23:00');
}
function setTime02Hours() {
var id_start = "{{ form.start_date.id_for_label }}";
var id_end_date = "{{ form.end_date.id_for_label }}";
var id_end_time = "{{ form.end_time.id_for_label }}";
if ($('#'+id_start).val() == $('#'+id_end_date).val()) {
var end_date = new Date($('#'+id_end_date).val());
end_date.setDate(end_date.getDate() + 1);
$('#'+id_end_date).val(end_date.getISOString());
}
$('#'+id_end_time).val('02:00');
}
$(document).ready(function () { $(document).ready(function () {
dur = matches ? 0 : 500;
{% if not object.pk and not form.errors %} {% if not object.pk and not form.errors %}
$('.form-hws').slideUp(dur, function () {
$('.form-hws').slideUp(function () { $('.form-is_rig').slideUp(dur);
$('.form-is_rig').slideUp();
}); });
{% elif not object.pk and form.errors %} {% elif not object.pk and form.errors %}
if ($('#{{form.is_rig.auto_id}}').attr('checked') != 'checked') { if ($('#{{form.is_rig.auto_id}}').attr('checked') !== 'checked') {
$('.form-is_rig').hide(); $('.form-is_rig').hide();
} }
{% endif %} {% endif %}
{% if not object.pk %} {% if not object.pk %}
$('#is_rig-selector button').on('click', function () { $('#is_rig-selector button').on('click', function () {
$('.form-non_rig').slideDown(); $('.form-non_rig').slideDown(dur);
if ($(this).data('is_rig') == 1) { if ($(this).data('is_rig') === 1) {
$('#{{form.is_rig.auto_id}}').prop('checked', true); $('#{{form.is_rig.auto_id}}').prop('checked', true);
if ($('.form-non_rig').is(':hidden')) { if ($('.form-non_rig').is(':hidden')) {
$('.form-is_rig').show(); $('.form-is_rig').show();
} else { } else {
$('.form-is_rig').slideDown(); $('.form-is_rig').slideDown(dur);
} }
$('.form-hws, .form-hws .form-is_rig').css('overflow', 'visible'); $('.form-hws, .form-hws .form-is_rig').css('overflow', 'visible');
} else { } else {
$('#{{form.is_rig.auto_id}}').prop('checked', false); $('#{{form.is_rig.auto_id}}').prop('checked', false);
$('.form-is_rig').slideUp(); $('.form-is_rig').slideUp(dur);
} }
}); });
{% endif %} {% endif %}
function supportsDate() {
//return false; //for development
var input = document.createElement('input');
input.setAttribute('type','date');
var notADateValue = 'not-a-date';
input.setAttribute('value', notADateValue);
return !(input.value === notADateValue);
}
if(supportsDate()){
//Good, we'll use the browser implementation
}else{
//Rubbish browser - do JQuery backup
$('<link>')
.appendTo('head')
.attr({type : 'text/css', rel : 'stylesheet'})
.attr('href', '{% static "css/bootstrap-datetimepicker.min.css" %}');
$.when(
$.getScript( "{% static "js/moment.js" %}" ),
$.getScript( "{% static "js/bootstrap-datetimepicker.js" %}" ),
$.Deferred(function( deferred ){
$( deferred.resolve );
})
).done(function(){
$('input[type=date]').attr('type','text').datetimepicker({
format: 'YYYY-MM-DD',
});
$('input[type=time]').attr('type','text').datetimepicker({
format: 'HH:mm',
});
$('input[type=datetime-local]').attr('type','text').datetimepicker({
format: 'YYYY-MM-DD[T]HH:mm',
sideBySide: true,
});
});
}
}); });
$(document).ready(function () { $(document).ready(function () {
setupItemTable($("#{{ form.items_json.id_for_label }}").val()); setupItemTable($("#{{ form.items_json.id_for_label }}").val());
}); });
@@ -124,59 +68,285 @@
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
}) })
</script> </script>
<noscript>
<style>
.form-hws {
display: inherit !important;
}
</style>
</noscript>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include 'item_modal.html' %} {% include 'item_modal.html' %}
<form class="form-horizontal itemised_form" role="form" method="POST"> <form class=" itemised_form" role="form" method="POST">
{% csrf_token %} {% csrf_token %}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-12">
<h2> {% include 'form_errors.html' %}
{% if duplicate %}
Duplicate of Event N{{ object.pk|stringformat:"05d" }}
{% elif object.pk %}
Event N{{ object.pk|stringformat:"05d" }}
{% else %}
New Event
{% endif %}
</h2>
</div> </div>
{% include 'form_errors.html' %}
{% render_field form.is_rig style="display: none" %} {% render_field form.is_rig style="display: none" %}
<input type="hidden" name="{{ form.items_json.name }}" id="{{ form.items_json.id_for_label }}" <input type="hidden" name="{{ form.items_json.name }}" id="{{ form.items_json.id_for_label }}"
value="{{ form.items_json.value }}"/> value="{{ form.items_json.value }}"/>
{# New rig buttons #} {# New rig buttons #}
{% if not object.pk %} {% if not object.pk %}
<div class="col-sm-12"> <div class="col-sm-12">
<div class="card row align-items-center"> <div class="card text-center" id="is_rig-selector">
<div class="card-body" id="is_rig-selector"> <div class="card-body">
<span data-toggle="tooltip" <span data-toggle="tooltip"
title="Anything that involves TEC kit, crew, or otherwise us providing a service to anyone."> title="Anything that involves TEC kit, crew, or otherwise us providing a service to anyone.">
<button type="button" class="btn btn-primary" data-is_rig="1">Rig</button> <button type="button" class="btn btn-primary w-25" data-is_rig="1">Rig</button>
</span> </span>
<span data-toggle="tooltip" <span data-toggle="tooltip"
title="Things that aren't service-based, like training, meetings and site visits."> title="Things that aren't service-based, like training, meetings and site visits.">
<button type="button" class="btn btn-info" data-is_rig="0">Non-Rig</button> <button type="button" class="btn btn-info w-25" data-is_rig="0">Non-Rig</button>
</span> </span>
<span data-toggle="tooltip" title="Coming soon...">
<button type="button" class="btn btn-warning" data-is_rig="-1">Subhire</button>
</span>
</div>
</div> </div>
</div> </div>
</div>
{% endif %} {% endif %}
{# Contact details #} {# Contact details #}
{% include 'partials/contact_details_form.html' %} <div class="col-md-6 mt-3">
<div class="card form-hws form-is_rig {% if object.pk and not object.is_rig %}hidden{% endif %} mb-3" {% if not object.pk and not form.errors %}style="display: none;"{% endif%}>
<div class="card-header">Contact Details</div>
<div class="card-body">
<div class="form-group" data-toggle="tooltip" title="The main contact for the event, can be left blank if purely an organisation">
<label for="{{ form.person.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.person.label }}</label>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}">
{% if person %}
<option value="{{form.person.value}}" selected="selected" data-update_url="{% url 'person_update' form.person.value %}">{{ person }}</option>
{% endif %}
</select>
</div>
<div class="col-sm-3 col-md-5 col-lg-4 align-right">
<div class="btn-group">
<a href="{% url 'person_create' %}" class="btn btn-success modal-href"
data-target="#{{ form.person.id_for_label }}">
<span class="fas fa-plus"></span>
</a>
<a {% if form.person.value %}href="{% url 'person_update' form.person.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.person.id_for_label }}-update" data-target="#{{ form.person.id_for_label }}">
<span class="fas fa-user-edit"></span>
</a>
</div>
</div>
</div>
</div>
</div>
<div class="form-group" data-toggle="tooltip" title="The client organisation, leave blank if client is an individual">
<label for="{{ form.organisation.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.organisation.label }}</label>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.organisation.id_for_label }}" name="{{ form.organisation.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}" >
{% if organisation %}
<option value="{{form.organisation.value}}" selected="selected" data-update_url="{% url 'organisation_update' form.organisation.value %}">{{ organisation }}</option>
{% endif %}
</select>
</div>
<div class="col-sm-3 col-md-5 col-lg-4 align-right">
<div class="btn-group">
<a href="{% url 'organisation_create' %}" class="btn btn-success modal-href"
data-target="#{{ form.organisation.id_for_label }}">
<span class="fas fa-plus"></span>
</a>
<a {% if form.organisation.value %}href="{% url 'organisation_update' form.organisation.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.organisation.id_for_label }}-update" data-target="#{{ form.organisation.id_for_label }}">
<span class="fas fa-edit"></span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card form-hws form-non_rig mb-3" {% if not object.pk and not form.errors %}style="display: none;"{% endif%}>
<div class="card-header">Event Description</div>
<div class="card-body">
<div class="form-group" data-toggle="tooltip" title="A short description of the event, shown on rigboard and on paperwork">
<label for="{{ form.description.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.description.label }}</label>
<div class="col-sm-8">
{% render_field form.description class+="form-control" %}
</div>
</div>
</div>
</div>
</div>
{# Event details #} {# Event details #}
{% include 'partials/event_details_form.html' %} <div class="col-md-6 my-3">
<div class="card card-default form-hws form-non_rig" {% if not object.pk and not form.errors %}style="display: none;"{% endif%}>
<div class="card-header">Event Details</div>
<div class="card-body">
<div id="form-hws">
<div class="form-group" data-toggle="tooltip" title="Name of the event, displays on rigboard and on paperwork">
<label for="{{ form.name.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.name.label }}</label>
<div class="col-sm-8">
{% render_field form.name class+="form-control" %}
</div>
</div>
<div class="form-group" data-toggle="tooltip" title="The venue for the rig, leave blank if unknown (e.g. for a dry hire)">
<label for="{{ form.venue.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.venue.label }}</label>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %}
<option value="{{form.venue.value}}" selected="selected" data-update_url="{% url 'venue_update' form.venue.value %}">{{ venue }}</option>
{% endif %}
</select>
</div>
<div class="col-sm-3 col-md-5 col-lg-4 align-right">
<div class="btn-group">
<a href="{% url 'venue_create' %}" class="btn btn-success modal-href"
data-target="#{{ form.venue.id_for_label }}">
<span class="fas fa-plus"></span>
</a>
<a href="{% if object.venue %}{% url 'venue_update' object.venue.pk %}{% endif %}" class="btn btn-warning modal-href" id="{{ form.venue.id_for_label }}-update" data-target="#{{ form.venue.id_for_label }}">
<span class="fas fa-edit"></span>
</a>
</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="{{ form.start_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
{% render_field form.start_date class+="form-control" %}
</div>
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="Start time of event, can be left blank">
{% render_field form.start_time class+="form-control" step="60" %}
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="{{ form.end_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
{% render_field form.end_date class+="form-control" %}
</div>
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="End time of event, leave blank if unknown">
{% render_field form.end_time class+="form-control" step="60" %}
</div>
</div>
</div>
</div>
{# Rig only information #}
<div class="form-is_rig {% if object.pk and not object.is_rig %}hidden{% endif %}">
<div class="form-group" data-toggle="tooltip" title="The date/time at which TEC have access to the venue">
<label for="{{ form.access_at.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.access_at.label }}</label>
<div class="col-sm-8">
{% render_field form.access_at class+="form-control" step="60" %}
</div>
</div>
<div class="form-group" data-toggle="tooltip" title="The date/time at which crew should meet for this event">
<label for="{{ form.meet_at.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.meet_at.label }}</label>
<div class="col-sm-8">
{% render_field form.meet_at class+="form-control" step="60" %}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-4 col-sm-8">
<div class="checkbox">
<label data-toggle="tooltip" title="Mark this event as a dry-hire, so it needs to be checked in at the end">
{% render_field form.dry_hire %}{{ form.dry_hire.label }}
</label>
</div>
</div>
</div>
</div>
{# Status is needed on all events types and it looks good here in the form #}
<div class="form-group" data-toggle="tooltip" title="The current status of the event. Only mark as booked once paperwork is received">
<label for="{{ form.status.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.status.label }}</label>
<div class="col-sm-8">
{% render_field form.status class+="form-control" %}
</div>
</div>
<div class="form-is_rig {% if object.pk and not object.is_rig %}hidden{% endif %}">
<div class="form-group" data-toggle="tooltip" title="The Member in Charge of this event">
<label for="{{ form.mic.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.mic.label }}</label>
<div class="col-sm-8">
<select id="{{ form.mic.id_for_label }}" name="{{ form.mic.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if mic %}
<option value="{{form.mic.value}}" selected="selected" >{{ mic.name }}</option>
{% endif %}
</select>
</div>
</div>
{% if object.dry_hire %}
<div class="form-group" data-toggle="tooltip" title="The person who checked-in this dry hire">
<label for="{{ form.checked_in_by.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.checked_in_by.label }}</label>
<div class="col-sm-8">
<select id="{{ form.checked_in_by.id_for_label }}" name="{{ form.checked_in_by.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if checked_in_by %}
<option value="{{form.checked_in_by.value}}" selected="selected" >{{ checked_in_by.name }}</option>
{% endif %}
</select>
</div>
</div>
{% endif %}
<div class="form-group" data-toggle="tooltip" title="The student ID of the client who collected the dry-hire">
<label for="{{ form.collector.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.collector.label }}</label>
<div class="col-sm-8">
{% render_field form.collector class+="form-control" %}
</div>
</div>
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
<label for="{{ form.purchase_order.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label>
<div class="col-sm-8">
{% render_field form.purchase_order class+="form-control" %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{# Notes and item shit #} {# Notes and item shit #}
<div class="col-sm-12"> <div class="col-sm-12">
<div class="card card-default form-hws form-is_rig {% if object.pk and not object.is_rig %}hidden{% endif %}"> <div class="card card-default form-hws form-is_rig {% if object.pk and not object.is_rig %}hidden{% endif %}" {% if not object.pk and not form.errors %}style="display: none;"{% endif%}>
<div class="card-body"> <div class="card-body">
<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">
@@ -188,12 +358,8 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm-12 text-right form-hws form-non_rig my-3"> <div class="col-sm-12 text-right form-hws form-non_rig my-3" {% if not object.pk and not form.errors %}style="display: none;"{% endif%}>
<div class="btn-group"> {% button 'submit' %}
<button type="submit" class="btn btn-primary" title="Save"><i
class="fas fa-save"></i> Save
</button>
</div>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -3,7 +3,7 @@
{% load static %} {% load static %}
<!DOCTYPE document SYSTEM "rml.dtd"> <!DOCTYPE document SYSTEM "rml.dtd">
<document filename="Event {{ object.id }} - {{ object.name }} - {{ object.start_date }}.pdf"> <document filename="{{filename}}">
<docinit> <docinit>
<registerTTFont faceName="OpenSans" fileName="{{ fonts.opensans.regular }}"/> <registerTTFont faceName="OpenSans" fileName="{{ fonts.opensans.regular }}"/>
<registerTTFont faceName="OpenSans-Bold" fileName="{{ fonts.opensans.bold }}"/> <registerTTFont faceName="OpenSans-Bold" fileName="{{ fonts.opensans.bold }}"/>
@@ -97,8 +97,6 @@
<drawString x="265" y="746">info@nottinghamtec.co.uk</drawString> <drawString x="265" y="746">info@nottinghamtec.co.uk</drawString>
<drawString x="137" y="732">Phone: (0115) 846 8720</drawString> <drawString x="137" y="732">Phone: (0115) 846 8720</drawString>
<setFont name="OpenSans" size="10" /> <setFont name="OpenSans" size="10" />
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString> <drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
<setFont name="OpenSans" size="7" /> <setFont name="OpenSans" size="7" />

View File

@@ -314,7 +314,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>24 Hour Emergency Contacts: 07825 065681 and 07825 065678</td> <td>General Enquires and 24 Hour Emergency Contact: 0115 84 68720</td>
</tr> </tr>
{% else %} {% else %}
<tr> <tr>

View File

@@ -1,104 +0,0 @@
<div class="d-none d-md-block">
<div class="table-responsive">
<table class="table mb-0">
<thead class="thead-dark">
<tr>
<th scope="col">#</th>
<th scope="col">Event Date</th>
<th scope="col">Event Details</th>
<th scope="col">MIC</th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr {% include 'partials/event_table_colour.html' %} id="event_row">
<!---Number-->
<th scope="row" id="event_number">{{ event.pk }}</th>
<!--Dates-->
<td id="event_dates">
<div><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></div>
{% if event.end_date and event.end_date != event.start_date %}
<div><strong>{{ event.end_date|date:"D d/m/Y" }}</strong></div>
{% endif %}
<!---Times-->
{% if not event.cancelled %}
<dl class="dl-horizontal">
{% if event.meet_at %}
<dt>Crew meet</dt>
<dd>{{ event.meet_at|date:"H:i" }}<br/>{{ event.meet_at|date:"(Y-m-d)" }}</dd>
{% endif %}
{% if event.has_start_time %}
<dt>Event starts</dt>
<dd>
{{ event.start_time|date:"H:i" }}<br/>
{{ event.start_date|date:"(Y-m-d)" }}<br/>
</dd>
{% endif %}
{% if event.has_end_time%}{% if event.start_date != event.end_date or event.start_time != event.end_time %}
<dt>Event ends</dt>
<dd>
{{ event.end_time|date:"H:i" }}<br/>
{{ event.end_date|date:"(Y-m-d)" }}
</dd>
{% endif %}{% endif %}
</dl>
{% endif %}
</td>
<!---Details-->
<td id="event_details">
<h4>
<a href="{% url 'event_detail' event.pk %}">
{{ event.name }}
</a>
{% if event.venue %}
<small>at {{ event.venue }}</small>
{% endif %}
{% if event.dry_hire %}
<span class="badge badge-secondary">Dry Hire</span>
{% endif %}
</h4>
{% if event.is_rig and not event.cancelled %}
<h5>
{{ event.person.name }}
{% if event.organisation %}
for {{ event.organisation.name }}
{% endif %}
</h5>
{% endif %}
{% if not event.cancelled and event.description %}
<p>{{ event.description|linebreaksbr }}</p>
{% endif %}
{% include 'partials/event_status.html' %}
</td>
<!---MIC-->
<td id="event_mic">
{% if event.mic %}
<div class="media">
{% if perms.RIGS.view_profile %}
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
{% endif %}
<img src="{{ event.mic.profile_picture }}" class="event-mic-photo mr-3"/>
{% if perms.RIGS.view_profile %}
</a>
{% endif %}
<div class="media-body">
<p>{{ event.mic.initials }}</p>
</div>
</div>
{% elif event.is_rig %}
<span class="fas fa-exclamation"></span>
{% endif %}
</td>
</tr>
{% empty %}
<tr class="bg-warning">
<td colspan="6">No events found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="d-xs-block d-sm-block d-md-none">
{% include 'event_table_mobile.html' %}
</div>

View File

@@ -1,41 +0,0 @@
<div class="card">
{% for event in events %}
<div class="card-header {% if event.cancelled %}
text-muted bg-secondary
{% elif event.authorised and event.risk_assessment_edit_url and event.mic %}
bg-success
{% elif not event.is_rig %}
bg-info
{% else %}
bg-warning
{% endif %}
">
<a href="{% url 'event_detail' event.pk %}">{{ event.pk }} | {{ event.name }}</a>
{% if event.dry_hire %}
<span class="badge badge-pill badge-secondary">Dry Hire</span>
{% endif %}
</div>
<div class="card-body">
{% include 'partials/event_status.html' %}
<h6 class="pt-2"><strong>{{ event.start_date|date:"D d/m/Y" }}</strong>
{% if event.end_date and event.end_date != event.start_date %}
<strong> {{ event.end_date|date:"D d/m/Y" }}</strong></h4>
{% else %}
</h6>
{% endif %}
<ul class="list-group list-group-flush pb-3">
<li class="list-group-item">Venue: {{ event.venue }}</li>
<li class="list-group-item">Client: {{ event.person }}</li>
<li class="list-group-item">Organisation: {{ event.organisation }}</li>
</ul>
{% if not event.cancelled and event.description %}
<p>{{ event.description|linebreaksbr }}</p>
{% endif %}
</div>
<div class="card-footer">MIC: {% if event.mic %}<p>{{ event.mic.initials }}</p>{% elif event.is_rig %}<i class="fas fa-exclamation"></i>{% endif %}</div>
{% empty %}
<div class="card-body bg-warning">
<p>No events found</p>
</div>
{% endfor %}
</div>

View File

@@ -0,0 +1,37 @@
{% extends 'base_client.html' %}
{% load widget_tweaks %}
{% load static %}
{% block js %}
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
$('form').on('submit', function () {
$('#loading-modal').modal({
backdrop: 'static',
show: true
});
});
</script>
{% endblock %}
{% block content %}
<div class="row my-3">
{% include 'partials/client_eventdetails.html' %}
</div>
<div class="row mb-3">
<div class="col-sm-12">
<div class="card">
{% with object=event auth=True %}
{% include 'item_table.html' %}
{% endwith %}
</div>
</div>
</div>
{% block authorisation %}
{% endblock %}
{% endblock %}

View File

@@ -9,13 +9,7 @@
by <b>{{ object.name }}</b> as of <b>{{ object.event.last_edited_at }}</b>. by <b>{{ object.name }}</b> as of <b>{{ object.event.last_edited_at }}</b>.
</p> </p>
<p> <p>Your event is now fully booked and payment will be processed by the finance department automatically.</p>
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
Your event is now fully booked and payment will be processed by the finance department automatically.
{% else %}{# external #}
Your event is now fully booked and our finance department will be contact to arrange payment.
{% endif %}
</p>
<p>TEC PA &amp; Lighting</p> <p>TEC PA &amp; Lighting</p>
{% endblock %} {% endblock %}

View File

@@ -2,10 +2,6 @@ Hi {{ to_name|default_if_none:"there" }},
Your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}. Your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
Your event is now fully booked and payment will be processed by the finance department automatically. Your event is now fully booked and payment will be processed by the finance department automatically.
{% else %}{# external #}
Your event is now fully booked and our finance department will be contact to arrange payment.
{% endif %}
TEC PA & Lighting TEC PA & Lighting

View File

@@ -1,130 +1,99 @@
{% extends 'base_client.html' %} {% extends 'eventauthorisation.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% load static %}
{% block js %} {% block js %}
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
$('form').on('submit', function () {
$('#loading-modal').modal({
backdrop: 'static',
show: true
});
});
</script>
{% endblock %} {% endblock %}
{% block title %} {% block authorisation %}
{% if event.is_rig %}N{{ event.pk|stringformat:"05d" }}{% else %}{{ event.pk }}{% endif %} | {{ event.name }} <div class="row">
{% endblock %} <div class="col-sm-12">
<div class="card border-primary">
<div class="card-header" id="eventauth">Event Authorisation</div>
{% block content %} <div class="card-body">
<div class="page-header my-3"> <form class=" itemised_form" role="form" method="POST" action="#eventauth">
<h1> {% csrf_token %}
{% if event.is_rig %}N{{ event.pk|stringformat:"05d" }}{% else %}{{ event.pk }}{% endif %} {% include 'form_errors.html' %}
| {{ event.name }} {% if event.dry_hire %}<span class="badge">Dry Hire</span>{% endif %} <div class="row">
</h1> <div class="col-sm-12">
</div> <p>
I agree that I am authorised to approve this event. I agree that I am the
<strong>President/Treasurer or account holder</strong> of the hirer, or that I
have the written permission of the
<strong>President/Treasurer or account holder</strong> of the hirer stating that
I can authorise this event.
</p>
</div>
<div class="row"> <div class="col-sm-12 col-md-6">
{% include 'client_eventdetails.html' %} <div class="col-sm-12 form-group form-row" data-toggle="tooltip"
</div> title="Your name as the person authorising the event.">
<label for="{{ form.name.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.name.label }}</label>
<div class="row"> <div class="col-sm-8">
<div class="col-sm-12"> {% render_field form.name class+="form-control" %}
{% with object=event %}
{% include 'item_table.html' %}
{% endwith %}
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="card border-primary">
<div class="card-header" id="eventauth">Event Authorisation</div>
<div class="card-body">
<form class="form-horizontal itemised_form" role="form" method="POST" action="#eventauth">
{% csrf_token %}
{% include 'form_errors.html' %}
<div class="row">
<div class="col-sm-12">
<p>
I agree that I am authorised to approve this event. I agree that I am the
<strong>President/Treasurer or account holder</strong> of the hirer, or that I
have the written permission of the
<strong>President/Treasurer or account holder</strong> of the hirer stating that
I can authorise this event.
</p>
</div>
<div class="col-sm-12 col-md-6">
<div class="col-sm-12 form-group" data-toggle="tooltip"
title="Your name as the person authorising the event.">
<label for="{{ form.name.id_for_label }}"
class="col-sm-4 control-label">{{ form.name.label }}</label>
<div class="col-sm-8">
{% render_field form.name class+="form-control" %}
</div>
</div>
<div class="col-sm-12 form-group" data-toggle="tooltip"
title="Your Student ID or Staff username as the person authorising the event.">
<label for="{{ form.uni_id.id_for_label }}"
class="col-sm-4 control-label">{{ form.uni_id.label }}</label>
<div class="col-sm-8">
{% render_field form.uni_id class+="form-control" %}
</div>
</div> </div>
</div> </div>
<div class="col-sm-12 col-md-6"> <div class="col-sm-12 form-group form-row" data-toggle="tooltip"
<div class="col-sm-12 form-group" data-toggle="tooltip" title="Your Student ID or Staff username as the person authorising the event.">
title="The Students' Union account code you wish this event to be charged to."> <label for="{{ form.uni_id.id_for_label }}"
<label for="{{ form.account_code.id_for_label }}" class="col-sm-4 col-form-label">{{ form.uni_id.label }}</label>
class="col-sm-4 control-label">{{ form.account_code.label }}</label> <div class="col-sm-8">
<div class="col-sm-8"> {% render_field form.uni_id class+="form-control" %}
{% render_field form.account_code class+="form-control" %}
</div>
</div> </div>
</div>
</div>
<div class="col-sm-12 form-group" data-toggle="tooltip" <div class="col-sm-12 col-md-6">
title="The full amount chargable for this event as displayed above, including VAT."> <div class="col-sm-12 form-group form-row" data-toggle="tooltip"
<label for="{{ form.amount.id_for_label }}" title="The Students' Union account code you wish this event to be charged to.">
class="col-sm-4 control-label">{{ form.amount.label }}</label> <label for="{{ form.account_code.id_for_label }}"
<div class="col-sm-8"> class="col-sm-4 col-form-label">{{ form.account_code.label }}</label>
{% render_field form.amount class+="form-control" %} <div class="col-sm-8">
</div> {% render_field form.account_code class+="form-control" %}
</div> </div>
</div> </div>
<div class="col-sm-12"> <div class="col-sm-12 form-group form-row" data-toggle="tooltip"
<div class="col-sm-12 col-md-8 form-group"> title="The full amount chargable for this event as displayed above, including VAT.">
<div class="col-sm-offset-4 col-sm-8 col-md-offset-3" data-toggle="tooltip" <label for="{{ form.amount.id_for_label }}"
title="In order to book an event you must agree to the TEC Terms of Hire."> class="col-sm-4 col-form-label">{{ form.amount.label }}</label>
<div class="checkbox"> <div class="col-sm-8">
<label> <div class="input-group mb-3">
{% render_field form.tos %} I have read and agree to the TEC <div class="input-group-prepend">
<a href="{{ tos_url }}">Terms of Hire</a>. E&OE. <span class="input-group-text">£</span>
</label> </div>
</div> {% render_field form.amount class+="form-control" %}
</div>
</div>
<div class="col-sm-12 col-md-4 text-right">
<div class="btn-group">
<button class="btn btn-primary btn-lg" type="submit">Authorise</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form>
</div> <div class="col-sm-12">
<div class="col-sm-12 col-md-8 form-group">
<div class="col-sm-offset-4 col-sm-8 col-md-offset-3" data-toggle="tooltip"
title="In order to book an event you must agree to the TEC Terms of Hire.">
<div class="checkbox">
<label>
{% render_field form.tos %} I have read and agree to the TEC
<a href="{{ tos_url }}">Terms of Hire</a>. E&OE.
</label>
</div>
</div>
</div>
<div class="text-right">
<div class="btn-group">
<button class="btn btn-primary btn-lg" type="submit">Authorise</button>
</div>
</div>
</div>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@@ -1,64 +1,42 @@
{% extends 'base_client.html' %} {% extends 'eventauthorisation.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block title %} {% block js %}
{% if event.is_rig %}N{{ event.pk|stringformat:"05d" }}{% else %}{{ event.pk }}{% endif %} | {{ event.name }}
{% endblock %} {% endblock %}
{% block content %} {% block authorisation %}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<h1> <div class="card">
{% if event.is_rig %}N{{ event.pk|stringformat:"05d" }}{% else %}{{ event.pk }}{% endif %} <div class="card-header">Event Authorisation</div>
| {{ event.name }} {% if event.dry_hire %}<span class="badge">Dry Hire</span>{% endif %}
</h1>
</div>
</div>
{% include 'client_eventdetails.html' %} <div class="card-body">
<div class="row">
<div class="col-sm-12 col-md-6">
<dl class="dl row">
<dt class="col-6">Name</dt>
<dd class="col-6">{{ object.name }}</dd>
<div class="row"> <dt class="col-6">Email</dt>
<div class="col-sm-12"> <dd class="col-6">{{ object.email }}</dd>
{% with object=event %}
{% include 'item_table.html' %}
{% endwith %}
</div>
</div>
<div class="row"> <dt class="col-6">University ID</dt>
<div class="col-sm-12"> <dd class="col-6">{{ object.uni_id }}</dd>
<div class="card card-default"> </dl>
<div class="card-header">Event Authorisation</div> </div>
<div class="card-body"> <div class="col-sm-12 col-md-6">
<div class="row"> <dl class="dl row">
<div class="col-sm-12 col-md-6"> <dt class="col-6">Account code</dt>
<dl class="dl-horizontal"> <dd class="col-6">{{ object.account_code }}</dd>
<dt>Name</dt>
<dd>{{ object.name }}</dd>
<dt>Email</dt> <dt class="col-6">Authorised amount</dt>
<dd>{{ object.email }}</dd> <dd class="col-6">£ {{ object.amount|floatformat:2 }}</dd>
</dl>
{% if internal %}
<dt>University ID</dt>
<dd>{{ object.uni_id }}</dd>
{% endif %}
</dl>
</div>
<div class="col-sm-12 col-md-6">
<dl class="dl-horizontal">
<dt>Account code</dt>
<dd>{{ object.account_code }}</dd>
<dt>Authorised amount</dt>
<dd>£ {{ object.amount|floatformat:2 }}</dd>
</dl>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,50 @@
{% extends 'base_rigs.html' %}
{% load paginator from filters %}
{% load button from filters %}
{% block content %}
<div class="table-responsive">
<table class="table mb-0">
<thead>
<tr>
<th scope="col">Event</th>
<th scope="col">Dates</th>
<th scope="col">RA</th>
<th scope="col">Checklists</th>
</tr>
</thead>
<tbody>
{% for event in object_list %}
<tr id="event_row">
<th scope="row" id="event_number"><a href="{% url 'event_detail' event.pk %}">{{ event }}</a></th>
<!--Dates-->
<td id="event_dates">
<span><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></span>
{% if event.end_date and event.end_date != event.start_date %}
<br><span><strong>{{ event.end_date|date:"D d/m/Y" }}</strong></span>
{% endif %}
</td>
<td>{% include 'partials/hs_status.html' with event=event object=event.riskassessment view='ra_detail' edit='ra_edit' create='event_ra' review='ra_review' perm=perms.RIGS.review_riskassessment %}</td>
<td>
{% for checklist in event.checklists.all %}
{% include 'partials/hs_status.html' with event=event object=checklist view='ec_detail' edit='ec_edit' create='event_ec' review='ec_review' perm=perms.RIGS.review_eventchecklist %}
<br>
{% endfor %}
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="d-none d-sm-inline">Create</span></a>
</td>
</tr>
{% empty %}
<tr class="bg-warning text-dark">
<td colspan="6">No events found</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if is_paginated %}
<div class="row justify-content-center">
{% paginator %}
</div>
{% endif %}
</div>
{% endblock %}

View File

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

View File

@@ -1,35 +1,20 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% block title %}Delete payment on invoice {{ object.invoice.pk }}{% endblock %} {% block title %}Delete Invoice {{ object.invoice.pk }}{% endblock %}
{% block content %} {% block content %}
<div class="col-sm-offset-2 col-sm-8"> <div class="alert alert-danger" role="alert">
<div class="alert alert-danger" role="alert"> <h2>Delete Invoice {{ object.pk }}</h2>
<h2>Delete invoice {{ object.pk }}</h2>
<p>Are you sure you wish to delete invoice {{ object.pk }}?</p> <p>Are you sure you wish to delete invoice {{ object.pk }}?</p>
<p class="text-center"><strong>This action cannot be undone!</strong></p> <div class="text-right">
<form action="{{ action_link }}" method="post">
<div class="row"> {% csrf_token %}
<div class="col-sm-12"> <input type="hidden" name="next" value="{% url 'invoice_list' %}"/>
<form action="{{ action_link }}" method="post">{% csrf_token %} <input type="submit" value="Yes" class="btn btn-danger col-sm-1"/>
<input type="hidden" name="next" value="{% url 'invoice_list' %}"/> <a href="{% url 'invoice_detail' object.pk %}" class="btn btn-success col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a> </form>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<input type="submit" value="Yes" class="btn btn-danger col-sm-1"/>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
</form>
</div>
</div>
</div>
</div> </div>
{% endblock %} </div>
{% endblock %}

View File

@@ -1,27 +1,20 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% load button from filters %}
{% block title %}Invoice {{ object.pk }}{% endblock %}
{% block content %} {% block content %}
<div class="col-sm-12"> <div class="col-sm-12">
<div class="row"> <div class="row justify-content-end py-3">
<div class="col-sm-8">
<h2>Invoice {{ object.pk }} ({{ object.invoice_date|date:"d/m/Y"}})</h2>
</div>
<div class="col-sm-4 text-right"> <div class="col-sm-4 text-right">
<div class="btn-group btn-page"> <div class="btn-group btn-page">
<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="hidden-xs">Delete</span> class="d-none d-sm-inline">Delete</span>
</a> </a>
<a href="{% url 'invoice_void' object.pk %}" class="btn btn-warning" title="Void Invoice"> <a href="{% url 'invoice_void' object.pk %}" class="btn btn-warning" title="Void Invoice">
<span class="fas fa-ban"></span> <span <span class="fas fa-ban"></span> <span
class="hidden-xs">Void</span> class="d-none d-sm-inline">Void</span>
</a> </a>
<a href="{% url 'invoice_print' object.pk %}" target="_blank" title="Print Invoice" class="btn btn-primary"><span {% button 'print' url='invoice_print' pk=object.pk %}
class="fas fa-print"></span> <span
class="hidden-xs">Print</span></a>
</div> </div>
</div> </div>
</div> </div>
@@ -53,13 +46,13 @@
{% endif %} {% endif %}
</div> </div>
<div class="row"> <div class="row py-4">
<div class="col-sm-6"> <div class="col-sm-6">
<div class="card card-default"> <div class="card card-default">
<div class="card-body"> <div class="card-body">
<div class="pull-right"> <div class="text-right py-3">
<a href="{% url 'payment_create' %}?invoice={{ object.pk }}" <a href="{% url 'payment_create' %}?invoice={{ object.pk }}"
class="btn btn-default modal-href" class="btn btn-success modal-href"
data-target="#{{ form.person.id_for_label }}"> data-target="#{{ form.person.id_for_label }}">
<span class="fas fa-plus"></span> Add <span class="fas fa-plus"></span> Add
</a> </a>
@@ -96,14 +89,14 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="card card-default"> <div class="card">
<div class="card-body"> {% with object.event as object %}
{% with object.event as object %} {% include 'item_table.html' %}
{% include 'item_table.html' %} {% endwith %}
{% endwith %}
</div>
</div> </div>
</div> </div>
</div>
<div class="col-12 text-right">
{% include 'partials/last_edited.html' with target="invoice_history" %} {% include 'partials/last_edited.html' with target="invoice_history" %}
</div> </div>
</div> </div>

View File

@@ -1,17 +1,15 @@
{% extends 'base_rigs.html' %} {% extends 'base_rigs.html' %}
{% load paginator from filters %} {% load paginator from filters %}
{% load button from filters %}
{% load static %} {% load static %}
{% block title %}Invoices{% endblock %}
{% block content %} {% block content %}
<div class="col-sm-12"> <div class="col-sm-12">
<h2>{% block heading %}Invoices{% endblock %}</h2> {{ description }}
{% block description %}{% endblock %}
{% block search %}{% endblock %} {% block search %}{% endblock %}
<div class="table-responsive col-sm-12"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover table-sm">
<thead class="thead-dark"> <thead>
<tr> <tr>
<th scope="col">Invoice #</th> <th scope="col">Invoice #</th>
<th scope="col">Event</th> <th scope="col">Event</th>
@@ -25,7 +23,7 @@
<tbody> <tbody>
{% for invoice in invoice_list %} {% for invoice in invoice_list %}
<tr class="table-{% if invoice.is_closed %}success{% else %}warning{% endif %}"> <tr class="table-{% if invoice.is_closed %}success{% else %}warning{% endif %}">
<th scope="row">{{ invoice.pk }}<br> <th scope="row">{{ invoice.display_id }}<br>
<span class="text-muted">{% if invoice.void %}(VOID){% elif invoice.is_closed %}(PAID){% else %}(O/S){% endif %}</span></th> <span class="text-muted">{% if invoice.void %}(VOID){% elif invoice.is_closed %}(PAID){% else %}(O/S){% endif %}</span></th>
<td><a href="{% url 'event_detail' invoice.event.pk %}">N{{ invoice.event.pk|stringformat:"05d" }}</a>: {{ invoice.event.name }} <br> <td><a href="{% url 'event_detail' invoice.event.pk %}">N{{ invoice.event.pk|stringformat:"05d" }}</a>: {{ invoice.event.name }} <br>
<span class="text-muted">{{ invoice.event.get_status_display }}{% if not invoice.event.mic %}, No MIC{% endif %} <span class="text-muted">{{ invoice.event.get_status_display }}{% if not invoice.event.mic %}, No MIC{% endif %}
@@ -51,9 +49,7 @@
{% endif %} {% endif %}
</td> </td>
<td class="text-right"> <td class="text-right">
<a href="{% url 'invoice_detail' invoice.pk %}" class="btn btn-primary"> {% button 'view' url='invoice_detail' pk=invoice.pk %}
<i class="fas fa-edit"></i>
</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -1,13 +0,0 @@
{% extends 'invoice_list.html' %}
{% block title %}
Outstanding Invoices
{% endblock %}
{% block heading %}
Outstanding Invoices ({{ count }} Events, £{{ total|floatformat:2 }})
{% endblock %}
{% block description %}
<p>Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger</p>
{% endblock %}

View File

@@ -1,24 +1,7 @@
{% extends 'invoice_list.html' %} {% extends 'invoice_list.html' %}
{% block title %}
Invoice Archive
{% endblock %}
{% block heading %}
All Invoices
{% endblock %}
{% block description %}
<p>This page displays all invoices: outstanding, paid, and void</p>
{% endblock %}
{% block search %} {% block search %}
<div class="col-sm-3 col-sm-offset-9"> <div class="py-3">
<form class="form form-horizontal col-sm-12"> {% include 'partials/list_search.html' %}
<div class="form-group">
<input type="search" name="q" placeholder="Search" value="{{ request.GET.q }}"
class="form-control"/>
</div>
</form>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -2,8 +2,6 @@
{% load paginator from filters %} {% load paginator from filters %}
{% load static %} {% load static %}
{% block title %}Events for Invoice{% endblock %}
{% block js %} {% block js %}
<script src="{% static "js/tooltip.js" %}"></script> <script src="{% static "js/tooltip.js" %}"></script>
<script> <script>
@@ -15,11 +13,10 @@
{% block content %} {% block content %}
<div class="col-sm-12"> <div class="col-sm-12">
<h2>Events for Invoice ({{count}} Events, £{{ total|floatformat:2 }})</h2>
<p>These events have happened, but paperwork has not yet been sent to treasury</p> <p>These events have happened, but paperwork has not yet been sent to treasury</p>
<div class="table-responsive col-sm-12"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover">
<thead class="thead-dark"> <thead>
<tr> <tr>
<th scope="col">Event #</th> <th scope="col">Event #</th>
<th scope="col">Start Date</th> <th scope="col">Start Date</th>
@@ -32,8 +29,8 @@
</thead> </thead>
<tbody> <tbody>
{% for event in object_list %} {% for event in object_list %}
<tr {% include 'partials/event_table_colour.html' %}> <tr class="{{event.status_color}}">
<th scope="row"><a href="{% url 'event_detail' event.pk %}">N{{ event.pk|stringformat:"05d" }}</a><br> <th scope="row"><a href="{% url 'event_detail' event.pk %}">{{ event.display_id }}</a><br>
<span class="text-muted">{{ event.get_status_display }}</span></th> <span class="text-muted">{{ event.get_status_display }}</span></th>
<td>{{ event.start_date }}</td> <td>{{ event.start_date }}</td>
<td> <td>
@@ -69,12 +66,15 @@
{% endif %} {% endif %}
</td> </td>
<td class="text-right"> <td class="text-right">
<a href="{% url 'invoice_event' event.pk %}" <div class="btn-group">
class="btn btn-primary" <a href="{% url 'invoice_event' event.pk %}"
data-toggle="tooltip" class="btn btn-primary">
title="'Invoice' this event - click this when paperwork has been sent to treasury"> <span class="fas fa-pound-sign"></span> Create Invoice
<i class="fas fa-pound-sign"></i> Paperwork Sent </a>
</a> <a href="{% url 'invoice_event_void' event.pk %}"
class="btn btn-warning">& Void
</a>
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -3,18 +3,21 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h4>{{ object.name|default:"New Event" }}</h4> <h4>{{ object.name|default:"New Event" }}</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div> </div>
<form id="item-form"> <form id="item-form">
<div class="modal-body"> <div class="modal-body">
<div class="form-group form-row"> <div class="form-group form-row">
<label for="item_name" class="col-sm-2 control-label">Item Name</label> <label for="item_name" class="col-sm-2 col-form-label">Item Name</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" placeholder="Item Name" class="form-control" required maxlength="255" <input type="text" placeholder="Item Name" class="form-control" required maxlength="255"
id="item_name"/> id="item_name"/>
</div> </div>
</div> </div>
<div class="form-group form-row"> <div class="form-group form-row">
<label for="item_description" class="col-sm-2 control-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"
id="item_description" rows="8"></textarea> id="item_description" rows="8"></textarea>
@@ -23,7 +26,7 @@
<div class="form-row"> <div class="form-row">
<div class="col-sm-6"> <div class="col-sm-6">
<div class="form-group form-row"> <div class="form-group form-row">
<label for="item_quantity" class="col-sm-4 control-label">Quantity</label> <label for="item_quantity" class="col-sm-4 col-form-label">Quantity</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" placeholder="Quantity" class="form-control" required <input type="number" placeholder="Quantity" class="form-control" required
min="1" id="item_quantity"/> min="1" id="item_quantity"/>
@@ -32,7 +35,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="form-group form-row"> <div class="form-group form-row">
<label for="item_cost" class="col-sm-4 control-label">Cost</label> <label for="item_cost" class="col-sm-4 col-form-label">Cost</label>
<div class="col-sm-8"> <div class="col-sm-8">
<div class="input-group"> <div class="input-group">

View File

@@ -15,12 +15,12 @@
{% 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">
<button type="button" class="item-edit btn btn-xs btn-warning" <button type="button" class="item-edit btn btn-sm btn-warning"
data-pk="{{item.pk}}" data-pk="{{item.pk}}"
data-toggle="modal" data-target="#itemModal"> data-toggle="modal" data-target="#itemModal">
<span class="fas fa-edit"></span> <span class="fas fa-edit"></span>
</button> </button>
<button type="button" class="item-delete btn btn-xs btn-danger" <button type="button" class="item-delete btn btn-sm btn-danger"
data-pk="{{item.pk}}"> data-pk="{{item.pk}}">
<span class="fas fa-times-circle"></span> <span class="fas fa-times-circle"></span>
</button> </button>

View File

@@ -12,7 +12,7 @@
{% endif %} {% endif %}
{% if edit %} {% if edit %}
<th scope="col" class="text-right align-self-start"> <th scope="col" class="text-right align-self-start">
<button type="button" class="btn btn-success btn-xs item-add" <button type="button" class="btn btn-success btn-sm item-add"
data-toggle="modal" data-toggle="modal"
data-target="#itemModal"> data-target="#itemModal">
<i class="fas fa-plus"></i> Add Item <i class="fas fa-plus"></i> Add Item
@@ -26,7 +26,7 @@
{% include 'item_row.html' %} {% include 'item_row.html' %}
{% endfor %} {% endfor %}
</tbody> </tbody>
{% if perms.RIGS.view_event %} {% if auth or perms.RIGS.view_event %}
<tfoot> <tfoot>
<tr> <tr>
<td rowspan="3" colspan="2"></td> <td rowspan="3" colspan="2"></td>
@@ -65,12 +65,12 @@
{% 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">
<button type="button" class="item-edit btn btn-xs btn-warning" <button type="button" class="item-edit btn btn-sm btn-warning"
data-pk="{{item.pk}}" data-pk="{{item.pk}}"
data-toggle="modal" data-target="#itemModal"> data-toggle="modal" data-target="#itemModal">
<span class="fas fa-edit"></span> <span class="fas fa-edit"></span>
</button> </button>
<button type="button" class="item-delete btn btn-xs btn-danger" <button type="button" class="item-delete btn btn-sm btn-danger"
data-pk="{{item.pk}}"> data-pk="{{item.pk}}">
<span class="fas fa-times-circle"></span> <span class="fas fa-times-circle"></span>
</button> </button>

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