Compare commits

..

84 Commits

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

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

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

* Improved MD support.

Add some styling for images in MD

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

* Add processing for <ul> in RML

* Add OL processing to RML

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

* Remove rml formatting in event_detail

* Improve handling of code blocks in RML

* Add MD to rigboard

Reduce MD title sizes as they were offensively large

* Add parsing of markdown when editing event items

* Improved list handling in RML

* Add tests for markdown support.

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

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

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

* Add failing test for markdown processing none

* Fix for failing test in e0d56e

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

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

Pass tests in ef3de607c3

* Enable GH flavour linebreaks in JS rendered markdown

* Made RML bullets pretty :)

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

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

* FIX: Re-enable markdown on paperwork

Strikethrough is broken in all sorts of places for whatever reason

* FEAT: Markdown support on asset comments

* FIX: Prevent js injection through markdown fields

* Initial fixes

* Basic dark theme for simplemde

* Swap to locally delivered SimpleMDE

* Region for selenium testing of SimpleMDE

Bleh, Javascript all around

* Tests passing!

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

* Pep8 fixes

* Fallback for null HCapatcha sitekey

I.e. when we're on a branch

* Fix item description print being broken

* Actually fix sitekey problem

* Fixes for using markdown in asset comments

* Properly initialise markdown on asset comments

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-08 12:48:44 +01:00
2fdb2f260f FEAT: Add ability to generate label images for cable assets
To come SoonTM: ability to generate a A4 page of labels at once
2021-09-07 14:54:01 +01:00
6de3cb5d8c Allow filling out of electrical checks for large events 2021-09-02 12:11:05 +01:00
7c38af66f6 May fix windows/chrome RA name chooser issue
No idea tbh
2021-08-31 19:47:49 +01:00
f1a624ec8f Improve RA detail layout slightly 2021-08-31 19:39:24 +01:00
ab01beb2cd Add title links to ra/ec detail 2021-08-31 19:33:03 +01:00
11636809ca Add link to subhire insurance form on event detail 2021-08-19 15:48:56 +01:00
d7458f6366 Account for null power MICs in event checklist detail 2021-08-16 20:28:30 +01:00
febf9cf3ed curses! 2021-08-05 12:07:23 +01:00
3322a5ddf8 Add badge to nav for number of waiting invoices
Might slightly help us stop leaving them waiting for far too long...
2021-08-05 11:37:10 +01:00
dependabot[bot]
673bee4215 Build(deps): Bump django from 3.1.8 to 3.1.12 (#436)
Bumps [django](https://github.com/django/django) from 3.1.8 to 3.1.12.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.8...3.1.12)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-29 17:17:19 +01:00
dependabot[bot]
bab31107f7 Build(deps): Bump pillow from 8.1.2 to 8.2.0 (#434)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.1.2 to 8.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.1.2...8.2.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-29 17:04:03 +01:00
dependabot[bot]
2d8473b698 Build(deps): Bump urllib3 from 1.26.4 to 1.26.5 (#432)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.4 to 1.26.5.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.4...1.26.5)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-29 16:48:50 +01:00
d81ecd9015 Minor fixes 2021-06-01 15:43:09 +01:00
b42c583897 FIX: Update detail test to match template change 2021-05-29 21:34:30 +01:00
57e966826e Redesign invoice detail page
Closes #431
2021-05-29 21:11:42 +01:00
6a5de4a9d6 Format all dates in event table the same way
Why did I think the old way was a good idea!
2021-05-19 11:17:51 +01:00
56bbf4c17c FIX #426: Override autofill styles in dark mode
Not super pretty, but you can at least read it!
2021-04-19 16:09:55 +01:00
dependabot[bot]
698f0be281 Build(deps): Bump django-debug-toolbar from 3.2 to 3.2.1 (#430)
Bumps [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar) from 3.2 to 3.2.1.
- [Release notes](https://github.com/jazzband/django-debug-toolbar/releases)
- [Changelog](https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst)
- [Commits](https://github.com/jazzband/django-debug-toolbar/compare/3.2...3.2.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 16:09:40 +01:00
dependabot[bot]
483f06e96f Build(deps): Bump django from 3.1.7 to 3.1.8 (#429)
Bumps [django](https://github.com/django/django) from 3.1.7 to 3.1.8.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.7...3.1.8)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 15:42:51 +01:00
dependabot[bot]
22193f3c39 Build(deps): Bump urllib3 from 1.26.3 to 1.26.4 (#428)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.3 to 1.26.4.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.3...1.26.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-15 16:45:31 +01:00
dependabot[bot]
59b63fe7aa Build(deps): Bump lxml from 4.6.2 to 4.6.3 (#427)
Bumps [lxml](https://github.com/lxml/lxml) from 4.6.2 to 4.6.3.
- [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.6.2...lxml-4.6.3)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-08 20:11:26 +01:00
5976ce9ea2 FIX Event form not displaying properly on creation error
Turns out that bit of code was needed ya goof
2021-04-08 19:32:28 +01:00
780d05e27c FIX: Rewrite event form hide/show logic.
Should be much more readable now. Closes #421
2021-03-31 18:46:16 +01:00
8cfa4bd79d Oh No (#425) 2021-03-25 14:27:14 +00:00
36f83ee59b Might fix CI 2021-03-15 12:18:30 +00:00
6d768832f4 Navbar layout wrangling 2021-03-15 11:59:37 +00:00
38da8642fa Add a bit of left/right padding to icons by default 2021-03-08 11:28:39 +00:00
f75e1d5bfc Use user-slash instead of (badly kerned) exclamation in Rigboard 2021-03-03 13:24:41 +00:00
3f959f8d56 Fix fix cabletype migration 2021-03-02 12:36:17 +00:00
b63a01120b Fix migrations 2021-03-02 12:15:56 +00:00
911336ceec More optimisation and cleanup (#420) 2021-03-02 11:29:57 +00:00
2bf0175786 Toolchain/Dependency Upgrade (#418)
* Upgrade to heroku-20 stack

* Move some gulp deps to dev rather than prod

* npm upgrade

* Fix audit time check in asset audit test

* Attempt at parallelising tests where possible

* Add basic calendar button test

Mainly to pickup on FullCalendar loading errors

* Upgrade python deps

* Tends to help if I push valid yaml

* You valid now?

* Fix whoops in requirements.txt

* Change python ver

* Define service in coveralls task

* Run parallelised RIGS tests as one matrix job

* Update python version in tests

* Cache python dependencies

Should majorly speedup parallelillelelised testing

* Purge old vagrant config

* No Ruby compass bodge, no need for rubocop!

* Purge old .idea config

* Switch to gh-a artifact uploading instead of imgur 'hack'

For test failure screenshots. Happy now @mattysmith22? ;p

* Oops, remove unused import

* Exclude tests from the coverage stats

Seems to be artifically deflating our stats

* Refactor asset audit tests with better selectors

Also fixed a silly title error with the modal

* Add title checking to the slightly insane assets test

* Fix unauth test to not just immediately pass out

* Upload failure screenshots as individual artifacts not a zip

Turns out I can't unzip things from my phone, which is a pain

* Should fix asset test on CI

* What about this?

* What about this?

Swear I spend my life jiggerypokerying the damn test suite...

* Does this help the coverage be less weird?

* Revert "Does this help the coverage be less weird?"

This reverts commit 39ab9df836.

* 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.

* Bah, codestyle

* Oops, remove obsolete if check

* Fix screenshot uploading on CI (again)

* Try this way of parallel coverage

* Add codeclimate maintainability badge

* Remove some unused gulp dependencies

* Run asset building serverside

* Still helps if I commit valid YAML

* See below

* Different approach to CI dependencies

* Exclude node_modules from codestyle

* Does this work?

* Parallel parallel builds were giving me a headache, try this

* Update codeclimate settings, purge some config files

* Well the YAML was *syntactically* valid....

* Switch back to old coveralls method

* Fix codeclimate config, mark 2

* Attempt to bodge asset test

* Oops, again

Probably bedtime..

* Might fix heroku building

* Attempt #2 at fixing heroku

* Belt and braces approach to coverage

* Github, you need a Actions YAML validator!

* Might fix actions?

* Try ignoring some third party deprecation warnings

* Another go at making coverage show up

* Some template cleanup

* Minor python cleanup

* Import optimisation

* Revert "Minor python cleanup"

This reverts commit 6a4620a2e5.

* Add format arg to coverage command

* Ignore test directories from Heroku slug

* Maybe this works to purge deps postbuild

* Bunch of test refactoring

* Restore signals import, screw you import optimisation

* Further template refactoring

* Add support for running tests with geckodriver, do this on CI

* Screw you codestyle

* Disable firefox tests for now

That was way more errors than I expected

* Run cleanup script from the right location

* Plausibly fix tests

* Helps if I don't delete the pipeline folder prior to collectstatic

* Enable whitenoise

* Can I delete pipeline here?

* Allow seconds difference in assert_times_equal

* Disable codeclimate

* Remove not working rm command

* Maybe this fixes coverage?

* Try different coverage reporter

* Fix search_help to need login

* Made versioning magic a bit less expansive

We have more apps than I thought...

* Fix IDI0T error in Assets URLS

* Refactor 'no access to unauthed' test to cover all of PyRIGS

* Add RAs/Checklists to sample data generator

* Fix some HTML errors in templates

Which apparently only Django's HTML parser cares about, browsers DGAF...

* Port title test to project level

* Fix more HTML

* Fix cable type detail
2021-01-31 04:05:33 +00:00
210 changed files with 15004 additions and 8384 deletions

View File

@@ -1,16 +0,0 @@
version: 2
plugins:
csslint:
enabled: true
duplication:
enabled: true
config:
languages:
- javascript
- python
eslint:
enabled: true
fixme:
enabled: true
radon:
enabled: true

View File

@@ -1,3 +1,5 @@
[run]
plugins = django_coverage_plugin
omit = *migrations*, *tests*
omit = */migrations/*
*/tests/*
*/site-packages/*
*/distutils/*

View File

@@ -10,49 +10,44 @@ 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
python-version: 3.9.1
- uses: actions/cache@v2
id: pcache
with:
path: ${{ env.pythonLocation }}
key: ${{ env.pythonLocation }}-${{ hashFiles('requirements.txt') }}
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
restore-keys: |
${{ runner.os }}-pipenv-
- 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
python -m pip install --upgrade pip pipenv
pipenv install -d
# if: steps.pcache.outputs.cache-hit != 'true'
- name: Cache Static Files
id: static-cache
uses: actions/cache@v2
with:
path: static/
key: static-${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
path: 'pipeline/built_assets'
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
- uses: bahmutov/npm-install@v1
if: steps.static-cache.outputs.cache-hit != 'true'
- run: node node_modules/gulp/bin/gulp build
if: steps.static-cache.outputs.cache-hit != 'true'
- name: Basic Checks
run: |
pycodestyle . --exclude=migrations,node_modules
python manage.py check
python manage.py makemigrations --check --dry-run
pipenv run pycodestyle . --exclude=migrations,node_modules
pipenv run python manage.py check
pipenv run python manage.py makemigrations --check --dry-run
pipenv run python manage.py collectstatic --noinput
- 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
run: pipenv run pytest -n auto -vv --cov
- uses: actions/upload-artifact@v2
if: failure()
with:
@@ -60,4 +55,4 @@ jobs:
path: screenshots/
retention-days: 5
- name: Coveralls
run: coveralls --service=github
run: pipenv run coveralls --service=github

1
.gitignore vendored
View File

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

View File

@@ -1,3 +1,6 @@
*.sqlite3
*.md
**/tests
conftest.py
pytest.ini
Dockerfile

104
Pipfile Normal file
View File

@@ -0,0 +1,104 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
ansicolors = "~=1.1.8"
asgiref = "~=3.3.1"
"backports.tempfile" = "~=1.0"
"backports.weakref" = "~=1.0.post1"
beautifulsoup4 = "~=4.9.3"
Brotli = "~=1.0.9"
cachetools = "~=4.2.1"
certifi = "~=2020.12.5"
chardet = "~=4.0.0"
configparser = "~=5.0.1"
contextlib2 = "~=0.6.0.post1"
cssselect = "~=1.1.0"
cssutils = "~=1.0.2"
dj-database-url = "~=0.5.0"
dj-static = "~=0.0.6"
Django = "~=3.2"
django-debug-toolbar = "~=3.2"
django-filter = "~=2.4.0"
django-ical = "~=1.7.1"
django-recurrence = "~=1.10.3"
django-registration-redux = "~=2.9"
django-reversion = "~=3.0.9"
django-toolbelt = "~=0.0.1"
django-widget-tweaks = "~=1.4.8"
django-htmlmin = "~=0.11.0"
envparse = "~=0.2.0"
gunicorn = "~=20.0.4"
icalendar = "~=4.0.7"
idna = "~=2.10"
lxml = "~=4.7.1"
Markdown = "~=3.3.3"
msgpack = "~=1.0.2"
pep517 = "~=0.9.1"
Pillow = "~=9.0.0"
premailer = "~=3.7.0"
progress = "~=1.5"
psutil = "~=5.8.0"
psycopg2 = "~=2.8.6"
Pygments = "~=2.7.4"
pyparsing = "~=2.4.7"
PyPDF2 = "~=1.26.0"
PyPOM = "~=2.2.0"
python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21"
pytz = "~=2020.5"
reportlab = "~=3.5.59"
requests = "~=2.25.1"
retrying = "~=1.3.3"
simplejson = "~=3.17.2"
six = "~=1.15.0"
soupsieve = "~=2.1"
sqlparse = "~=0.4.2"
static3 = "~=0.7.0"
svg2rlg = "~=0.3"
tini = "~=3.0.1"
tornado = "~=6.1"
urllib3 = "~=1.26.5"
whitenoise = "~=5.2.0"
yolk = "~=0.4.3"
"z3c.rml" = "~=4.1.2"
zipp = "~=3.4.0"
"zope.component" = "~=4.6.2"
"zope.deferredimport" = "~=4.3.1"
"zope.deprecation" = "~=4.4.0"
"zope.event" = "~=4.5.0"
"zope.hookable" = "~=5.0.1"
"zope.interface" = "~=5.2.0"
"zope.proxy" = "~=4.3.5"
"zope.schema" = "~=6.0.1"
sentry-sdk = "*"
diff-match-patch = "*"
python-barcode = "*"
django-hCaptcha = "*"
importlib-metadata = "*"
django-hcaptcha = "*"
[dev-packages]
selenium = "~=3.141.0"
pycodestyle = "*"
coveralls = "*"
django-coverage-plugin = "*"
pytest-cov = "*"
pytest-django = "*"
pluggy = "*"
pytest-splinter = "*"
pytest = "*"
pytest-reverse = "*"
[requires]
python_version = "3.9"
[dev-packages.pytest-xdist]
extras = [ "psutil",]
version = "*"
[dev-packages.PyPOM]
extras = [ "splinter",]
version = "*"

1714
Pipfile.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect
from django.shortcuts import render
@@ -15,11 +16,7 @@ def get_oembed(login_url, request, oembed_view, kwargs):
return resp
def has_oembed(oembed_view, login_url=None):
if not login_url:
from django.conf import settings
login_url = settings.LOGIN_URL
def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
def _dec(view_func):
def _checklogin(request, *args, **kwargs):
if request.user.is_authenticated:

View File

View File

View File

@@ -9,27 +9,21 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
"""
import datetime
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
from pathlib import Path
import secrets
import raven
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from envparse import env
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG', cast=bool, default=True)
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']
@@ -55,6 +49,7 @@ if DEBUG:
# Application definition
INSTALLED_APPS = (
'whitenoise.runserver_nostatic',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@@ -66,18 +61,18 @@ INSTALLED_APPS = (
'users',
'RIGS',
'assets',
'training',
'debug_toolbar',
'registration',
'reversion',
'captcha',
'widget_tweaks',
'raven.contrib.django.raven_compat',
'hcaptcha',
)
MIDDLEWARE = (
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
@@ -86,6 +81,8 @@ MIDDLEWARE = (
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'htmlmin.middleware.HtmlMinifyMiddleware',
'htmlmin.middleware.MarkRequestMiddleware',
)
ROOT_URLCONF = 'PyRIGS.urls'
@@ -93,11 +90,10 @@ ROOT_URLCONF = 'PyRIGS.urls'
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
'NAME': str(BASE_DIR / 'db.sqlite3'),
}
}
@@ -175,9 +171,12 @@ else:
}
}
RAVEN_CONFIG = {
'dsn': env('RAVEN_DSN', default=""),
}
# Error/performance monitoring
sentry_sdk.init(
dsn=env('SENTRY_DSN', default=""),
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
# User system
AUTH_USER_MODEL = 'RIGS.Profile'
@@ -188,12 +187,9 @@ LOGOUT_URL = '/user/logout/'
ACCOUNT_ACTIVATION_DAYS = 7
# reCAPTCHA settings
RECAPTCHA_PUBLIC_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key
RECAPTCHA_PRIVATE_KEY = env('RECAPTCHA_PUBLIC_KEY', default="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
NOCAPTCHA = True
SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error']
# CAPTCHA settings
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001')
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000')
# Email
EMAILER_TEST = False
@@ -230,21 +226,18 @@ USE_TZ = True
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
# Static files (CSS, JavaScript, Images)
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
STATIC_DIRS = (
os.path.join(BASE_DIR, 'static/')
)
STATIC_ROOT = str(BASE_DIR / 'static/')
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'pipeline/built_assets/'),
str(BASE_DIR / 'pipeline/built_assets'),
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates')
BASE_DIR / 'templates'
],
'APP_DIRS': True,
'OPTIONS': {
@@ -267,3 +260,5 @@ USE_GRAVATAR = True
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

View File

@@ -11,33 +11,29 @@ from selenium.webdriver.support.wait import WebDriverWait
from RIGS import models as rigsmodels
from . import pages
from envparse import env
from pytest_django.asserts import assertContains
def create_datetime(year, month, day, hour, min):
def create_datetime(year, month, day, hour, minute):
tz = pytz.timezone(settings.TIME_ZONE)
return tz.localize(datetime(year, month, day, hour, min)).astimezone(pytz.utc)
return tz.localize(datetime(year, month, day, hour, minute)).astimezone(tz)
def create_browser(browser):
if browser == "firefox":
options = webdriver.FirefoxOptions()
options.headless = True
driver = webdriver.Firefox(options=options)
else:
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
options.add_argument("--headless")
if settings.CI:
options.add_argument("--no-sandbox")
driver = webdriver.Chrome(options=options)
def create_browser():
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
options.add_argument("--headless")
if settings.CI:
options.add_argument("--no-sandbox")
driver = webdriver.Chrome(options=options)
return driver
class BaseTest(LiveServerTestCase):
def setUp(self):
super().setUpClass()
self.driver = create_browser(env('BROWSER', default="chrome"))
self.driver = create_browser()
self.wait = WebDriverWait(self.driver, 15)
def tearDown(self):
@@ -56,6 +52,7 @@ class AutoLoginTest(BaseTest):
login_page.login("EventTest", "EventTestPassword")
# FIXME Refactor as a pytest fixture
def screenshot_failure(func):
def wrapper_func(self, *args, **kwargs):
try:
@@ -79,5 +76,30 @@ def screenshot_failure_cls(cls):
return cls
def assert_times_equal(first_time, second_time):
assert first_time.replace(microsecond=0) == second_time.replace(microsecond=0)
def assert_times_almost_equal(first_time, second_time):
assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0)
def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url):
# Test the meta tag is in place
response = client.get(event_url, follow=True, HTTP_HOST='example.com')
assertContains(response, 'application/json+oembed')
assertContains(response, oembed_url)
# Test that the JSON exists
response = client.get(oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assertContains(response, event_embed_url)
# Should also work for non-existant events
response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assertContains(response, alt_event_embed_url)
def login(client, django_user_model):
pwd = 'testuser'
usr = 'TestUser'
user = django_user_model.objects.create_user(username=usr, email="TestUser@test.com", password=pwd,
is_superuser=True,
is_active=True, is_staff=True)
assert client.login(username=usr, password=pwd)
return user

View File

@@ -71,11 +71,12 @@ class BootstrapSelectElement(Region):
self.find_element(*self._deselect_all_locator).click()
def search(self, query):
# self.wait.until(expected_conditions.visibility_of_element_located(self._status_locator))
search_box = self.find_element(*self._search_locator)
self.open()
search_box.clear()
search_box.send_keys(query)
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
def options(self):
@@ -83,7 +84,7 @@ class BootstrapSelectElement(Region):
return [self.BootstrapSelectOption(self, i) for i in options]
def set_option(self, name, selected):
options = list((x for x in self.options if x.name == name))
options = [x for x in self.options if x.name == name]
assert len(options) == 1
options[0].set_selected(selected)
@@ -116,6 +117,15 @@ class TextBox(Region):
self.root.send_keys(value)
class SimpleMDETextArea(Region):
@property
def value(self):
return self.driver.execute_script("return document.querySelector('#' + arguments[0]).nextSibling.nextSibling.CodeMirror.getDoc().getValue();", self.root.get_attribute("id"))
def set_value(self, value):
self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.nextSibling.CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
class CheckBox(Region):
def toggle(self):
self.root.click()

137
PyRIGS/tests/test_unit.py Normal file
View File

@@ -0,0 +1,137 @@
import pytest
from django.core.management import call_command
from django.template.defaultfilters import striptags
from django.urls import URLPattern, URLResolver
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from pytest_django.asserts import assertRedirects, assertContains, assertNotContains
from pytest_django.asserts import assertTemplateUsed, assertInHTML
from PyRIGS import urls
from RIGS.models import Event, Profile
from assets.models import Asset
from django.db import connection
from django.template.defaultfilters import striptags
from django.urls.exceptions import NoReverseMatch
from django.test import TestCase, TransactionTestCase
from django.test.utils import override_settings
def find_urls_recursive(patterns):
urls_to_check = []
for url in patterns:
if isinstance(url, URLResolver):
urls_to_check += find_urls_recursive(url.url_patterns)
elif isinstance(url, URLPattern):
# Skip some things that actually don't need auth (mainly OEmbed JSONs that are essentially just a redirect)
if url.name is not None and url.name != "closemodal" and "json" not in str(url):
urls_to_check.append(url)
return urls_to_check
def get_request_url(url):
pattern = str(url.pattern)
try:
kwargz = {}
if ":pk>" in pattern:
kwargz['pk'] = 1
if ":model>" in pattern:
kwargz['model'] = "event"
return reverse(url.name, kwargs=kwargz)
except NoReverseMatch:
print("Couldn't test url " + pattern)
@pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData',
'deleteSampleData', 'generateSampleTrainingData', 'generate_sample_training_users'])
def test_production_exception(command):
from django.core.management.base import CommandError
with pytest.raises(CommandError, match=".*production"):
call_command(command)
class TestSampleDataGenerator(TestCase):
@override_settings(DEBUG=True)
def test_sample_data(self):
call_command('generateSampleData')
assert Asset.objects.all().count() > 50
assert Event.objects.all().count() > 100
call_command('deleteSampleData')
assert Asset.objects.all().count() == 0
assert Event.objects.all().count() == 0
@override_settings(DEBUG=True)
@pytest.mark.skip(reason="broken")
def test_unauthenticated(client): # Nothing should be available to the unauthenticated
call_command('generateSampleData')
for url in find_urls_recursive(urls.urlpatterns):
request_url = get_request_url(url)
if request_url and 'user' not in request_url: # User module is full of edge cases
response = client.get(request_url, follow=True, HTTP_HOST='example.com')
assertContains(response, 'Login')
if 'application/json+oembed' in response.content.decode():
assertTemplateUsed(response, 'login_redirect.html')
else:
if "embed" in str(url):
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
else:
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
assertRedirects(response, expected_url)
call_command('deleteSampleData')
@override_settings(DEBUG=True)
@pytest.mark.skip(reason="broken")
def test_basic_access(client):
call_command('generateSampleData')
assert client.login(username="basic", password="basic")
url = reverse('asset_list')
response = client.get(url)
# Check edit and duplicate buttons NOT shown in list
assertNotContains(response, 'Edit')
assertNotContains(response,
'Duplicate') # If this line is randomly failing, check the debug toolbar HTML hasn't crept in
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = client.get(url)
assertNotContains(response, 'Purchase Details')
assertNotContains(response, 'View Revision History')
urlz = {'asset_history', 'asset_update', 'asset_duplicate'}
for url_name in urlz:
request_url = reverse(url_name, kwargs={'pk': Asset.objects.first().asset_id})
response = client.get(request_url, follow=True)
assert response.status_code == 403
request_url = reverse('supplier_create')
response = client.get(request_url, follow=True)
assert response.status_code == 403
request_url = reverse('supplier_update', kwargs={'pk': 1})
response = client.get(request_url, follow=True)
assert response.status_code == 403
client.logout()
call_command('deleteSampleData')
@override_settings(DEBUG=True)
@pytest.mark.skip(reason="broken")
def test_keyholder_access(client):
call_command('generateSampleData')
assert client.login(username="keyholder", password="keyholder")
url = reverse('asset_list')
response = client.get(url)
# Check edit and duplicate buttons shown in list
assertContains(response, 'Edit')
assertContains(response, 'Duplicate')
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = client.get(url)
assertContains(response, 'Purchase Details')
assertContains(response, 'View Revision History')
client.logout()
call_command('deleteSampleData')

View File

@@ -3,7 +3,7 @@ from django.conf.urls import include
from django.contrib import admin
from django.contrib.auth.decorators import login_required
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path, re_path
from django.urls import path
from django.views.generic import TemplateView
from PyRIGS import views
@@ -12,6 +12,7 @@ urlpatterns = [
path('', include('versioning.urls')),
path('', include('RIGS.urls')),
path('assets/', include('assets.urls')),
path('training/', include('training.urls')),
path('', login_required(views.Index.as_view()), name='index'),
@@ -22,18 +23,19 @@ urlpatterns = [
name="api_secure"),
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
path('search_help/', views.SearchHelp.as_view(), name='search_help'),
path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'),
path('', include('users.urls')),
path('admin/', admin.site.urls),
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
]
if settings.DEBUG:
urlpatterns += staticfiles_urlpatterns()
import debug_toolbar
urlpatterns = [
re_path(r'^__debug__/', include(debug_toolbar.urls)),
urlpatterns += [
path('__debug__/', include(debug_toolbar.urls)),
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
] + urlpatterns
]

View File

@@ -3,6 +3,7 @@ import operator
from functools import reduce
import simplejson
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.core import serializers
from django.core.exceptions import PermissionDenied
@@ -11,18 +12,18 @@ from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.views import generic
from django.views.decorators.clickjacking import xframe_options_exempt
from RIGS import models
from assets import models as asset_models
from training import models as training_models
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):
class Index(generic.TemplateView): # Displays the current rig count along with a few other bits and pieces
template_name = 'index.html'
def get_context_data(self, **kwargs):
@@ -38,7 +39,8 @@ class SecureAPIRequest(generic.View):
'organisation': models.Organisation,
'profile': models.Profile,
'event': models.Event,
'supplier': asset_models.Supplier
'supplier': asset_models.Supplier,
'training_item': training_models.TrainingItem,
}
perms = {
@@ -47,7 +49,8 @@ class SecureAPIRequest(generic.View):
'organisation': 'RIGS.view_organisation',
'profile': 'RIGS.view_profile',
'event': None,
'supplier': None
'supplier': None,
'training_item': None, # TODO
}
'''
@@ -75,6 +78,9 @@ class SecureAPIRequest(generic.View):
fields = request.GET.get('fields', None)
if fields:
fields = fields.split(",")
filters = request.GET.get('filters', [])
if filters:
filters = filters.split(",")
# Supply data for one record
if pk:
@@ -95,8 +101,13 @@ class SecureAPIRequest(generic.View):
for field in fields:
q = Q(**{field + "__icontains": part})
qs.append(q)
queries.append(reduce(operator.or_, qs))
for f in filters:
q = Q(**{f: True})
queries.append(q)
# Build the data response list
results = []
query = reduce(operator.and_, queries)
@@ -230,15 +241,29 @@ 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):
"""
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.
"""
template_name = 'closemodal.html'
def get_context_data(self, **kwargs):
return {'messages': messages.get_messages(self.request)}
class OEmbedView(generic.View):
def get(self, request, pk=None):
embed_url = reverse(self.url_name, args=[pk])
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
data = {
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
'version': '1.0',
'type': 'rich',
'height': '250'
}
json = simplejson.JSONEncoderForHTML().encode(data)
return HttpResponse(json, content_type="application/json")

View File

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

View File

@@ -14,7 +14,7 @@ from reversion.admin import VersionAdmin
from RIGS import models
from users import forms as user_forms
# Register your models here.
admin.site.register(models.VatRate, VersionAdmin)
admin.site.register(models.Event, VersionAdmin)
admin.site.register(models.EventItem, VersionAdmin)

View File

@@ -10,7 +10,7 @@ from django.http import Http404, HttpResponseRedirect
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_lazy
from django.urls import reverse
from django.views import generic
from z3c.rml import rml2pdf
@@ -24,7 +24,7 @@ class InvoiceIndex(generic.ListView):
template_name = 'invoice_list.html'
def get_context_data(self, **kwargs):
context = super(InvoiceIndex, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
total = 0
for i in context['object_list']:
total += i.balance
@@ -33,20 +33,7 @@ class InvoiceIndex(generic.ListView):
return context
def get_queryset(self):
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
sql = "SELECT * FROM " \
"(SELECT " \
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
"AS sub " \
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
"ORDER BY invoice_date"
query = self.model.objects.raw(sql)
return query
return self.model.objects.outstanding_invoices()
class InvoiceDetail(generic.DetailView):
@@ -54,8 +41,15 @@ class InvoiceDetail(generic.DetailView):
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"))
context = super().get_context_data(**kwargs)
invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date}) "
if self.object.void:
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
elif self.object.is_closed:
context['page_title'] += "<span class='badge badge-success float-right'>PAID</span>"
else:
context['page_title'] += "<span class='badge badge-info float-right'>OUTSTANDING</span>"
return context
@@ -67,12 +61,6 @@ class InvoicePrint(generic.View):
context = {
'object': object,
'fonts': {
'opensans': {
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'invoice': invoice,
'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))
@@ -98,8 +86,8 @@ class InvoiceVoid(generic.View):
object.save()
if object.void:
return HttpResponseRedirect(reverse_lazy('invoice_list'))
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
return HttpResponseRedirect(reverse('invoice_list'))
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': object.pk}))
class InvoiceDelete(generic.DeleteView):
@@ -110,14 +98,14 @@ class InvoiceDelete(generic.DeleteView):
obj = self.get_object()
if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.')
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk}))
return super(InvoiceDelete, self).get(pk)
def post(self, request, pk):
obj = self.get_object()
if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.')
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk}))
return super(InvoiceDelete, self).post(pk)
def get_success_url(self):
@@ -130,7 +118,7 @@ class InvoiceArchive(generic.ListView):
paginate_by = 25
def get_context_data(self, **kwargs):
context = super(InvoiceArchive, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['page_title'] = "Invoice Archive"
context['description'] = "This page displays all invoices: outstanding, paid, and void"
return context
@@ -172,30 +160,14 @@ class InvoiceWaiting(generic.ListView):
def get_context_data(self, **kwargs):
context = super(InvoiceWaiting, self).get_context_data(**kwargs)
total = 0
for obj in self.get_objects():
objects = self.get_queryset()
for obj in objects:
total += obj.sum_total
context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(self.get_objects()), total)
context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(objects), total)
return context
def get_queryset(self):
return self.get_objects()
def get_objects(self):
# @todo find a way to select items
events = self.model.objects.filter(
(
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
) & Q(invoice__isnull=True) & # Has not already been invoiced
Q(is_rig=True) # Is a rig (not non-rig)
).order_by('start_date') \
.select_related('person',
'organisation',
'venue', 'mic') \
.prefetch_related('items')
return events
return self.model.objects.waiting_invoices()
class InvoiceEvent(generic.View):
@@ -216,7 +188,7 @@ class InvoiceEvent(generic.View):
invoice.save()
messages.warning(self.request, 'Invoice voided')
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': invoice.pk}))
class PaymentCreate(generic.CreateView):
@@ -225,7 +197,7 @@ class PaymentCreate(generic.CreateView):
template_name = 'payment_form.html'
def get_initial(self):
initial = super(generic.CreateView, self).get_initial()
initial = super().get_initial()
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
if invoicepk is None:
raise Http404()
@@ -242,7 +214,7 @@ class PaymentCreate(generic.CreateView):
def get_success_url(self):
messages.info(self.request, "location.reload()")
return reverse_lazy('closemodal')
return reverse('closemodal')
class PaymentDelete(generic.DeleteView):

View File

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

View File

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

View File

@@ -0,0 +1,43 @@
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group
from assets import models
from RIGS import models as rigsmodels
from training import models as tmodels
class Command(BaseCommand):
help = 'Deletes testing sample data'
def handle(self, *args, **kwargs):
from django.conf import settings
if not settings.DEBUG:
raise CommandError('You cannot run this command in production')
self.delete_objects(models.AssetCategory)
self.delete_objects(models.AssetStatus)
self.delete_objects(models.Supplier)
self.delete_objects(models.Connector)
self.delete_objects(models.Asset)
self.delete_objects(rigsmodels.VatRate)
self.delete_objects(rigsmodels.Profile)
self.delete_objects(rigsmodels.Person)
self.delete_objects(rigsmodels.Organisation)
self.delete_objects(rigsmodels.Venue)
self.delete_objects(Group)
self.delete_objects(rigsmodels.Event)
self.delete_objects(rigsmodels.EventItem)
self.delete_objects(rigsmodels.Invoice)
self.delete_objects(rigsmodels.Payment)
self.delete_objects(rigsmodels.RiskAssessment)
self.delete_objects(rigsmodels.EventChecklist)
self.delete_objects(tmodels.TrainingCategory)
self.delete_objects(tmodels.TrainingItem)
self.delete_objects(tmodels.TrainingLevel)
self.delete_objects(tmodels.TrainingItemQualification)
self.delete_objects(tmodels.TrainingLevelRequirement)
def delete_objects(self, model):
for obj in model.objects.all():
obj.delete()

View File

@@ -1,11 +1,15 @@
from django.core.management import call_command
from django.core.management.base import BaseCommand
from RIGS import models
class Command(BaseCommand):
help = 'Adds sample data to use for testing'
can_import_settings = True
def handle(self, *args, **options):
call_command('generateSampleUserData')
call_command('generateSampleRIGSData')
call_command('generateSampleAssetsData')
call_command('generateSampleTrainingData')

View File

@@ -4,6 +4,7 @@ import random
from django.contrib.auth.models import Group, Permission
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
from reversion import revisions as reversion
from RIGS import models
@@ -16,13 +17,11 @@ class Command(BaseCommand):
people = []
organisations = []
venues = []
profiles = []
keyholder_group = None
finance_group = None
hs_group = None
events = []
profiles = models.Profile.objects.all()
def handle(self, *args, **options):
print("Generating rigboard data")
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
@@ -33,20 +32,13 @@ class Command(BaseCommand):
with transaction.atomic():
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
self.setup_people()
self.setup_organisations()
self.setup_venues()
self.setup_events()
print("Done generating rigboard data")
self.setupGenericProfiles()
self.setupPeople()
self.setupOrganisations()
self.setupVenues()
self.setupGroups()
self.setupEvents()
self.setupUsefulProfiles()
def setupPeople(self):
def setup_people(self):
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe",
"Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore",
"Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan",
@@ -61,25 +53,25 @@ class Command(BaseCommand):
"Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
reversion.set_user(random.choice(models.Profile.objects.all()))
person = models.Person.objects.create(name=name)
newPerson = models.Person.objects.create(name=name)
if i % 3 == 0:
newPerson.email = "address@person.com"
person.email = "address@person.com"
if i % 5 == 0:
newPerson.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
person.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
newPerson.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
person.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
newPerson.phone = "01234 567894"
person.phone = "01234 567894"
newPerson.save()
self.people.append(newPerson)
person.save()
self.people.append(person)
def setupOrganisations(self):
def setup_organisations(self):
names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars",
"ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc",
"Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp",
@@ -108,27 +100,28 @@ class Command(BaseCommand):
"Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
newOrganisation = models.Organisation.objects.create(name=name)
reversion.set_user(random.choice(models.Profile.objects.all()))
new_organisation = models.Organisation.objects.create(name=name)
if i % 2 == 0:
newOrganisation.has_su_account = True
new_organisation.has_su_account = True
if i % 3 == 0:
newOrganisation.email = "address@organisation.com"
new_organisation.email = "address@organisation.com"
if i % 5 == 0:
newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
new_organisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
new_organisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
newOrganisation.phone = "01234 567894"
new_organisation.phone = "01234 567894"
newOrganisation.save()
self.organisations.append(newOrganisation)
new_organisation.save()
self.organisations.append(new_organisation)
def setupVenues(self):
def setup_venues(self):
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch",
"The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands",
"The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins",
@@ -144,108 +137,27 @@ class Command(BaseCommand):
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
newVenue = models.Venue.objects.create(name=name)
new_venue = models.Venue.objects.create(name=name)
if i % 2 == 0:
newVenue.three_phase_available = True
new_venue.three_phase_available = True
if i % 3 == 0:
newVenue.email = "address@venue.com"
new_venue.email = "address@venue.com"
if i % 5 == 0:
newVenue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
new_venue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
new_venue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
newVenue.phone = "01234 567894"
new_venue.phone = "01234 567894"
newVenue.save()
self.venues.append(newVenue)
new_venue.save()
self.venues.append(new_venue)
def setupGroups(self):
self.keyholder_group = Group.objects.create(name='Keyholders')
self.finance_group = Group.objects.create(name='Finance')
self.hs_group = Group.objects.create(name='H&S')
keyholderPerms = ["add_event", "change_event", "view_event",
"add_eventitem", "change_eventitem", "delete_eventitem",
"add_organisation", "change_organisation", "view_organisation",
"add_person", "change_person", "view_person", "view_profile",
"add_venue", "change_venue", "view_venue",
"add_asset", "change_asset", "delete_asset",
"view_asset", "view_supplier", "change_supplier", "asset_finance",
"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",
"add_payment", "change_payment", "delete_payment"]
hsPerms = keyholderPerms + ["review_riskassessment", "review_eventchecklist"]
for permId in keyholderPerms:
self.keyholder_group.permissions.add(Permission.objects.get(codename=permId))
for permId in financePerms:
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):
names = ["Clara Oswin Oswald", "Rory Williams", "Amy Pond", "River Song", "Martha Jones", "Donna Noble",
"Jack Harkness", "Mickey Smith", "Rose Tyler"]
for i, name in enumerate(names):
newProfile = models.Profile.objects.create(username=name.replace(" ", ""), first_name=name.split(" ")[0],
last_name=name.split(" ")[-1],
email=name.replace(" ", "") + "@example.com",
initials="".join([j[0].upper() for j in name.split()]))
if i % 2 == 0:
newProfile.phone = "01234 567894"
newProfile.save()
self.profiles.append(newProfile)
def setupUsefulProfiles(self):
superUser = models.Profile.objects.create(username="superuser", first_name="Super", last_name="User",
initials="SU",
email="superuser@example.com", is_superuser=True, is_active=True,
is_staff=True)
superUser.set_password('superuser')
superUser.save()
financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User",
initials="FU",
email="financeuser@example.com", is_active=True, is_approved=True)
financeUser.groups.add(self.finance_group)
financeUser.groups.add(self.keyholder_group)
financeUser.set_password('finance')
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",
initials="KU",
email="keyholderuser@example.com", is_active=True, is_approved=True)
keyholderUser.groups.add(self.keyholder_group)
keyholderUser.set_password('keyholder')
keyholderUser.save()
basicUser = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User", initials="BU",
email="basicuser@example.com", is_active=True, is_approved=True)
basicUser.set_password('basic')
basicUser.save()
def setupEvents(self):
def setup_events(self):
names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball",
"Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show",
@@ -256,7 +168,7 @@ class Command(BaseCommand):
notes = ["The client came into the office at some point", "Who knows if this will happen",
"Probably should check this event", "Maybe not happening", "Run away!"]
itemOptions = [
item_options = [
{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2,
'cost': 200.00},
{'name': 'Projector',
@@ -273,7 +185,7 @@ class Command(BaseCommand):
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
dayDelta = -120 # start adding events from 4 months ago
day_delta = -120 # start adding events from 4 months ago
for i in range(150): # Let's add 100 events
with reversion.create_revision():
@@ -281,65 +193,100 @@ class Command(BaseCommand):
name = names[i % len(names)]
startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
dayDelta = dayDelta + random.randint(0, 3)
start_date = datetime.date.today() + datetime.timedelta(days=day_delta)
day_delta = day_delta + random.randint(0, 3)
newEvent = models.Event.objects.create(name=name, start_date=startDate)
new_event = models.Event.objects.create(name=name, start_date=start_date)
if random.randint(0, 2) > 1: # 1 in 3 have a start time
newEvent.start_time = datetime.time(random.randint(15, 20))
new_event.start_time = datetime.time(random.randint(15, 20))
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
newEvent.end_time = datetime.time(random.randint(21, 23))
new_event.end_time = datetime.time(random.randint(21, 23))
elif random.randint(0, 1) > 0: # half of the others finish early the next day
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=1)
newEvent.end_time = datetime.time(random.randint(0, 5))
new_event.end_date = new_event.start_date + datetime.timedelta(days=1)
new_event.end_time = datetime.time(random.randint(0, 5))
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1, 4))
new_event.end_date = new_event.start_date + datetime.timedelta(days=random.randint(1, 4))
if random.randint(0, 6) > 0: # 5 in 6 have MIC
newEvent.mic = random.choice(self.profiles)
new_event.mic = random.choice(self.profiles)
if random.randint(0, 6) > 0: # 5 in 6 have organisation
newEvent.organisation = random.choice(self.organisations)
new_event.organisation = random.choice(self.organisations)
if random.randint(0, 6) > 0: # 5 in 6 have person
newEvent.person = random.choice(self.people)
new_event.person = random.choice(self.people)
if random.randint(0, 6) > 0: # 5 in 6 have venue
newEvent.venue = random.choice(self.venues)
new_event.venue = random.choice(self.venues)
# Could have any status, equally weighted
newEvent.status = random.choice(
new_event.status = random.choice(
[models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
newEvent.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
new_event.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
if random.randint(0, 1) > 0: # 1 in 2 have description
newEvent.description = random.choice(descriptions)
new_event.description = random.choice(descriptions)
if random.randint(0, 1) > 0: # 1 in 2 have notes
newEvent.notes = random.choice(notes)
new_event.notes = random.choice(notes)
newEvent.save()
new_event.save()
# Now add some items
for j in range(random.randint(1, 5)):
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
newItem.save()
item_data = item_options[random.randint(0, len(item_options) - 1)]
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
new_item.save()
while newEvent.sum_total < 0:
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
newItem.save()
while new_event.sum_total < 0:
item_data = item_options[random.randint(0, len(item_options) - 1)]
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
new_item.save()
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
if newEvent.start_date < datetime.date.today(): # think about adding an invoice
if new_event.start_date < datetime.date.today(): # think about adding an invoice
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
newInvoice = models.Invoice.objects.create(event=newEvent)
if newEvent.status is models.Event.CANCELLED: # void cancelled events
newInvoice.void = True
new_invoice = models.Invoice.objects.create(event=new_event)
if new_event.status is models.Event.CANCELLED: # void cancelled events
new_invoice.void = True
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance,
models.Payment.objects.create(invoice=new_invoice, amount=new_invoice.balance,
date=datetime.date.today())
if i == 1 or random.randint(0, 5) > 0: # Event 1 and 1 in 5 have a RA
models.RiskAssessment.objects.create(event=new_event, supervisor_consulted=bool(random.getrandbits(1)),
nonstandard_equipment=bool(random.getrandbits(1)),
nonstandard_use=bool(random.getrandbits(1)),
contractors=bool(random.getrandbits(1)),
other_companies=bool(random.getrandbits(1)),
crew_fatigue=bool(random.getrandbits(1)),
big_power=bool(random.getrandbits(1)),
generators=bool(random.getrandbits(1)),
other_companies_power=bool(random.getrandbits(1)),
nonstandard_equipment_power=bool(random.getrandbits(1)),
multiple_electrical_environments=bool(random.getrandbits(1)),
noise_monitoring=bool(random.getrandbits(1)),
known_venue=bool(random.getrandbits(1)),
safe_loading=bool(random.getrandbits(1)),
safe_storage=bool(random.getrandbits(1)),
area_outside_of_control=bool(random.getrandbits(1)),
barrier_required=bool(random.getrandbits(1)),
nonstandard_emergency_procedure=bool(random.getrandbits(1)),
special_structures=bool(random.getrandbits(1)),
suspended_structures=bool(random.getrandbits(1)),
outside=bool(random.getrandbits(1)))
if i == 0 or random.randint(0, 1) > 0: # Event 1 and 1 in 10 have a Checklist
models.EventChecklist.objects.create(event=new_event, power_mic=random.choice(self.profiles),
safe_parking=bool(random.getrandbits(1)),
safe_packing=bool(random.getrandbits(1)),
exits=bool(random.getrandbits(1)),
trip_hazard=bool(random.getrandbits(1)),
warning_signs=bool(random.getrandbits(1)),
ear_plugs=bool(random.getrandbits(1)),
hs_location="Locked away safely",
extinguishers_location="Somewhere, I forgot",
earthing=bool(random.getrandbits(1)),
pat=bool(random.getrandbits(1)),
date=timezone.now(), venue=random.choice(self.venues))

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
# Generated by Django 2.0.13 on 2020-01-11 18:29
# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
from django.db import migrations
def approve_legacy(apps, schema_editor):
@@ -15,5 +15,5 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(approve_legacy)
migrations.RunPython(approve_legacy, migrations.RunPython.noop)
]

View File

@@ -4,6 +4,7 @@ import RIGS.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import versioning
class Migration(migrations.Migration):
@@ -58,7 +59,7 @@ class Migration(migrations.Migration):
'ordering': ['event'],
'permissions': [('review_eventchecklist', 'Can review Event Checklists')],
},
bases=(models.Model, RIGS.models.RevisionMixin),
bases=(models.Model, versioning.versioning.RevisionMixin),
),
migrations.CreateModel(
name='EventChecklistCrew',
@@ -69,7 +70,7 @@ class Migration(migrations.Migration):
('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),
bases=(models.Model, versioning.versioning.RevisionMixin),
),
migrations.CreateModel(
name='EventChecklistVehicle',
@@ -78,7 +79,7 @@ class Migration(migrations.Migration):
('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),
bases=(models.Model, versioning.versioning.RevisionMixin),
),
migrations.CreateModel(
name='RiskAssessment',
@@ -117,7 +118,7 @@ class Migration(migrations.Migration):
'ordering': ['event'],
'permissions': [('review_riskassessment', 'Can review Risk Assessments')],
},
bases=(models.Model, RIGS.models.RevisionMixin),
bases=(models.Model, versioning.versioning.RevisionMixin),
),
migrations.RemoveField(
model_name='eventcrew',

View File

@@ -0,0 +1,67 @@
# Generated by Django 3.1.7 on 2021-03-02 11:48
from django.db import migrations
def postgres_migration_prep(apps, schema_editor):
model = apps.get_model("RIGS", "Event")
for field in ["auth_request_to", "collector", "description", "notes", "purchase_order"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "EventAuthorisation")
for field in ["account_code", "uni_id"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "EventChecklist")
for field in ["extinguishers_location", "hs_location", "w1_description", "w2_description", "w3_description"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "EventItem")
for field in ["description"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Organisation")
for field in ["address", "email", "notes", "phone"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Payment")
for field in ["method"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Person")
for field in ["address", "email", "notes", "phone"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Profile")
for field in ["phone"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "RiskAssessment")
for field in ["general_notes", "persons_responsible_structures", "power_notes", "rigging_plan", "sound_notes"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Venue")
for field in ["address", "email", "notes", "phone"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0039_auto_20210123_1910'),
]
operations = [
migrations.RunPython(postgres_migration_prep, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,201 @@
# Generated by Django 3.1.7 on 2021-03-02 12:04
import RIGS.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0040_auto_20210302_1148'),
]
operations = [
migrations.RemoveField(
model_name='event',
name='meet_info',
),
migrations.RemoveField(
model_name='event',
name='payment_method',
),
migrations.RemoveField(
model_name='event',
name='payment_received',
),
migrations.AddField(
model_name='profile',
name='dark_theme',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='event',
name='auth_request_to',
field=models.EmailField(blank=True, default='', max_length=254),
),
migrations.AlterField(
model_name='event',
name='collector',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='collected by'),
),
migrations.AlterField(
model_name='event',
name='description',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='event',
name='notes',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='event',
name='purchase_order',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='PO'),
),
migrations.AlterField(
model_name='eventauthorisation',
name='account_code',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.AlterField(
model_name='eventauthorisation',
name='uni_id',
field=models.CharField(blank=True, default='', max_length=10, verbose_name='University ID'),
),
migrations.AlterField(
model_name='eventchecklist',
name='extinguishers_location',
field=models.CharField(blank=True, default='', help_text='Location of fire extinguishers', max_length=255),
),
migrations.AlterField(
model_name='eventchecklist',
name='hs_location',
field=models.CharField(blank=True, default='', help_text='Location of Safety Bag/Box', max_length=255),
),
migrations.AlterField(
model_name='eventchecklist',
name='w1_description',
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
),
migrations.AlterField(
model_name='eventchecklist',
name='w2_description',
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
),
migrations.AlterField(
model_name='eventchecklist',
name='w3_description',
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
),
migrations.AlterField(
model_name='eventitem',
name='description',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='organisation',
name='address',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='organisation',
name='email',
field=models.EmailField(blank=True, default='', max_length=254),
),
migrations.AlterField(
model_name='organisation',
name='notes',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='organisation',
name='phone',
field=models.CharField(blank=True, default='', max_length=15),
),
migrations.AlterField(
model_name='payment',
name='method',
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('SU', 'SU Core'), ('T', 'TEC Adjustment')], default='', max_length=2),
),
migrations.AlterField(
model_name='person',
name='address',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='person',
name='email',
field=models.EmailField(blank=True, default='', max_length=254),
),
migrations.AlterField(
model_name='person',
name='notes',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='person',
name='phone',
field=models.CharField(blank=True, default='', max_length=15),
),
migrations.AlterField(
model_name='profile',
name='api_key',
field=models.CharField(blank=True, default='', editable=False, max_length=40),
),
migrations.AlterField(
model_name='profile',
name='phone',
field=models.CharField(blank=True, default='', max_length=13),
),
migrations.AlterField(
model_name='riskassessment',
name='general_notes',
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
),
migrations.AlterField(
model_name='riskassessment',
name='persons_responsible_structures',
field=models.TextField(blank=True, default='', help_text='Who are the persons on site responsible for their use?'),
),
migrations.AlterField(
model_name='riskassessment',
name='power_notes',
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
),
migrations.AlterField(
model_name='riskassessment',
name='power_plan',
field=models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[RIGS.models.validate_url]),
),
migrations.AlterField(
model_name='riskassessment',
name='rigging_plan',
field=models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[RIGS.models.validate_url]),
),
migrations.AlterField(
model_name='riskassessment',
name='sound_notes',
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
),
migrations.AlterField(
model_name='venue',
name='address',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='venue',
name='email',
field=models.EmailField(blank=True, default='', max_length=254),
),
migrations.AlterField(
model_name='venue',
name='notes',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='venue',
name='phone',
field=models.CharField(blank=True, default='', max_length=15),
),
]

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ 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.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from reversion import revisions as reversion
@@ -20,12 +20,16 @@ from reversion.models import Version
class Profile(AbstractUser):
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
phone = models.CharField(max_length=13, null=True, blank=True)
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
initials = models.CharField(max_length=5, null=True, blank=False)
phone = models.CharField(max_length=13, blank=True, default='')
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
is_approved = models.BooleanField(default=False)
last_emailed = models.DateTimeField(blank=True,
null=True) # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
last_emailed = models.DateTimeField(blank=True, null=True)
dark_theme = models.BooleanField(default=False)
is_supervisor = models.BooleanField(default=False)
reversion_hide = True
@classmethod
def make_api_key(cls):
@@ -51,7 +55,7 @@ class Profile(AbstractUser):
@property
def latest_events(self):
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
@classmethod
def admins(cls):
@@ -64,10 +68,8 @@ class Profile(AbstractUser):
def __str__(self):
return self.name
# TODO move to versioning - currently get import errors with that
class RevisionMixin(object):
class RevisionMixin:
@property
def is_first_version(self):
versions = Version.objects.get_for_object(self)
@@ -97,17 +99,17 @@ class RevisionMixin(object):
version = self.current_version
if version is None:
return None
return "V{0} | R{1}".format(version.pk, version.revision.pk)
return f"V{version.pk} | R{version.revision.pk}"
class Person(models.Model, RevisionMixin):
name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, null=True)
email = models.EmailField(blank=True, null=True)
phone = models.CharField(max_length=15, blank=True, default='')
email = models.EmailField(blank=True, default='')
address = models.TextField(blank=True, null=True)
address = models.TextField(blank=True, default='')
notes = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, default='')
def __str__(self):
string = self.name
@@ -133,17 +135,17 @@ class Person(models.Model, RevisionMixin):
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def get_absolute_url(self):
return reverse_lazy('person_detail', kwargs={'pk': self.pk})
return reverse('person_detail', kwargs={'pk': self.pk})
class Organisation(models.Model, RevisionMixin):
name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, null=True)
email = models.EmailField(blank=True, null=True)
phone = models.CharField(max_length=15, blank=True, default='')
email = models.EmailField(blank=True, default='')
address = models.TextField(blank=True, null=True)
address = models.TextField(blank=True, default='')
notes = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, default='')
union_account = models.BooleanField(default=False)
def __str__(self):
@@ -170,7 +172,7 @@ class Organisation(models.Model, RevisionMixin):
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def get_absolute_url(self):
return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
return reverse('organisation_detail', kwargs={'pk': self.pk})
class VatManager(models.Manager):
@@ -178,7 +180,6 @@ class VatManager(models.Manager):
return self.find_rate(timezone.now())
def find_rate(self, date):
# return self.filter(startAt__lte=date).latest()
try:
return self.filter(start_at__lte=date).latest()
except VatRate.DoesNotExist:
@@ -206,17 +207,17 @@ class VatRate(models.Model, RevisionMixin):
get_latest_by = 'start_at'
def __str__(self):
return self.comment + " " + str(self.start_at) + " @ " + str(self.as_percent) + "%"
return f"{self.comment} {self.start_at} @ {self.as_percent}%"
class Venue(models.Model, RevisionMixin):
name = models.CharField(max_length=255)
phone = models.CharField(max_length=15, blank=True, null=True)
email = models.EmailField(blank=True, null=True)
phone = models.CharField(max_length=15, blank=True, default='')
email = models.EmailField(blank=True, default='')
three_phase_available = models.BooleanField(default=False)
notes = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, default='')
address = models.TextField(blank=True, null=True)
address = models.TextField(blank=True, default='')
def __str__(self):
string = self.name
@@ -229,24 +230,23 @@ class Venue(models.Model, RevisionMixin):
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def get_absolute_url(self):
return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
return reverse('venue_detail', kwargs={'pk': self.pk})
class EventManager(models.Manager):
def current_events(self):
events = self.filter(
(models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False) & ~models.Q(
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=timezone.now().date()) & ~models.Q(
(models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(
status=Event.CANCELLED)) | # Active dry hire
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
models.Q(status=Event.CANCELLED, start_date__gte=timezone.now().date()) # Canceled but not started
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
'organisation',
'venue', 'mic')
models.Q(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
return events
def events_in_bounds(self, start, end):
@@ -269,16 +269,29 @@ class EventManager(models.Manager):
def rig_count(self):
event_count = self.filter(
(models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False,
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False, is_rig=True) & ~models.Q(
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=timezone.now().date(), is_rig=True) & ~models.Q(
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q(
status=Event.CANCELLED)) # Active dry hire
).count()
return event_count
def waiting_invoices(self):
events = self.filter(
(
models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before
) & models.Q(invoice__isnull=True) & # Has not already been invoiced
models.Q(is_rig=True) # Is a rig (not non-rig)
).order_by('start_date') \
.select_related('person', 'organisation', 'venue', 'mic') \
.prefetch_related('items')
return events
@reversion.register(follow=['items'])
class Event(models.Model, RevisionMixin):
@@ -298,8 +311,8 @@ class Event(models.Model, RevisionMixin):
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
description = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, null=True)
description = models.TextField(blank=True, default='')
notes = models.TextField(blank=True, default='')
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
dry_hire = models.BooleanField(default=False)
is_rig = models.BooleanField(default=True)
@@ -313,7 +326,6 @@ class Event(models.Model, RevisionMixin):
end_time = models.TimeField(blank=True, null=True)
access_at = models.DateTimeField(blank=True, null=True)
meet_at = models.DateTimeField(blank=True, null=True)
meet_info = models.CharField(max_length=255, blank=True, null=True)
# Crew management
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
@@ -322,23 +334,24 @@ class Event(models.Model, RevisionMixin):
verbose_name="MIC", on_delete=models.CASCADE)
# Monies
payment_method = models.CharField(max_length=255, blank=True, null=True)
payment_received = models.CharField(max_length=255, blank=True, null=True)
purchase_order = models.CharField(max_length=255, blank=True, null=True, verbose_name='PO')
collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='collected by')
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
# Authorisation request details
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
auth_request_at = models.DateTimeField(null=True, blank=True)
auth_request_to = models.EmailField(null=True, blank=True)
auth_request_to = models.EmailField(blank=True, default='')
@property
def display_id(self):
if self.is_rig:
return str("N%05d" % self.pk)
else:
if self.pk:
if self.is_rig:
return str("N%05d" % self.pk)
return self.pk
return "????"
# Calculated values
"""
EX Vat
@@ -346,7 +359,7 @@ class Event(models.Model, RevisionMixin):
@property
def sum_total(self):
total = EventItem.objects.filter(event=self).aggregate(
total = self.items.aggregate(
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
output_field=models.DecimalField(max_digits=10, decimal_places=2))
)['sum_total']
@@ -360,6 +373,9 @@ class Event(models.Model, RevisionMixin):
@property
def vat(self):
# No VAT is owed on internal transfers
if self.internal:
return 0
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
"""
@@ -456,10 +472,10 @@ class Event(models.Model, RevisionMixin):
objects = EventManager()
def get_absolute_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.pk})
return reverse('event_detail', kwargs={'pk': self.pk})
def __str__(self):
return "{}: {}".format(self.display_id, self.name)
return f"{self.display_id}: {self.name}"
def clean(self):
errdict = {}
@@ -490,7 +506,7 @@ class Event(models.Model, RevisionMixin):
class EventItem(models.Model, RevisionMixin):
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
description = models.TextField(blank=True, default='')
quantity = models.IntegerField()
cost = models.DecimalField(max_digits=10, decimal_places=2)
order = models.IntegerField()
@@ -505,11 +521,11 @@ class EventItem(models.Model, RevisionMixin):
ordering = ['order']
def __str__(self):
return str(self.event.pk) + "." + str(self.order) + ": " + self.event.name + " | " + self.name
return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
@property
def activity_feed_string(self):
return str("item {}".format(self.name))
return f"item {self.name}"
@reversion.register
@@ -517,17 +533,34 @@ class EventAuthorisation(models.Model, RevisionMixin):
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
email = models.EmailField()
name = models.CharField(max_length=255)
uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID")
account_code = models.CharField(max_length=50, blank=True, null=True)
uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID")
account_code = models.CharField(max_length=50, default='', blank=True)
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
def get_absolute_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
return reverse('event_detail', kwargs={'pk': self.event_id})
@property
def activity_feed_string(self):
return "{} (requested by {})".format(self.event.display_id, self.sent_by.initials)
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
class InvoiceManager(models.Manager):
def outstanding_invoices(self):
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
sql = "SELECT * FROM " \
"(SELECT " \
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
"AS sub " \
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
"ORDER BY invoice_date"
query = self.raw(sql)
return query
@reversion.register(follow=['payment_set'])
@@ -538,6 +571,8 @@ class Invoice(models.Model, RevisionMixin):
reversion_perm = 'RIGS.view_invoice'
objects = InvoiceManager()
@property
def sum_total(self):
return self.event.sum_total
@@ -562,11 +597,11 @@ class Invoice(models.Model, RevisionMixin):
return self.balance == 0 or self.void
def get_absolute_url(self):
return reverse_lazy('invoice_detail', kwargs={'pk': self.pk})
return reverse('invoice_detail', kwargs={'pk': self.pk})
@property
def activity_feed_string(self):
return "#{} for Event {}".format(self.display_id, "N%05d" % self.event.pk)
return "#{} for Event {}".format(self.display_id, self.event.display_id)
def __str__(self):
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
@@ -597,7 +632,7 @@ class Payment(models.Model, RevisionMixin):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
date = models.DateField()
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True)
method = models.CharField(max_length=2, choices=METHODS, default='', blank=True)
reversion_hide = True
@@ -632,12 +667,10 @@ class RiskAssessment(models.Model, RevisionMixin):
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?")
general_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
# Power
# 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?")
@@ -645,12 +678,12 @@ class RiskAssessment(models.Model, RevisionMixin):
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])
power_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
power_plan = models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
# Sound
noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?")
sound_notes = models.TextField(blank=True, 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?")
sound_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
# Site
known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
@@ -663,8 +696,8 @@ class RiskAssessment(models.Model, RevisionMixin):
# 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])
persons_responsible_structures = models.TextField(blank=True, default='', help_text="Who are the persons on site responsible for their use?")
rigging_plan = models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
# Blimey that was a lot of options
@@ -708,6 +741,10 @@ class RiskAssessment(models.Model, RevisionMixin):
('review_riskassessment', 'Can review Risk Assessments')
]
@cached_property
def fieldz(self):
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
@property
def event_size(self):
# Confirm event size. Check all except generators, since generators entails outside
@@ -723,7 +760,7 @@ class RiskAssessment(models.Model, RevisionMixin):
return str(self.event)
def get_absolute_url(self):
return reverse_lazy('ra_detail', kwargs={'pk': self.pk})
return reverse('ra_detail', kwargs={'pk': self.pk})
def __str__(self):
return "%i - %s" % (self.pk, self.event)
@@ -746,8 +783,8 @@ class EventChecklist(models.Model, RevisionMixin):
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")
hs_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of Safety Bag/Box")
extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers")
# Small Electrical Checks
rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
@@ -765,21 +802,21 @@ class EventChecklist(models.Model, RevisionMixin):
fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N")
fd_voltage_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_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
# Worst case points
w1_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w1_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")
w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
w2_earth_fault = models.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")
w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
w3_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</small>")
public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
@@ -796,12 +833,16 @@ class EventChecklist(models.Model, RevisionMixin):
('review_eventchecklist', 'Can review Event Checklists')
]
@cached_property
def fieldz(self):
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
@property
def activity_feed_string(self):
return str(self.event)
def get_absolute_url(self):
return reverse_lazy('ec_detail', kwargs={'pk': self.pk})
return reverse('ec_detail', kwargs={'pk': self.pk})
def __str__(self):
return "%i - %s" % (self.pk, self.event)

View File

@@ -11,7 +11,7 @@ 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.contrib.staticfiles import finders
from django.core import signing
from django.core.exceptions import SuspiciousOperation
from django.core.mail import EmailMultiAlternatives
@@ -27,6 +27,7 @@ from django.views import generic
from z3c.rml import rml2pdf
from PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax
from RIGS import models, forms
__author__ = 'ghost'
@@ -37,10 +38,10 @@ class RigboardIndex(generic.TemplateView):
def get_context_data(self, **kwargs):
# get super context
context = super(RigboardIndex, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
# call out method to get current events
context['events'] = models.Event.objects.current_events()
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
context['page_title'] = "Rigboard"
return context
@@ -49,7 +50,7 @@ class WebCalendar(generic.TemplateView):
template_name = 'calendar.html'
def get_context_data(self, **kwargs):
context = super(WebCalendar, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['view'] = kwargs.get('view', '')
context['date'] = kwargs.get('date', '')
return context
@@ -59,36 +60,31 @@ class EventDetail(generic.DetailView):
template_name = 'event_detail.html'
model = models.Event
class EventOembed(generic.View):
model = models.Event
def get(self, request, pk=None):
embed_url = reverse('event_embed', args=[pk])
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
data = {
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
'version': '1.0',
'type': 'rich',
'height': '250'
}
json = simplejson.JSONEncoderForHTML().encode(data)
return HttpResponse(json, content_type="application/json")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
title = f"{self.object.display_id} | {self.object.name}"
if self.object.dry_hire:
title += " <span class='badge badge-secondary'>Dry Hire</span>"
context['page_title'] = title
return context
class EventEmbed(EventDetail):
template_name = 'event_embed.html'
class EventOEmbed(OEmbedView):
model = models.Event
url_name = 'event_embed'
class EventCreate(generic.CreateView):
model = models.Event
form_class = forms.EventForm
template_name = 'event_form.html'
def get_context_data(self, **kwargs):
context = super(EventCreate, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['page_title'] = "New Event"
context['edit'] = True
context['currentVAT'] = models.VatRate.objects.current_rate()
@@ -114,8 +110,8 @@ class EventUpdate(generic.UpdateView):
template_name = 'event_form.html'
def get_context_data(self, **kwargs):
context = super(EventUpdate, self).get_context_data(**kwargs)
context['page_title'] = "Event {}".format(self.object.display_id)
context = super().get_context_data(**kwargs)
context['page_title'] = f"Event {self.object.display_id}"
context['edit'] = True
form = context['form']
@@ -138,7 +134,7 @@ class EventUpdate(generic.UpdateView):
if hasattr(self.object, 'authorised'):
messages.warning(self.request,
'This event has already been authorised by the client, any changes to the price will require reauthorisation.')
return super(EventUpdate, self).render_to_response(context, **response_kwargs)
return super().render_to_response(context, **response_kwargs)
def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
@@ -146,7 +142,7 @@ class EventUpdate(generic.UpdateView):
class EventDuplicate(EventUpdate):
def get_object(self, queryset=None):
old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
old = super().get_object(queryset) # Get the object (the event you're duplicating)
new = copy.copy(old) # Make a copy of the object in memory
new.based_on = old # Make the new event based on the old event
new.purchase_order = None # Remove old PO
@@ -155,9 +151,10 @@ class EventDuplicate(EventUpdate):
# Clear checked in by if it's a dry hire
if new.dry_hire is True:
new.checked_in_by = None
new.collector = None
# Remove all the authorisation information from the new event
new.auth_request_to = None
new.auth_request_to = ''
new.auth_request_by = None
new.auth_request_at = None
@@ -170,8 +167,8 @@ class EventDuplicate(EventUpdate):
return new
def get_context_data(self, **kwargs):
context = super(EventDuplicate, self).get_context_data(**kwargs)
context['page_title'] = "Duplicate of Event {}".format(self.object.display_id)
context = super().get_context_data(**kwargs)
context['page_title'] = f"Duplicate of Event {self.object.display_id}"
context["duplicate"] = True
return context
@@ -185,15 +182,9 @@ class EventPrint(generic.View):
context = {
'object': object,
'fonts': {
'opensans': {
'regular': 'static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'static/fonts/OPENSANS-BOLD.TTF',
}
},
'quote': True,
'current_user': request.user,
'filename': 'Event {} {} {}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
'filename': 'Event_{}_{}_{}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
}
rml = template.render(context)
@@ -219,8 +210,7 @@ class EventArchive(generic.ListView):
paginate_by = 25
def get_context_data(self, **kwargs):
# get super context
context = super(EventArchive, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['start'] = self.request.GET.get('start', None)
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
@@ -275,7 +265,7 @@ class EventArchive(generic.ListView):
# Preselect related for efficiency
qs.select_related('person', 'organisation', 'venue', 'mic')
if len(qs) == 0:
if not qs.exists():
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
return qs
@@ -284,6 +274,7 @@ class EventArchive(generic.ListView):
class EventAuthorise(generic.UpdateView):
template_name = 'eventauthorisation_form.html'
success_template = 'eventauthorisation_success.html'
preview = False
def form_valid(self, form):
self.object = form.save()
@@ -291,7 +282,7 @@ class EventAuthorise(generic.UpdateView):
self.template_name = self.success_template
messages.add_message(self.request, messages.SUCCESS,
'Success! Your event has been authorised. ' +
'You will also receive email confirmation to %s.' % self.object.email)
f'You will also receive email confirmation to {self.object.email}.')
return self.render_to_response(self.get_context_data())
@property
@@ -305,12 +296,13 @@ class EventAuthorise(generic.UpdateView):
return forms.InternalClientEventAuthorisationForm
def get_context_data(self, **kwargs):
context = super(EventAuthorise, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['event'] = self.event
context['tos_url'] = settings.TERMS_OF_HIRE_URL
context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name)
context['page_title'] = f"{self.event.display_id}: {self.event.name}"
if self.event.dry_hire:
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
context['preview'] = self.preview
return context
def get(self, request, *args, **kwargs):
@@ -326,7 +318,7 @@ class EventAuthorise(generic.UpdateView):
return super(EventAuthorise, self).get(request, *args, **kwargs)
def get_form(self, **kwargs):
form = super(EventAuthorise, self).get_form(**kwargs)
form = super().get_form(**kwargs)
form.instance.event = self.event
form.instance.email = self.request.email
form.instance.sent_by = self.request.sent_by
@@ -342,7 +334,7 @@ class EventAuthorise(generic.UpdateView):
except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist):
raise SuspiciousOperation(
"This URL is invalid. Please ask your TEC contact for a new URL")
return super(EventAuthorise, self).dispatch(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
@@ -352,14 +344,14 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
@method_decorator(decorators.nottinghamtec_address_required)
def dispatch(self, *args, **kwargs):
return super(EventAuthorisationRequest, self).dispatch(*args, **kwargs)
return super().dispatch(*args, **kwargs)
@property
def object(self):
return self.get_object()
def get_success_url(self):
if self.request.is_ajax():
if is_ajax(self.request):
url = reverse_lazy('closemodal')
messages.info(self.request, "location.reload()")
else:
@@ -397,7 +389,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
to=[email],
reply_to=[self.request.user.email],
)
css = staticfiles_storage.path('css/email.css')
css = finders.find('css/email.css')
html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
external_styles=css).transform()
msg.attach_alternative(html, 'text/html')
@@ -412,19 +404,19 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
model = models.Event
def render_to_response(self, context, **response_kwargs):
from django.contrib.staticfiles.storage import staticfiles_storage
css = staticfiles_storage.path('css/email.css')
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
css = finders.find('css/email.css')
response = super().render_to_response(context, **response_kwargs)
assert isinstance(response, HttpResponse)
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
return response
def get_context_data(self, **kwargs):
context = super(EventAuthoriseRequestEmailPreview, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['hmac'] = signing.dumps({
'pk': self.object.pk,
'email': self.request.GET.get('email', 'hello@world.test'),
'sent_by': self.request.user.pk,
})
context['to_name'] = self.request.GET.get('to_name', None)
context['target'] = 'event_authorise_form_preview'
return context

View File

@@ -6,7 +6,7 @@ from io import BytesIO
from PyPDF2 import PdfFileReader, PdfFileMerger
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
from django.contrib.staticfiles import finders
from django.core.cache import cache
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.db.models.signals import post_save
@@ -25,12 +25,6 @@ def send_eventauthorisation_success_email(instance):
# Generate PDF first to prevent context conflicts
context = {
'object': instance.event,
'fonts': {
'opensans': {
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'receipt': True,
'current_user': False,
}
@@ -69,7 +63,7 @@ def send_eventauthorisation_success_email(instance):
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
)
css = staticfiles_storage.path('css/email.css')
css = finders.find('css/email.css')
html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
external_styles=css).transform()
client_email.attach_alternative(html, 'text/html')
@@ -127,7 +121,7 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
to=[admin.email],
reply_to=[user.email],
)
css = staticfiles_storage.path('css/email.css')
css = finders.find('css/email.css')
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
external_styles=css).transform()
email.attach_alternative(html, 'text/html')

BIN
RIGS/static/imgs/assets.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
RIGS/static/imgs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
RIGS/static/imgs/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
RIGS/static/imgs/rigs.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -4,12 +4,12 @@
{% block title %}Calendar{% endblock %}
{% block css %}
<link href="{% static 'css/main.min.css' %}" rel='stylesheet' />
<link href="{% static 'css/main.css' %}" rel='stylesheet' />
{% endblock %}
{% block js %}
<script src="{% static 'js/moment.js' %}"></script>
<script src="{% static 'js/main.min.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script>
<script>
viewToUrl = {
'timeGridWeek':'week',

View File

@@ -5,11 +5,13 @@
{% load static %}
{% block css %}
<link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
{% endblock %}
{% block preload_js %}
<script src="{% static 'js/bootstrap-select.js' %}"></script>
{{ block.super }}
<script src="{% static 'js/selects.js' %}" async></script>
{% endblock %}
{% block content %}

View File

@@ -8,7 +8,7 @@
<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" %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div>
</div>
@@ -32,7 +32,11 @@
</dd>
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
<dd class="col-6">
{% if object.power_mic %}
<a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a>
{% else %}
None
{% endif %}
</dd>
</dl>
<p>List vehicles and their drivers</p>
@@ -98,6 +102,10 @@
<td>{{crew.role}}</td>
<td>{{crew.end}}</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center bg-warning">Apparently this event happened by magic...</td>
</tr>
{% endfor %}
</tbody>
</table>
@@ -105,9 +113,27 @@
</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 %}
{% if object.event.riskassessment.event_size == 0 %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
<dd class="col-2">
{{ object.rcds|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
<dd class="col-2">
{{ object.supply_test|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
<dd class="col-2">
{{ object.earthing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
<dd class="col-2">
{{ object.pat|yesnoi }}
</dd>
</dl>
{% else %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt>
<dd class="col-2">
@@ -212,29 +238,8 @@
</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 %}

View File

@@ -7,24 +7,18 @@
{% block css %}
{{ block.super }}
<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/selects.css' %}"/>
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/bootstrap-select.js' %}"></script>
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
<script src="{% static 'js/selects.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>
<script src="{% static 'js/tooltip.js' %}"></script>
{% include 'partials/datetime-fix.html' %}
@@ -32,9 +26,9 @@
$(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'))
let target = $($(this).attr('data-target'));
let newID = Number(target.attr('data-pk'));
let newRow = $($(this).attr('data-clone'))
.clone().attr('style', "")
.attr('id', function(i, val){
return val.split("_")[0] + '_' + newID;
@@ -134,20 +128,20 @@
<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>
<td><select data-container="body" 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">
<select data-container="body" 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>
<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>
@@ -202,17 +196,17 @@
<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>
<select name="crewmember_new" class="form-control" data-container="body" 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="start_new" type="datetime-local" class="form-control" value="{{ i.start }}" disabled=""/></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>
<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">
<select data-container="body" 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 %}
@@ -221,7 +215,7 @@
<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>
<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>
@@ -240,7 +234,7 @@
<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-header">Electrical Checks <small>for Small TEC Events <6kVA (approx. 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 %}
@@ -250,12 +244,19 @@
</div>
</div>
</div>
{% elif event.riskassessment.event_size == 1 %}
{% else %}
<div class="row my-3" id="size-1">
<div class="col-12">
{% if event.riskassessment.event_size == 1 %}
<div class="card border-warning">
<div class="card-header">Electrical Checks <small>for Medium TEC Events </small></div>
<div class="card-body">
{% else %}
<div class="card border-danger">
<div class="card-header">Electrical Checks <small>for Large TEC Events</small></div>
<div class="card-body">
<div class="alert alert-danger"><strong>Here be dragons. Ensure you have appeased the Power Gods before continuing... (If you didn't check with a Supervisor, <em>you cannot continue your event!</em>)</strong></div>
{% endif %}
{% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %}
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
@@ -337,7 +338,7 @@
</tbody>
</table>
</div>
<hr>
<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' %}
@@ -345,17 +346,6 @@
</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">

View File

@@ -1,71 +1,20 @@
{% 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 %}
{% load markdown_tags %}
{% block content %}
<div class="row my-3 py-3">
{% if not request.is_ajax %}
<div class="col-sm-12">
<h1>
{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
| {{ object.name }} {% if event.dry_hire %}<span class="badge badge-secondary">Dry Hire</span>{% endif %}
</h1>
</div>
{% if perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
{% include 'event_detail_buttons.html' %}
{% include 'partials/event_detail_buttons.html' %}
</div>
{% endif %}
{% endif %}
{% if object.is_rig and perms.RIGS.view_event %}
{# only need contact details for a rig #}
<div class="col-md-6">
{% if event.person %}
<div class="card card-default mb-3">
<div class="card-header">Contact Details</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6">Person</dt>
<dd class="col-sm-6">
{% if object.person %}
<a href="{% url 'person_detail' object.person.pk %}" class="modal-href">
{{ object.person|namewithnotes:'person_detail' }}
</a>
{% endif %}
</dd>
<dt class="col-sm-6">Email</dt>
<dd class="col-sm-6">{{ object.person.email|linkornone:'mailto' }}</dd>
<dt class="col-sm-6">Phone Number</dt>
<dd class="col-sm-6">{{ object.person.phone|linkornone:'tel' }}</a></dd>
</dl>
</div>
</div>
{% endif %}
{% if event.organisation %}
<div class="card card-default">
<div class="card-header">Organisation</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6">Organisation</dt>
<dd class="col-sm-6">
{% if object.organisation %}
<a href="{% url 'organisation_detail' object.organisation.pk %}" class="modal-href">
{{ object.organisation|namewithnotes:'organisation_detail' }}
</a>
{% endif %}
</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>
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</a></dd>
<dt class="col-sm-6">Has SU Account</dt>
<dd class="col-sm-6">{{ event.organisation.union_account|yesno|capfirst }}</dd>
</dl>
</div>
</div>
{% endif %}
{% include 'partials/contact_details.html' %}
</div>
{% endif %}
<div class="col-md-6">
@@ -85,7 +34,7 @@
{% endif %}
{% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
{% include 'event_detail_buttons.html' %}
{% include 'partials/event_detail_buttons.html' %}
</div>
{% endif %}
{% if event.is_rig %}
@@ -96,16 +45,16 @@
{% if perms.RIGS.view_event %}
<h4>Notes</h4>
<hr>
<p class="dont-break-out">{{ event.notes|linebreaksbr }}</p>
<p class="dont-break-out">{{ event.notes|markdown }}</p>
{% endif %}
<br>
{% include 'item_table.html' %}
{% include 'partials/item_table.html' %}
</div>
</div>
</div>
{% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
{% include 'event_detail_buttons.html' %}
{% include 'partials/event_detail_buttons.html' %}
</div>
{% endif %}
{% endif %}

View File

@@ -1,87 +1,78 @@
{% extends 'base_embed.html' %}
{% load static %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<a href="/">
<span class="source"> R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></span>
</a>
</div>
{% block extra-head %}
<link href="{% static 'fontawesome_free/css/fontawesome.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'fontawesome_free/css/solid.css' %}" rel="stylesheet" type="text/css">
{% endblock %}
<div class="col-sm-12">
<span class="pull-right">
{% block content %}
<span class="float-right">
{% if object.mic %}
<div class="text-center">
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo rounded"/>
</div>
<div class="text-center">
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo rounded"/>
</div>
{% elif object.is_rig %}
<span class="fas fa-exclamation-sign"></span>
<span class="fas fa-exclamation-sign"></span>
{% endif %}
</span>
<h3>
<a href="{% url 'event_detail' object.pk %}">
{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
| {{ object.name }} </a>
{% if object.venue %}
<small>at {{ object.venue }}</small>
{% endif %}
<br/><small>
{{ object.start_date|date:"D d/m/Y" }}
{% if object.has_start_time %}
<a href="{% url 'event_detail' object.pk %}">{{ object.display_id }} | {{ object.name }}</a>
{% if object.venue %}
<small>at {{ object.venue }}</small>
{% endif %}
<br/><small>
{{ object.start_date|date:"D d/m/Y" }}
{% if object.has_start_time %}
{{ object.start_time|date:"H:i" }}
{% endif %}
{% if object.end_date or object.has_end_time %}
{% endif %}
{% if object.end_date or object.has_end_time %}
&ndash;
{% endif %}
{% if object.end_date and object.end_date != object.start_date %}
{{ object.end_date|date:"D d/m/Y" }}
{% endif %}
{% if object.has_end_time %}
{% endif %}
{% if object.end_date and object.end_date != object.start_date %}
{{ object.end_date|date:"D d/m/Y" }}
{% endif %}
{% if object.has_end_time %}
{{ object.end_time|date:"H:i" }}
{% endif %}
{% endif %}
</small>
</h3>
<div class="row">
<div class="col-xs-6">
<p>
<strong>Status:</strong>
{{ object.get_status_display }}
</p>
{% include 'partials/event_status.html' %}
<div class="row ml-2">
<div class="col-xs-6 pr-2">
<p>
{% if object.is_rig %}
<strong>Client:</strong> {{ object.person.name }}
{% if object.organisation %}
for {{ object.organisation.name }}
{% endif %}
{% if object.dry_hire %}(Dry Hire){% endif %}
<strong>Client:</strong> {{ object.person.name }}
{% if object.organisation %}
for {{ object.organisation.name }}
{% endif %}
{% if object.dry_hire %}(Dry Hire){% endif %}
{% else %}
<strong>Non-Rig</strong>
<strong>Non-Rig</strong>
{% endif %}
</p>
<p>
<strong>MIC:</strong>
{% if object.mic %}
{{object.mic.name}}
{{object.mic.name}}
{% else %}
None
None
{% endif %}
</p>
</div>
<div class="col-xs-6">
<div class="col-xs-6 px-2">
{% if object.meet_at %}
<p>
<strong>Crew meet:</strong>
{{ object.meet_at|date:"H:i" }} {{ object.meet_at|date:"(Y-m-d)" }}
</p>
<p>
<strong>Crew meet:</strong>
{{ object.meet_at|date:"H:i" }} {{ object.meet_at|date:"(Y-m-d)" }}
</p>
{% endif %}
{% if object.access_at %}
<p>
<strong>Access at:</strong>
{{ object.access_at|date:"H:i" }} {{ object.access_at|date:"(Y-m-d)" }}
</p>
<p>
<strong>Access at:</strong>
{{ object.access_at|date:"H:i" }} {{ object.access_at|date:"(Y-m-d)" }}
</p>
{% endif %}
<p>
<strong>Last updated:</strong>
@@ -90,12 +81,9 @@
</div>
</div>
{% if object.description %}
<p>
<p>
<strong>Description: </strong>
{{ object.description|linebreaksbr }}
</p>
</p>
{% endif %}
</table>
</div>
</div>
{% endblock %}

View File

@@ -7,25 +7,21 @@
{% block css %}
{{ block.super }}
<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/flatpickr.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/simplemde.min.css' %}">
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/bootstrap-select.js' %}"></script>
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/simplemde.min.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>
<script src="{% static 'js/interaction.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
{% include 'partials/datetime-fix.html' %}
@@ -33,18 +29,26 @@
const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches;
$(document).ready(function () {
dur = matches ? 0 : 500;
{% if not object.pk and not form.errors %}
$('.form-hws').slideUp(dur, function () {
$('.form-is_rig').slideUp(dur);
});
{% elif not object.pk and form.errors %}
if ($('#{{form.is_rig.auto_id}}').attr('checked') !== 'checked') {
{% if object.pk %}
// Editing
{% if not object.is_rig %}
$('.form-is_rig').hide();
}
{% endif %}
{% if not object.pk %}
{% endif %}
//Creation
{% else %}
// If there were errors, apply the previous Rig/not-Rig selection
{% if form.errors %}
$('.form-hws').show();
if ($('#{{form.is_rig.auto_id}}').attr('checked') !== 'checked') {
$('.form-is_rig').hide();
}
{% else %}
//Initial hide
$('.form-hws').slideUp(dur);
{% endif %}
//Button handling
$('#is_rig-selector button').on('click', function () {
$('.form-non_rig').slideDown(dur);
$('.form-non_rig').slideDown(dur); //Non rig stuff also needed for rig, so always slide down
if ($(this).data('is_rig') === 1) {
$('#{{form.is_rig.auto_id}}').prop('checked', true);
if ($('.form-non_rig').is(':hidden')) {
@@ -54,7 +58,6 @@
}
$('.form-hws, .form-hws .form-is_rig').css('overflow', 'visible');
} else {
$('#{{form.is_rig.auto_id}}').prop('checked', false);
$('.form-is_rig').slideUp(dur);
}
@@ -62,24 +65,27 @@
{% endif %}
});
$(document).ready(function () {
setupMDE('#id_description');
setupMDE('#id_notes');
setupMDE('#item_description');
$('#itemModal').on('shown.bs.modal', function (e) {
$('#item_description').data('mde_editor').value(
$('#item_description').val()
);
});
setupItemTable($("#{{ form.items_json.id_for_label }}").val());
});
$(function () {
$('[data-toggle="tooltip"]').tooltip();
})
});
</script>
<noscript>
<style>
.form-hws {
display: inherit !important;
}
</style>
</noscript>
{% endblock %}
{% block content %}
{% include 'item_modal.html' %}
<form class=" itemised_form" role="form" method="POST">
{% include 'partials/item_modal.html' %}
<form class="itemised_form" role="form" method="POST">
{% csrf_token %}
<div class="row">
<div class="col-12">
@@ -174,7 +180,7 @@
<label for="{{ form.description.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.description.label }}</label>
<div class="col-sm-8">
<div class="col-sm-12">
{% render_field form.description class+="form-control" %}
</div>
</div>
@@ -332,7 +338,7 @@
<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>
class="col-sm-4 col-fitem_tableorm-label">{{ form.purchase_order.label }}</label>
<div class="col-sm-8">
{% render_field form.purchase_order class+="form-control" %}
@@ -351,10 +357,10 @@
<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">
<label for="{{ form.notes.id_for_label }}">{{ form.notes.label }}</label>
{% render_field form.notes class+="form-control" %}
{% render_field form.notes class+="form-control md-enabled" %}
</div>
</div>
{% include 'item_table.html' %}
{% include 'partials/item_table.html' %}
</div>
</div>
</div>

View File

@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?>
{% load multiply from filters %}
{% load static %}
<!DOCTYPE document SYSTEM "rml.dtd">
<document filename="{{filename}}">
<docinit>
<registerTTFont faceName="OpenSans" fileName="{{ fonts.opensans.regular }}"/>
<registerTTFont faceName="OpenSans-Bold" fileName="{{ fonts.opensans.bold }}"/>
<registerTTFont faceName="OpenSans" fileName="static/fonts/OpenSans-Regular.tff"/>
<registerTTFont faceName="OpenSans-Bold" fileName="static/fonts/OpenSans-Bold.tff"/>
<registerFontFamily name="OpenSans" bold="OpenSans-Bold" boldItalic="OpenSans-Bold"/>
</docinit>
@@ -77,16 +74,24 @@
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
</blockTableStyle>
<listStyle name="ol"
bulletFormat="%s."
bulletFontSize="10" />
<listStyle name="ul"
start="bulletchar"
bulletFontSize="10"/>
</stylesheet>
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
<pageTemplate id="Headed" >
<pageGraphics>
<image file="RIGS/static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
<image file="RIGS/static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
<image file="static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
{# logo positioned 42 from left, 33 from top #}
<image file="RIGS/static/imgs/paperwork/tec-logo.jpg" x="42" y="719" height="90" width="84"/>
<image file="static/imgs/paperwork/tec-logo.jpg" x="42" y="719" height="90" width="84"/>
<setFont name="OpenSans-Bold" size="22.5" leading="10"/>
<drawString x="137" y="780">TEC PA &amp; Lighting</drawString>
@@ -110,8 +115,8 @@
<pageTemplate id="Main">
<pageGraphics>
<image file="RIGS/static/imgs/paperwork/corner-tr.jpg" x="395" y="642" height="200" width="200"/>
<image file="RIGS/static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
<image file="static/imgs/paperwork/corner-tr.jpg" x="395" y="642" height="200" width="200"/>
<image file="static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
<setFont name="OpenSans" size="10"/>
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>

View File

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

View File

@@ -1,10 +0,0 @@
<blockTable style="signatureTable" colWidths="50,120,60,120,35,110">
<tr>
<td>Signature</td>
<td></td>
<td>Print Name</td>
<td></td>
<td>Date</td>
<td></td>
</tr>
</blockTable>

View File

@@ -26,7 +26,7 @@
<div class="col-sm-12">
<div class="card">
{% with object=event auth=True %}
{% include 'item_table.html' %}
{% include 'partials/item_table.html' %}
{% endwith %}
</div>
</div>

View File

@@ -1,19 +1,17 @@
{% extends 'base_client_email.html' %}
{% block content %}
<p>Hi {{ to_name|default:"there" }},</p>
<p><b>{{ request.user.get_full_name }}</b> has requested that you authorise <b>N{{ object.pk|stringformat:"05d" }}
| {{ object.name }}</b>{% if not to_name %} on behalf of <b>{{ object.person.name }}</b>{% endif %}.</p>
<p><b>{{ request.user.get_full_name }}</b> has requested that you authorise <b>{{ object.display_id }}
| {{ object.name }}</b>{% if not to_name %} on behalf of <b>{% if object.person %}{{ object.person.name }}{% else %}{{ object.organisation.name }}{% endif %}</b>{% endif %}.</p>
<p>
Please find the link below to complete the event booking process.
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
Remember that only Presidents or Treasurers are allowed to sign off payments. You may need to forward
this
email on.
{% endif %}
Remember that only Presidents or Treasurers are allowed to sign off payments. You may need to forward
this
email on.
</p>
@@ -23,7 +21,7 @@
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td class="button" align="center">
<a href="{{ request.scheme }}://{{ request.get_host }}{% url 'event_authorise' object.pk hmac %}">
<a href="{{ request.scheme }}://{{ request.get_host }}{% url target|default:'event_authorise' object.pk hmac %}">
Complete Authorisation Form
</a>
</td>
@@ -36,6 +34,7 @@
<p>Your event will not be booked until you complete this form.</p>
<p>TEC PA &amp; Lighting<br/>
<p>TEC PA &amp; Lighting</p>
<br/>
{% endblock %}

View File

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

View File

@@ -1,9 +1,6 @@
{% extends 'eventauthorisation.html' %}
{% load widget_tweaks %}
{% block js %}
{% endblock %}
{% block authorisation %}
<div class="row">
<div class="col-sm-12">
@@ -86,7 +83,7 @@
<div class="text-right">
<div class="btn-group">
<button class="btn btn-primary btn-lg" type="submit">Authorise</button>
<button class="btn btn-primary btn-lg" type="submit" {% if preview %}disabled="" data-toggle="tooltip" title="This is only a preview!"{%endif%}>Authorise</button>
</div>
</div>
</div>

View File

@@ -1,24 +1,48 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load button from filters %}
{% block title %}Request Authorisation{% endblock %}
{% block js %}
<script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/popover.js' %}"></script>
<script src="{% static 'js/clipboard.min.js' %}"></script>
<script>
var clipboard = new ClipboardJS('.btn');
clipboard.on('success', function(e) {
$(e.trigger).popover('show');
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
e.clearSelection();
});
</script>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<div class="alert alert-warning">
<div class="alert alert-warning pb-0">
<h1>Send authorisation request email.</h1>
<p>Pressing send will email the address provided. Please triple check everything before continuing.</p>
<p>Pressing send will email the address provided. <strong>Please triple check everything before continuing.</strong></p>
</div>
<div class="alert alert-info">
<div class="alert alert-info pb-0">
{% if object.person.email or object.organisation.email %}
<dl class="dl-horizontal">
{% if object.person.email %}
<dt>Person Email</dt>
<dd>{{ object.person.email }}</dd>
<dd><span id="person-email">{{ object.person.email }}</span>{% button 'copy' id='#person-email' %}</dd>
{% endif %}
{% if object.organisation.email %}
<dt>Organisation Email</dt>
<dd>{{ object.organisation.email }}</dd>
<dd><span id="org-email">{{ object.organisation.email }}</span>{% button 'copy' id='#org-email' %}</dd>
{% endif %}
</dl>
{% else %}
<p>No email addresses saved to the client &#3232;_&#3232;</p>
{% endif %}
</div>
<form action="{{ form.action|default:request.path }}" method="POST" id="auth-request-form">
{% csrf_token %}
@@ -30,14 +54,6 @@
{% render_field form.email type="email" class+="form-control" %}
</div>
</div>
<div class="text-right col-sm-3 offset-sm-9">
<div class="form-group">
<button type="submit" class="form-control btn btn-primary">
<i class="fas fa-paper-plane"></i>
Send
</button>
</div>
</div>
</form>
</div>
</div>
@@ -48,3 +64,14 @@
});
</script>
{% endblock %}
{% block footer %}
<div class="form-row">
<div class="btn-group" role="group">
<a type="button" target="_blank" href="{% url 'event_authorise_preview' object.pk %}" class="btn btn-info text-nowrap"><span class="fas fa-drafting-compass"></span> Preview</a>
<button type="submit" class="form-control btn btn-primary" form="auth-request-form">
<span class="fas fa-paper-plane"></span> Send
</button>
</div>
</div>
{% endblock %}

View File

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

View File

@@ -15,13 +15,13 @@
<div class="row">
<div class="col-12">
<div class="table-responsive">
<table class="table mb-0">
<table class="table mb-0 table-sm">
<thead>
<tr>
<th scope="col">Event</th>
{# mmm hax #}
{% if object_list.0 != None %}
{% for field in fields %}
{% for field in object_list.0.fieldz %}
<th scope="col">{{ object_list.0|verbose_name:field|title }}</th>
{% endfor %}
{% endif %}
@@ -32,8 +32,8 @@
{% 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 %}
<th scope="row"><a href="{% url 'event_detail' object.event.pk %}">{{ object.event }}</a><br><small>{{ object.event.get_status_display }}</small></th>
{% for field in object_list.0.fieldz %}
<td>{{ object|get_field:field }}</td>
{% endfor %}
{# Buttons #}

View File

@@ -2,102 +2,88 @@
{% load button from filters %}
{% block content %}
<div class="col-sm-12">
<div class="row justify-content-end py-3">
<div class="col-sm-4 text-right">
<div class="btn-group btn-page">
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-danger" title="Delete Invoice">
<span class="fas fa-times"></span> <span
class="d-none d-sm-inline">Delete</span>
</a>
<a href="{% url 'invoice_void' object.pk %}" class="btn btn-warning" title="Void Invoice">
<span class="fas fa-ban"></span> <span
class="d-none d-sm-inline">Void</span>
</a>
{% button 'print' url='invoice_print' pk=object.pk %}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="card card-default">
<div class="card-header">Invoice Details<span class="float-right">
{% if object.void %}(VOID){% elif object.is_closed %}(PAID){% else %}(OUTSTANDING){% endif %}
</span></div>
<div class="card-body">
{% if object.event.organisation %}
{{ object.event.organisation.name }}<br/>
{{ object.event.organisation.address|linebreaksbr }}
{% else %}
{{ object.event.person.name }}<br/>
{{ object.event.person.address|linebreaksbr }}
{% endif %}
</div>
</div>
</div>
<div class="col-sm-6">
{% include 'partials/event_details.html' %}
</div>
{% if object.event.internal %}
<div class="col-sm-6">
{% include 'partials/auth_details.html' %}
</div>
{% endif %}
</div>
<div class="row py-4">
<div class="col-sm-6">
<div class="card card-default">
<div class="card-body">
<div class="text-right py-3">
<a href="{% url 'payment_create' %}?invoice={{ object.pk }}"
class="btn btn-success modal-href"
data-target="#{{ form.person.id_for_label }}">
<span class="fas fa-plus"></span> Add
</a>
</div>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Date</td>
<th scope="col">Amount</td>
<th scope="col">Method</td>
<th scope="col"></td>
</tr>
</thead>
<tbody>
{% for payment in object.payment_set.all %}
<tr>
<th scope="row">{{ payment.date }}</th>
<td>{{ payment.amount|floatformat:2 }}</td>
<td>{{ payment.get_method_display }}</td>
<td>
<a href="{% url 'payment_delete' payment.pk %}" class="btn btn-small btn-danger"><span class="fas fa-times"</a>
</td>
</tr>
{% endfor %}
<tr>
<td class="text-right"><strong>Balance:</strong></td>
<td>{{ object.balance|floatformat:2 }}</td>
<td></td>
<td></td>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="card">
{% with object.event as object %}
{% include 'item_table.html' %}
{% endwith %}
</div>
</div>
</div>
<div class="col-12 text-right">
{% include 'partials/last_edited.html' with target="invoice_history" %}
<div class="row py-4">
<div class="col-sm-12 text-right px-0">
<div class="btn-group">
<a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary">Open Event Page <span class="fas fa-eye"></span></a>
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-danger" title="Delete Invoice">
<span class="fas fa-times"></span> <span
class="d-none d-sm-inline">Delete</span>
</a>
<a href="{% url 'invoice_void' object.pk %}" class="btn btn-warning" title="Void Invoice">
<span class="fas fa-ban"></span> <span
class="d-none d-sm-inline">Void</span>
</a>
{% button 'print' url='invoice_print' pk=object.pk %}
</div>
</div>
<div>
<div class="row py-4">
{% with object.event as object %}
<div class="col-sm-6">
{% include 'partials/contact_details.html' %}
</div>
<div class="col-sm-6">
{% include 'partials/event_details.html' %}
</div>
{% if object.event.internal %}
<div class="col-sm-6">
{% include 'partials/auth_details.html' %}
</div>
{% endif %}
{% endwith %}
</div>
<div class="row py-4">
<div class="col-sm-6">
<div class="card card-default">
<div class="card-body">
<div class="text-right py-3">
<a href="{% url 'payment_create' %}?invoice={{ object.pk }}"
class="btn btn-success modal-href"
data-target="#{{ form.person.id_for_label }}">
<span class="fas fa-plus"></span> Add
</a>
</div>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Amount</th>
<th scope="col">Method</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for payment in object.payment_set.all %}
<tr>
<th scope="row">{{ payment.date }}</th>
<td>{{ payment.amount|floatformat:2 }}</td>
<td>{{ payment.get_method_display }}</td>
<td>
<a href="{% url 'payment_delete' payment.pk %}" class="btn btn-small btn-danger"><span class="fas fa-times"></span></a>
</td>
</tr>
{% endfor %}
<tr>
<td class="text-right"><strong>Balance:</strong></td>
<td>{{ object.balance|floatformat:2 }}</td>
<td></td>
<td></td>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="card">
{% with object.event as object %}
{% include 'partials/item_table.html' %}
{% endwith %}
</div>
</div>
</div>
<div class="col-12 text-right">
{% include 'partials/last_edited.html' with target="invoice_history" %}
</div>
{% endblock %}

View File

@@ -27,7 +27,7 @@
<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>
<span class="text-muted">{{ invoice.event.get_status_display }}{% if not invoice.event.mic %}, No MIC{% endif %}
</span></td>
</span>
</td>
<td>{% if invoice.event.organisation %}
{{ invoice.event.organisation.name }}
@@ -42,7 +42,7 @@
<td>{{ invoice.event.start_date }}</td>
<td>{{ invoice.invoice_date }}</td>
<td>
{{ invoice.balance|floatformat:2 }}
£{{ invoice.balance|floatformat:2 }}
{% if not invoice.event.internal %}
<br />
<span class="text-muted">{{ invoice.event.purchase_order }}</span>

View File

@@ -53,7 +53,7 @@
{% endif %}
</td>
<td>
{{ event.sum_total|floatformat:2 }}
£{{ event.sum_total|floatformat:2 }}
<br />
<span class="text-muted">{% if not event.internal %}{{ event.purchase_order }}{% endif %}</span>
</td>

View File

@@ -1,15 +1,15 @@
<div class="card card-default
{% if object.authorised %}
card-success
{% if event.authorised %}
border-success
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
card-warning
border-warning
{% elif event.auth_request_to %}
card-info
border-info
{% endif %}
">
<div class="card-header">Client Authorisation</div>
<div class="card-body row">
<dl class="col-md-6">
<dl class="col-sm-6">
<dt>Authorisation Request</dt>
<dd>{{ object.auth_request_to|yesno:"Yes,No" }}</dd>
@@ -22,8 +22,8 @@
<dt>To</dt>
<dd>{{ object.auth_request_to }}</dd>
</dl>
<dd class="d-block d-sm-none">&nbsp;</dd>
<dl class="col-md-6">
<dl class="col-sm-6">
<hr class="d-block d-sm-none">
<dt>Authorised</dt>
<dd>{{ object.authorised|yesno:"Yes,No" }}</dd>

View File

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

View File

@@ -0,0 +1,47 @@
{% load linkornone from filters %}
{% load namewithnotes from filters %}
{% if object.person %}
<div class="card card-default mb-3">
<div class="card-header">Person Details</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6">Person</dt>
<dd class="col-sm-6">
{% if object.person %}
<a href="{% url 'person_detail' object.person.pk %}" class="modal-href">
{{ object.person|namewithnotes:'person_detail' }}
</a>
{% endif %}
</dd>
<dt class="col-sm-6">Email</dt>
<dd class="col-sm-6">{{ object.person.email|linkornone:'mailto' }}</dd>
<dt class="col-sm-6">Phone Number</dt>
<dd class="col-sm-6">{{ object.person.phone|linkornone:'tel' }}</dd>
</dl>
</div>
</div>
{% endif %}
{% if object.organisation %}
<div class="card card-default">
<div class="card-header">Organisation Details</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6">Organisation</dt>
<dd class="col-sm-6">
{% if object.organisation %}
<a href="{% url 'organisation_detail' object.organisation.pk %}" class="modal-href">
{{ object.organisation|namewithnotes:'organisation_detail' }}
</a>
{% endif %}
</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>
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</dd>
<dt class="col-sm-6">Has SU Account</dt>
<dd class="col-sm-6">{{ event.organisation.union_account|yesno|capfirst }}</dd>
</dl>
</div>
</div>
{% endif %}

View File

@@ -20,6 +20,7 @@
<tr>
<td>300</td>
<td>167</td>
</tr>
<tr>
<td>500</td>
<td>100</td>

View File

@@ -9,7 +9,7 @@
{% if event.internal %}
<a class="btn item-add modal-href event-authorise-request
{% if event.authorised %}
btn-success
btn-success active
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
btn-warning
{% elif event.auth_request_to %}
@@ -19,7 +19,7 @@
{% endif %}
"
href="{% url 'event_authorise_request' object.pk %}">
<i class="fas fa-paper-plane"></i>
<span class="fas fa-paper-plane"></span>
<span class="d-none d-sm-inline">
{% if event.authorised %}
Authorised
@@ -47,5 +47,7 @@
class="fas fa-pound-sign"></span>
<span class="d-none d-sm-inline">Invoice</span></a>
{% endif %}
<a href="https://docs.google.com/forms/d/e/1FAIpQLSf-TBOuJZCTYc2L8DWdAaC3_Werq0ulsUs8-6G85I6pA9WVsg/viewform" class="btn btn-danger"><span class="fas fa-file-invoice-dollar"></span> <span class="d-none d-sm-inline">Subhire Insurance Form</span></a>
{% endif %}
</div>

View File

@@ -1,4 +1,5 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<div class="card card-info">
<div class="card-header">Event Info</div>
<div class="card-body">
@@ -20,15 +21,7 @@
{% if event.is_rig %}
<dt class="col-sm-6">Event MIC</dt>
<dd class="col-sm-6">
{% if event.mic and perms.RIGS.view_profile %}
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
{{ event.mic.name }}
</a>
{% else %}
{{ event.mic.name }}
{% endif %}
</dd>
<dd class="col-sm-6">{% include 'partials/linked_name.html' with profile=event.mic %}</dd>
{% endif %}
<dt class="col-sm-6">Status</dt>
@@ -54,7 +47,7 @@
<dd class="col-sm-12">&nbsp;</dd>
<dt class="col-sm-6">Event Description</dt>
<dd class="dont-break-out col-sm-12">{{ event.description|linebreaksbr }}</dd>
<dd class="dont-break-out col-sm-12">{{ event.description|markdown }}</dd>
<dd class="col-sm-12">&nbsp;</dd>
@@ -71,7 +64,7 @@
{% if event.dry_hire %}
<dt class="col-sm-6">Checked In By</dt>
<dd class="col-sm-6">{{ object.checked_in_by.name }}</dd>
<dd class="col-sm-6">{% include 'partials/linked_name.html' with profile=event.checked_in_by %}</dd>
{% endif %}
{% if event.is_rig %}

View File

@@ -1,12 +1,18 @@
<h5>
<div>
<span class="badge badge-{% if event.confirmed %}success{% elif event.cancelled %}dark{% else %}warning{% endif %}">Status: {{ event.get_status_display }}</span>
{% if event.is_rig %}
{% if event.purchase_order %}
<span class="badge badge-success">PO: {{ event.purchase_order }}</span>
{% elif event.authorised %}
<span class="badge badge-success">Authorisation: Complete <span class="fas fa-check"></span></span>
{% else %}
<span class="badge badge-danger">Authorisation: <span class="fas fa-times"></span></span>
{% if event.sum_total > 0 %}
{% if event.purchase_order %}
<span class="badge badge-success">PO: {{ event.purchase_order }}</span>
{% elif event.authorised %}
<span class="badge badge-success">Authorisation: Complete <span class="fas fa-check"></span></span>
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
<span class="badge badge-warning"> Authorisation: Issue <span class="fas fa-exclamation-circle"></span></span>
{% elif event.auth_request_to %}
<span class="badge badge-info"> Authorisation: Sent <span class="fas fa-paper-plane"></span></span>
{% else %}
<span class="badge badge-danger">Authorisation: <span class="fas fa-times"></span></span>
{% endif %}
{% endif %}
{% if not event.dry_hire %}
{% if event.riskassessment %}
@@ -14,8 +20,6 @@
{% else %}
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>
{% endif %}
{% else %}
<span class="badge badge-secondary">RA: N/A</span>
{% endif %}
{% if not event.dry_hire %}
{% if event.hs_done %}
@@ -24,8 +28,6 @@
{% else %}
<span class="badge badge-danger">Checklist: <span class="fas fa-times"></span></span>
{% endif %}
{% else %}
<span class="badge badge-secondary">Checklist: N/A</span>
{% endif %}
{% if perms.RIGS.view_invoice %}
{% if event.invoice %}
@@ -41,4 +43,4 @@
{% endif %}
{% endif %}
{% endif %}
</h5>
</div>

View File

@@ -25,30 +25,30 @@
{% endif %}
{% else %}
table-warning
{% endif %}" id="event_row">
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<!---Number-->
<th scope="row" id="event_number">{{ event.display_id }}</th>
<!--Dates & Times-->
<td id="event_dates">
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}</strong>
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}
{% if event.has_start_time %}
{{ event.start_time|date:"H:i" }}
{% endif %}
{% endif %}</strong>
</span>
{% if event.end_date %}
<br>
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}</strong>{% endif %}
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}{% endif %}
{% if event.has_end_time %}
{{ event.end_time|date:"H:i" }}
{% endif %}
{% endif %}</strong>
</span>
{% endif %}
{% if not event.cancelled %}
{% if event.meet_at %}
<br><span>Crew meet: <strong>{{ event.meet_at|date:"H:i" }}</strong> {{ event.meet_at|date:"(d/m/Y)" }}</span>
<br><span class="text-nowrap">Meet: <strong>{{ event.meet_at|date:"D d/m/Y H:i" }}</strong></span>
{% endif %}
{% if event.access_at %}
<br><span>Access at: <strong>{{ event.access_at|date:"H:i" }}</strong> {{ event.access_at|date:"(d/m/Y)" }}</span>
<br><span class="text-nowrap">Access: <strong>{{ event.access_at|date:" D d/m/Y H:i" }}</strong></span>
{% endif %}
{% endif %}
</td>
@@ -67,9 +67,9 @@
</h4>
{% if event.is_rig and not event.cancelled %}
<h5>
{{ event.person.name }}
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
{% if event.organisation %}
for {{ event.organisation.name }}
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
{% endif %}
</h5>
{% endif %}
@@ -90,7 +90,7 @@
</a>
{% endif %}
{% elif event.is_rig %}
<span class="fas fa-exclamation"></span>
<span class="fas fa-user-slash"></span>
{% endif %}
</td>
</tr>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
{% if profile and perms.RIGS.view_profile %}
<a href="{% url 'profile_detail' profile.pk %}" class="modal-href">
{{ profile.name }}
</a>
{% else %}
{{ profile.name }}
{% endif %}

View File

@@ -2,9 +2,9 @@
{% load button from filters %}
{% block content %}
<div class="row align-items-center justify-content-between py-2">
<div class="col-sm-12 col-md">
Key: <span class="table-success mr-1 px-2">Ready</span><span class="table-warning mr-1 px-2">Action Required</span><span class="table-danger mr-1 px-2">Needs MIC</span><span class="table-secondary mr-1 px-2">Cancelled</span><span class="table-info px-2">Non-Rig</span>
<div class="row align-items-center justify-content-between py-2 align-middle">
<div class="col-sm-12 col-md align-middle">
Key: <span class="table-success mr-1 px-2 rounded">Ready</span><span class="table-warning mr-1 px-2 rounded">Action Required</span><span class="table-danger mr-1 px-2 rounded">Needs MIC</span><span class="table-secondary mr-1 px-2 rounded">Cancelled</span><span class="table-info px-2 rounded">Non-Rig</span>
</div>
{% if perms.RIGS.add_event %}
<div class="col text-right">

View File

@@ -1,13 +1,11 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% block title %}Risk Assessment for Event N{{ object.event.pk|stringformat:"05d" }} {{ object.event.name }}{% endblock %}
{% load help_text from filters %}
{% load yesnoi from filters %}
{% load linkornone from filters %}
{% block content %}
<div class="row py-3">
<div class="col-12">
<h3>Risk Assessment for Event N{{ object.event.pk|stringformat:"05d" }} {{ object.event.name }}</h3>
<div class="row py-3">
<div class="col-12">
<div class="card card-default mb-3">
<div class="card-header">General</div>
<div class="card-body">
@@ -47,15 +45,15 @@
<dd class="col-sm-6">
{{ object.big_power|yesnoi:'invert' }}
</dd>
<dt class="col-sm-6">{{ object|help_text:'power_mic' }}</dt>
<dt class="col-sm-6">{{ object|help_text:'power_mic'|safe }}</dt>
<dd class="col-sm-6">
{{ object.power_mic.name|default:'None' }}
</dd>
<dt class="col-sm-6">{{ object|help_text:'generators' }}</dt>
<dt class="col-sm-6">{{ object|help_text:'outside' }}</dt>
<dd class="col-sm-6">
{{ object.outside|yesnoi:'invert' }}
</dd>
<dt class="col-sm-6">{{ object|help_text:'outside' }}</dt>
<dt class="col-sm-6">{{ object|help_text:'generators' }}</dt>
<dd class="col-sm-6">
{{ object.generators|yesnoi:'invert' }}
</dd>
@@ -97,68 +95,73 @@
</dl>
</div>
</div>
<div class="card card-default mb-3">
<div class="card-header">Site Details</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6">{{ object|help_text:'known_venue' }}</dt>
<dd class="col-sm-6">
{{ object.known_venue|yesnoi:'invert' }}
</dd>
<dt class="col-sm-6">{{ object|help_text:'safe_loading'|safe }}</dt>
<dd class="col-sm-6">
{{ object.safe_loading|yesnoi:'invert' }}
</dd>
<dt class="col-sm-6">{{ object|help_text:'safe_storage' }}</dt>
<dd class="col-sm-6">
{{ object.safe_storage|yesnoi:'invert' }}
</dd>
<dt class="col-sm-6">{{ object|help_text:'area_outside_of_control' }}</dt>
<dd class="col-sm-6">
{{ object.area_outside_of_control|yesnoi:'invert' }}
</dd>
<dt class="col-sm-6">{{ object|help_text:'barrier_required' }}</dt>
<dd class="col-sm-6">
{{ object.barrier_required|yesnoi:'invert' }}
</dd>
<dt class="col-sm-6">{{ object|help_text:'nonstandard_emergency_procedure' }}</dt>
<dd class="col-sm-6">
{{ object.nonstandard_emergency_procedure|yesnoi:'invert' }}
</dd>
</dl>
<div class="row">
<div class="col-lg-6 col-12">
<div class="card card-default mb-3">
<div class="card-header">Site Details</div>
<div class="card-body">
<dl class="row">
<dt class="col-10">{{ object|help_text:'known_venue' }}</dt>
<dd class="col-2">
{{ object.known_venue|yesnoi:'invert' }}
</dd>
<dt class="col-10">{{ object|help_text:'safe_loading'|safe }}</dt>
<dd class="col-2">
{{ object.safe_loading|yesnoi:'invert' }}
</dd>
<dt class="col-10">{{ object|help_text:'safe_storage' }}</dt>
<dd class="col-2">
{{ object.safe_storage|yesnoi:'invert' }}
</dd>
<dt class="col-10">{{ object|help_text:'area_outside_of_control' }}</dt>
<dd class="col-2">
{{ object.area_outside_of_control|yesnoi:'invert' }}
</dd>
<dt class="col-10">{{ object|help_text:'barrier_required' }}</dt>
<dd class="col-2">
{{ object.barrier_required|yesnoi:'invert' }}
</dd>
<dt class="col-10">{{ object|help_text:'nonstandard_emergency_procedure' }}</dt>
<dd class="col-2">
{{ object.nonstandard_emergency_procedure|yesnoi:'invert' }}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="card card-default mb-3">
<div class="card-header">Structures</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6">{{ object|help_text:'special_structures' }}</dt>
<dd class="col-sm-6">
{{ object.special_structures|yesnoi:'invert' }}
</dd>
<dt class="col-sm-6">{{ object|help_text:'suspended_structures' }}</dt>
<dd class="col-sm-6">
{{ object.suspended_structures|yesnoi:'invert' }}
</dd>
<dt class="col-sm-6">{{ object|help_text:'persons_responsible_structures' }}</dt>
<dd class="col-sm-6">
{{ object.persons_responsible_structures.name|default:'N/A'|linebreaks }}
</dd>
<dt class="col-6">{{ object|help_text:'rigging_plan'|safe }}</dt>
<dd class="col-6">
{{ object.rigging_plan|linkornone }}
</dd>
</dl>
<div class="col-lg-6 col-12">
<div class="card card-default mb-3">
<div class="card-header">Structures</div>
<div class="card-body">
<dl class="row">
<dt class="col-10">{{ object|help_text:'special_structures' }}</dt>
<dd class="col-2">
{{ object.special_structures|yesnoi:'invert' }}
</dd>
<dt class="col-10">{{ object|help_text:'suspended_structures' }}</dt>
<dd class="col-2">
{{ object.suspended_structures|yesnoi:'invert' }}
</dd>
<dt class="col-12">{{ object|help_text:'persons_responsible_structures' }}</dt>
<dd class="col-12">
{{ object.persons_responsible_structures.name|default:'N/A'|linebreaks }}
</dd>
<dt class="col-12">{{ object|help_text:'rigging_plan'|safe }}</dt>
<dd class="col-12">
{{ object.rigging_plan|linkornone|default:'N/A' }}
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 text-right">
<a href="{% url 'ra_edit' object.pk %}" class="btn btn-warning my-3"><span class="fas fa-edit"></span> <span
class="d-none d-sm-inline">Edit</span></a>
<a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View Event</a>
{{ object.review_string|safe }}
{% include 'partials/last_edited.html' with target="riskassessment_history" %}
</div>
</div>
<div class="col-12 text-right">
<a href="{% url 'ra_edit' object.pk %}" class="btn btn-warning my-3"><span class="fas fa-edit"></span> <span
class="d-none d-sm-inline">Edit</span></a>
<a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View Event</a>
{{ object.review_string|safe }}
{% include 'partials/last_edited.html' with target="riskassessment_history" %}
</div>
</div>
{% endblock %}

View File

@@ -5,25 +5,16 @@
{% load nice_errors 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' %}"/>
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/bootstrap-select.js' %}"></script>
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
<script src="{% static 'js/selects.js' %}" async></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>
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
function parseBool(str) {

View File

@@ -4,7 +4,7 @@ from django.forms.forms import NON_FIELD_ERRORS
from django.forms.utils import ErrorDict
from django.template.defaultfilters import stringfilter
from django.template.defaultfilters import yesno, title, truncatewords
from django.urls import reverse_lazy
from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import SafeData, mark_safe
from django.utils.text import normalize_newlines
@@ -114,10 +114,8 @@ def orderby(request, field, attr):
return dict_.urlencode()
# Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
@register.filter(needs_autoescape=True)
@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
def get_field(obj, field, autoescape=True):
value = getattr(obj, field)
if(isinstance(value, bool)):
@@ -173,7 +171,7 @@ def title_spaced(string):
@register.filter(needs_autoescape=True)
def namewithnotes(obj, url, autoescape=True):
if hasattr(obj, 'notes') and obj.notes is not None and len(obj.notes) > 0:
return mark_safe(obj.name + " <a href='{}'><span class='far fa-sticky-note'></span></a>".format(reverse_lazy(url, kwargs={'pk': obj.pk})))
return mark_safe(obj.name + " <a href='{}'><span class='fas fa-sticky-note'></span></a>".format(reverse(url, kwargs={'pk': obj.pk})))
else:
return obj.name
@@ -212,8 +210,25 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
clazz += " btn-primary "
icon = "fa-plus"
text = "New"
elif type == 'copy':
return {'copy': True, 'id': id, 'style': style}
elif type == 'search':
return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style}
elif type == 'submit':
return {'submit': True, 'class': 'btn-primary', 'icon': 'fa-save', 'text': 'Save', 'id': id, 'style': style}
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}
@register.simple_tag # TODO Can these be done with annotation/aggregation?
def invoices_waiting():
return len(models.Event.objects.waiting_invoices())
@register.simple_tag
def invoices_outstanding():
return len(models.Invoice.objects.outstanding_invoices())
@register.simple_tag
def total_invoices_todo():
return len(models.Event.objects.waiting_invoices()) + len(models.Invoice.objects.outstanding_invoices())

View File

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

108
RIGS/tests/conftest.py Normal file
View File

@@ -0,0 +1,108 @@
from RIGS import models
import pytest
from django.utils import timezone
@pytest.fixture
def basic_event(db):
event = models.Event.objects.create(name="TE E1", start_date=timezone.now())
yield event
event.delete()
@pytest.fixture
def ra(basic_event, admin_user):
ra = models.RiskAssessment.objects.create(event=basic_event, nonstandard_equipment=False, nonstandard_use=False,
contractors=False, other_companies=False, crew_fatigue=False,
big_power=False, power_mic=admin_user, generators=False,
other_companies_power=False, nonstandard_equipment_power=False,
multiple_electrical_environments=False, noise_monitoring=False,
known_venue=True, safe_loading=True, safe_storage=True,
area_outside_of_control=True, barrier_required=True,
nonstandard_emergency_procedure=True, special_structures=False,
suspended_structures=False, outside=False)
yield ra
ra.delete()
@pytest.fixture
def medium_ra(ra):
ra.big_power = True
ra.save()
yield ra
ra.big_power = False
ra.save()
@pytest.fixture
def venue(db):
venue = models.Venue.objects.create(name="Venue 1")
yield venue
venue.delete()
@pytest.fixture # TODO parameterise with Event sizes
def checklist(basic_event, venue, admin_user, ra):
checklist = models.EventChecklist.objects.create(event=basic_event, power_mic=admin_user, safe_parking=False,
safe_packing=False, exits=False, trip_hazard=False, warning_signs=False,
ear_plugs=False, hs_location="Locked away safely",
extinguishers_location="Somewhere, I forgot", earthing=False, pat=False,
date=timezone.now(), venue=venue)
yield checklist
checklist.delete()
@pytest.fixture
def many_events(db, admin_user, scope="class"):
many_events = {
# produce 7 normal events - 5 current
1: models.Event.objects.create(name="TE E1", start_date=date.today() + timedelta(days=6),
description="start future no end"),
2: models.Event.objects.create(name="TE E2", start_date=date.today(), description="start today no end"),
3: models.Event.objects.create(name="TE E3", start_date=date.today(), end_date=date.today(),
description="start today with end today"),
4: models.Event.objects.create(name="TE E4", start_date='2014-03-20', description="start past no end"),
5: models.Event.objects.create(name="TE E5", start_date='2014-03-20', end_date='2014-03-21',
description="start past with end past"),
6: models.Event.objects.create(name="TE E6", start_date=date.today() - timedelta(days=2),
end_date=date.today() + timedelta(days=2),
description="start past, end future"),
7: models.Event.objects.create(name="TE E7", start_date=date.today() + timedelta(days=2),
end_date=date.today() + timedelta(days=2),
description="start + end in future"),
# 2 cancelled - 1 current
8: models.Event.objects.create(name="TE E8", start_date=date.today() + timedelta(days=2),
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
description="cancelled in future"),
9: models.Event.objects.create(name="TE E9", start_date=date.today() - timedelta(days=1),
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
description="cancelled and started"),
# 5 dry hire - 3 current
10: models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True,
description="dryhire today"),
11: models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True,
checked_in_by=admin_user,
description="dryhire today, checked in"),
12: models.Event.objects.create(name="TE E12", start_date=date.today() - timedelta(days=1), dry_hire=True,
status=models.Event.BOOKED, description="dryhire past"),
13: models.Event.objects.create(name="TE E13", start_date=date.today() - timedelta(days=2), dry_hire=True,
checked_in_by=admin_user, description="dryhire past checked in"),
14: models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True,
status=models.Event.CANCELLED, description="dryhire today cancelled"),
# 4 non rig - 3 current
15: models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False,
description="non rig today"),
16: models.Event.objects.create(name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False,
description="non rig tomorrow"),
17: models.Event.objects.create(name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False,
description="non rig yesterday"),
18: models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False,
status=models.Event.CANCELLED,
description="non rig today cancelled"),
}
yield many_events
for event in many_events:
event.delete()

View File

@@ -52,8 +52,8 @@ class EventDetail(BasePage):
URL_TEMPLATE = 'event/{event_id}'
# TODO Refactor into regions to match template fragmentation
_event_name_selector = (By.XPATH, '//h1')
_person_panel_selector = (By.XPATH, '//div[contains(text(), "Contact Details")]/..')
_event_name_selector = (By.XPATH, '//h2')
_person_panel_selector = (By.XPATH, '//div[contains(text(), "Person Details")]/..')
_name_selector = (By.XPATH, '//dt[text()="Person"]/following-sibling::dd[1]')
_email_selector = (By.XPATH, '//dt[text()="Email"]/following-sibling::dd[1]')
_phone_selector = (By.XPATH, '//dt[text()="Phone Number"]/following-sibling::dd[1]')
@@ -96,7 +96,7 @@ class CreateEvent(FormPage):
_warning_selector = (By.XPATH, '/html/body/div[1]/div[1]')
form_items = {
'description': (regions.TextBox, (By.ID, 'id_description')),
'description': (regions.SimpleMDETextArea, (By.ID, 'id_description')),
'name': (regions.TextBox, (By.ID, 'id_name')),
'start_date': (regions.DatePicker, (By.ID, 'id_start_date')),
@@ -110,7 +110,7 @@ class CreateEvent(FormPage):
'collected_by': (regions.TextBox, (By.ID, 'id_collector')),
'po': (regions.TextBox, (By.ID, 'id_purchase_order')),
'notes': (regions.TextBox, (By.ID, 'id_notes'))
'notes': (regions.SimpleMDETextArea, (By.ID, 'id_notes'))
}
def select_event_type(self, type_name):
@@ -230,9 +230,11 @@ class CreateEventChecklist(FormPage):
URL_TEMPLATE = 'event/{event_id}/checklist'
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
_power_mic_selector = (By.XPATH, "//div[@id='id_power_mic-group']//div[contains(@class, 'bootstrap-select')]")
_power_mic_selector = (By.XPATH, "//div[select[@id='id_power_mic']]")
_add_vehicle_locator = (By.XPATH, "//button[contains(., 'Vehicle')]")
_add_crew_locator = (By.XPATH, "//button[contains(., 'Crew')]")
_vehicle_row_locator = ('xpath', "//tr[@id[starts-with(., 'vehicle') and not(contains(.,'new'))]]")
_crew_row_locator = ('xpath', "//tr[@id[starts-with(., 'crew') and not(contains(.,'new'))]]")
form_items = {
'safe_parking': (regions.CheckBox, (By.ID, 'id_safe_parking')),
@@ -271,11 +273,61 @@ class CreateEventChecklist(FormPage):
def power_mic(self):
return regions.BootstrapSelectElement(self, self.find_element(*self._power_mic_selector))
@property
def vehicles(self):
return [self.VehicleRow(self, el) for el in self.find_elements(*self._vehicle_row_locator)]
class VehicleRow(Region):
_name_locator = ('xpath', ".//input")
_select_locator = ('xpath', ".//div[contains(@class,'bootstrap-select')]/..")
@property
def name(self):
return regions.TextBox(self, self.root.find_element(*self._name_locator))
@property
def vehicle(self):
return regions.BootstrapSelectElement(self, self.root.find_element(*self._select_locator))
@property
def crew(self):
return [self.CrewRow(self, el) for el in self.find_elements(*self._crew_row_locator)]
class CrewRow(Region):
_select_locator = ('xpath', ".//div[contains(@class,'bootstrap-select')]/..")
_start_time_locator = ('xpath', ".//input[@name[starts-with(., 'start') and not(contains(.,'new'))]]")
_end_time_locator = ('xpath', ".//input[@name[starts-with(., 'end') and not(contains(.,'new'))]]")
_role_locator = ('xpath', ".//input[@name[starts-with(., 'role') and not(contains(.,'new'))]]")
@property
def crewmember(self):
return regions.BootstrapSelectElement(self, self.root.find_element(*self._select_locator))
@property
def start_time(self):
return regions.DateTimePicker(self, self.root.find_element(*self._start_time_locator))
@property
def end_time(self):
return regions.DateTimePicker(self, self.root.find_element(*self._end_time_locator))
@property
def role(self):
return regions.TextBox(self, self.root.find_element(*self._role_locator))
@property
def success(self):
return '{event_id}' not in self.driver.current_url
class EditEventChecklist(CreateEventChecklist):
URL_TEMPLATE = '/event/checklist/{pk}/edit'
@property
def success(self):
return 'edit' not in self.driver.current_url
class GenericList(BasePage):
_search_selector = (By.CSS_SELECTOR, 'div.input-group:nth-child(2) > input:nth-child(1)')
_search_go_selector = (By.ID, 'id_search')

View File

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

View File

@@ -8,54 +8,12 @@ from django.http import HttpResponseBadRequest
from django.test import TestCase
from django.urls import reverse
import PyRIGS.tests.base
from RIGS import models
from pytest_django.asserts import assertContains, assertNotContains
class BaseCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
cls.profile = models.Profile.objects.get_or_create(
first_name='Test',
last_name='TEC User',
username='eventauthtest',
email='teccie@functional.test',
is_superuser=True # lazily grant all permissions
)[0]
def setUp(self):
super().setUp()
self.profile.set_password('testuser')
self.profile.save()
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
venue = models.Venue.objects.create(name='Authorisation Test Venue')
client = models.Person.objects.create(name='Authorisation Test Person', email='authorisation@functional.test')
organisation = models.Organisation.objects.create(name='Authorisation Test Organisation', union_account=True)
self.event = models.Event.objects.create(
name='Authorisation Test',
start_date=date.today(),
venue=venue,
person=client,
organisation=organisation,
)
class TestEventValidation(BaseCase):
def test_create(self):
url = reverse('event_create')
# end time before start access after start
response = self.client.post(url, {'start_date': datetime.date(2020, 1, 1), 'start_time': datetime.time(10, 00),
'end_time': datetime.time(9, 00),
'access_at': datetime.datetime(2020, 1, 5, 10)})
self.assertFormError(response, 'form', 'end_time',
"Unless you've invented time travel, the event can't finish before it has started.")
self.assertFormError(response, 'form', 'access_at',
"Regardless of what some clients might think, access time cannot be after the event has started.")
from pytest_django.asserts import assertContains, assertNotContains, assertFormError
def setup_event():
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
venue = models.Venue.objects.create(name='Authorisation Test Venue')
client = models.Person.objects.create(name='Authorisation Test Person', email='authorisation@functional.test')
organisation = models.Organisation.objects.create(name='Authorisation Test Organisation', union_account=True)
@@ -84,6 +42,18 @@ def setup_mail(event, profile):
return auth_data, hmac, url
def test_create(admin_client):
url = reverse('event_create')
# end time before start access after start
response = admin_client.post(url, {'start_date': datetime.date(2020, 1, 1), 'start_time': datetime.time(10, 00),
'end_time': datetime.time(9, 00),
'access_at': datetime.datetime(2020, 1, 5, 10)})
assertFormError(response, 'form', 'end_time',
"Unless you've invented time travel, the event can't finish before it has started.")
assertFormError(response, 'form', 'access_at',
"Regardless of what some clients might think, access time cannot be after the event has started.")
def test_requires_valid_hmac(client, admin_user):
event = setup_event()
auth_data, hmac, url = setup_mail(event, admin_user)
@@ -138,7 +108,7 @@ def test_duplicate_warning(client, admin_user):
assertContains(response, 'amount has changed')
@pytest.mark.django_db(transaction=True)
@pytest.mark.django_db
def test_email_sent(admin_client, admin_user, mailoutbox):
event = setup_event()
auth_data, hmac, url = setup_mail(event, admin_user)
@@ -152,36 +122,36 @@ def test_email_sent(admin_client, admin_user, mailoutbox):
assert mailoutbox[1].to == [settings.AUTHORISATION_NOTIFICATION_ADDRESS]
class TECEventAuthorisationTest(BaseCase):
def setUp(self):
super().setUp()
self.url = reverse('event_authorise_request', kwargs={'pk': self.event.pk})
def test_email_check(admin_client, admin_user):
event = setup_event()
url = reverse('event_authorise_request', kwargs={'pk': event.pk})
admin_user.email = 'teccie@someotherdomain.com'
admin_user.save()
def test_email_check(self):
self.profile.email = 'teccie@someotherdomain.com'
self.profile.save()
response = admin_client.post(url)
response = self.client.post(self.url)
assertContains(response, 'must have an @nottinghamtec.co.uk email address')
self.assertContains(response, 'must have an @nottinghamtec.co.uk email address')
def test_request_send(self):
self.profile.email = 'teccie@nottinghamtec.co.uk'
self.profile.save()
response = self.client.post(self.url)
self.assertContains(response, 'This field is required.')
def test_request_send(admin_client, admin_user):
event = setup_event()
url = reverse('event_authorise_request', kwargs={'pk': event.pk})
admin_user.email = 'teccie@nottinghamtec.co.uk'
admin_user.save()
response = admin_client.post(url)
assertContains(response, 'This field is required.')
mail.outbox = []
mail.outbox = []
response = self.client.post(self.url, {'email': 'client@functional.test'})
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0]
self.assertIn('client@functional.test', email.to)
self.assertIn('/event/%d/' % (self.event.pk), email.body)
response = admin_client.post(url, {'email': 'client@functional.test'})
assert response.status_code == 302
assert len(mail.outbox) == 1
email = mail.outbox[0]
assert 'client@functional.test' in email.to
assert '/event/%d/' % event.pk in email.body
# Check sent by details are populated
self.event.refresh_from_db()
self.assertEqual(self.event.auth_request_by, self.profile)
self.assertEqual(self.event.auth_request_to, 'client@functional.test')
self.assertIsNotNone(self.event.auth_request_at)
# Check sent by details are populated
event.refresh_from_db()
assert event.auth_request_by == admin_user
assert event.auth_request_to == 'client@functional.test'
assert event.auth_request_at is not None

View File

@@ -15,6 +15,11 @@ from PyRIGS.tests.pages import animation_is_finished
from RIGS import models
from RIGS.tests import regions
from . import pages
import pytest
import time as t
pytestmark = pytest.mark.django_db(transaction=True)
@screenshot_failure_cls
@@ -307,13 +312,13 @@ class TestEventDuplicate(BaseRigboardTest):
# TODO Rewrite when EventDetail page is implemented
newEvent = models.Event.objects.latest('pk')
self.assertEqual(newEvent.auth_request_to, None)
assert newEvent.auth_request_to == ''
self.assertEqual(newEvent.auth_request_by, None)
self.assertEqual(newEvent.auth_request_at, None)
self.assertFalse(newEvent.authorised)
self.assertNotIn("N%05d" % self.testEvent.pk, self.driver.find_element_by_xpath('//h1').text)
self.assertNotIn("N%05d" % self.testEvent.pk, self.driver.find_element_by_xpath('//h2').text)
self.assertNotIn("Event data duplicated but not yet saved", self.page.warning) # Check info message not visible
# Check the new items are visible
@@ -445,7 +450,7 @@ class TestEventDetail(BaseRigboardTest):
self.assertIn("N%05d | %s" % (self.testEvent.pk, self.testEvent.name), self.page.event_name)
self.assertEqual(self.client.name, self.page.name)
self.assertEqual(self.client.email, self.page.email)
self.assertEqual(self.client.phone, None)
assert self.client.phone == ''
@screenshot_failure_cls
@@ -633,270 +638,190 @@ class TestCalendar(BaseRigboardTest):
else:
self.assertNotContains(response, "TE E" + str(test) + " ")
def test_calendar_buttons(self): # If FullCalendar fails to load for whatever reason, the buttons don't work
self.page = pages.CalendarPage(self.driver, self.live_server_url).open()
self.assertIn(timezone.now().strftime("%Y-%m"), self.driver.current_url)
target_date = datetime.date(2020, 1, 1)
self.page.target_date.set_value(target_date)
self.page.go()
self.assertIn(self.page.target_date.value.strftime("%Y-%m"), self.driver.current_url)
def test_calendar_buttons(logged_in_browser, live_server): # If FullCalendar fails to load for whatever reason, the buttons don't work
page = pages.CalendarPage(logged_in_browser.driver, live_server.url).open()
assert timezone.now().strftime("%Y-%m") in logged_in_browser.url
self.page.next()
target_date += datetime.timedelta(days=32)
self.assertIn(target_date.strftime("%m"), self.driver.current_url)
target_date = datetime.date(2020, 1, 1)
page.target_date.set_value(target_date)
page.go()
assert page.target_date.value.strftime("%Y-%m") in logged_in_browser.url
page.next()
target_date += datetime.timedelta(days=32)
assert target_date.strftime("%m") in logged_in_browser.url
@screenshot_failure_cls
class TestHealthAndSafety(BaseRigboardTest):
def setUp(self):
super().setUp()
self.profile = models.Profile.objects.get_or_create(
first_name='Test',
last_name='TEC User',
username='eventtest',
email='teccie@functional.test',
is_superuser=True # lazily grant all permissions
)[0]
self.venue = models.Venue.objects.create(name="Venue 1")
def test_ra_edit(logged_in_browser, live_server, ra):
page = pages.EditRiskAssessment(logged_in_browser.driver, live_server.url, pk=ra.pk).open()
page.nonstandard_equipment = nse = True
page.general_notes = gn = "There are some notes, but I've not written them here as that would be helpful"
page.submit()
assert not page.success
page.supervisor_consulted = True
page.submit()
assert page.success
# Check that data is right
ra = models.RiskAssessment.objects.get(pk=ra.pk)
assert ra.general_notes == gn
assert ra.nonstandard_equipment == nse
self.testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL,
start_date=date.today() + timedelta(days=6),
description="start future no end",
purchase_order='TESTPO',
person=self.client,
venue=self.venue)
self.testEvent2 = models.Event.objects.create(name="TE E2", status=models.Event.PROVISIONAL,
start_date=date.today() + timedelta(days=6),
description="start future no end",
purchase_order='TESTPO',
person=self.client,
venue=self.venue)
self.testEvent3 = models.Event.objects.create(name="TE E3", status=models.Event.PROVISIONAL,
start_date=date.today() + timedelta(days=6),
description="start future no end",
purchase_order='TESTPO',
person=self.client,
venue=self.venue)
self.testRA = models.RiskAssessment.objects.create(event=self.testEvent2, supervisor_consulted=False, 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=True,
safe_loading=True,
safe_storage=True,
area_outside_of_control=False,
barrier_required=False,
nonstandard_emergency_procedure=False,
special_structures=False,
suspended_structures=False,
outside=False)
self.testRA2 = models.RiskAssessment.objects.create(event=self.testEvent3, supervisor_consulted=False, nonstandard_equipment=False,
nonstandard_use=False,
contractors=False,
other_companies=False,
crew_fatigue=False,
big_power=True,
generators=False,
other_companies_power=False,
nonstandard_equipment_power=False,
multiple_electrical_environments=False,
noise_monitoring=False,
known_venue=True,
safe_loading=True,
safe_storage=True,
area_outside_of_control=False,
barrier_required=False,
nonstandard_emergency_procedure=False,
special_structures=False,
suspended_structures=False,
outside=False)
self.page = pages.EventDetail(self.driver, self.live_server_url, event_id=self.testEvent.pk).open()
# TODO Can I loop through all the boolean fields and test them at once?
def test_ra_creation(self):
self.page = pages.CreateRiskAssessment(self.driver, self.live_server_url, event_id=self.testEvent.pk).open()
def small_ec(page, admin_user):
page.safe_parking = True
page.safe_packing = True
page.exits = True
page.trip_hazard = True
page.warning_signs = True
page.ear_plugs = True
page.hs_location = "The Moon"
page.extinguishers_location = "With the rest of the fire"
# If we do this first the search fails, for ... reasons
page.power_mic.search(admin_user.name)
page.power_mic.toggle()
assert not page.power_mic.is_open
page.earthing = True
page.rcds = True
page.supply_test = True
page.pat = True
# Check there are no defaults
self.assertIsNone(self.page.nonstandard_equipment)
# No database side validation, only HTML5.
def test_ec_create_small(logged_in_browser, live_server, admin_user, ra):
page = pages.CreateEventChecklist(logged_in_browser.driver, live_server.url, event_id=ra.event.pk).open()
small_ec(page, admin_user)
page.submit()
assert page.success
self.page.nonstandard_equipment = False
self.page.nonstandard_use = False
self.page.contractors = False
self.page.other_companies = False
self.page.crew_fatigue = False
self.page.general_notes = "There are no notes."
self.page.big_power = False
self.page.outside = False
self.page.power_mic.search(self.profile.name)
self.page.power_mic.set_option(self.profile.name, True)
# TODO This should not be necessary, normally closes automatically
self.page.power_mic.toggle()
self.assertFalse(self.page.power_mic.is_open)
self.page.generators = False
self.page.other_companies_power = False
self.page.nonstandard_equipment_power = False
self.page.multiple_electrical_environments = False
self.page.power_notes = "Remember to bring some power"
self.page.noise_monitoring = False
self.page.sound_notes = "Loud, but not too loud"
self.page.known_venue = False
self.page.safe_loading = False
self.page.safe_storage = False
self.page.area_outside_of_control = False
self.page.barrier_required = False
self.page.nonstandard_emergency_procedure = False
self.page.special_structures = False
# self.page.persons_responsible_structures = "Nobody and her cat, She"
self.page.suspended_structures = True
# TODO Test for this proper
self.page.rigging_plan = "https://nottinghamtec.sharepoint.com/test/"
self.page.submit()
self.assertFalse(self.page.success)
def test_ec_create_medium(logged_in_browser, live_server, admin_user, medium_ra):
page = pages.CreateEventChecklist(logged_in_browser.driver, live_server.url, event_id=medium_ra.event.pk).open()
self.page.suspended_structures = False
self.page.submit()
self.assertTrue(self.page.success)
page.safe_parking = True
page.safe_packing = True
page.exits = True
page.trip_hazard = True
page.warning_signs = True
page.ear_plugs = True
page.hs_location = "Death Valley"
page.extinguishers_location = "With the rest of the fire"
# If we do this first the search fails, for ... reasons
page.power_mic.search(admin_user.name)
page.power_mic.toggle()
assert not page.power_mic.is_open
# Test that we can't make another one
self.page = pages.CreateRiskAssessment(self.driver, self.live_server_url, event_id=self.testEvent.pk).open()
self.assertIn('edit', self.driver.current_url)
# Gotta scroll to make the button clickable
logged_in_browser.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
def test_ra_edit(self):
self.page = pages.EditRiskAssessment(self.driver, self.live_server_url, pk=self.testRA.pk).open()
self.page.nonstandard_equipment = nse = True
self.page.general_notes = gn = "There are some notes, but I've not written them here as that would be helpful"
self.page.submit()
self.assertFalse(self.page.success)
self.page.supervisor_consulted = True
self.page.submit()
self.assertTrue(self.page.success)
# Check that data is right
ra = models.RiskAssessment.objects.get(pk=self.testRA.pk)
self.assertEqual(ra.general_notes, gn)
self.assertEqual(ra.nonstandard_equipment, nse)
page.earthing = True
page.pat = True
page.source_rcd = True
page.labelling = True
page.fd_voltage_l1 = 240
page.fd_voltage_l2 = 235
page.fd_voltage_l3 = 0
page.fd_phase_rotation = True
page.fd_earth_fault = "1.21"
page.fd_pssc = 1984
page.w1_description = "In the carpark, by the bins"
page.w1_polarity = True
page.w1_voltage = 240
page.w1_earth_fault = "0.42"
def test_ec_create_small(self):
self.page = pages.CreateEventChecklist(self.driver, self.live_server_url, event_id=self.testEvent2.pk).open()
page.submit()
assert page.success
self.page.safe_parking = True
self.page.safe_packing = True
self.page.exits = True
self.page.trip_hazard = True
self.page.warning_signs = True
self.page.ear_plugs = True
self.page.hs_location = "The Moon"
self.page.extinguishers_location = "With the rest of the fire"
# If we do this first the search fails, for ... reasons
self.page.power_mic.search(self.profile.name)
self.page.power_mic.toggle()
self.assertFalse(self.page.power_mic.is_open)
# Gotta scroll to make the button clickable
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
def test_ec_create_vehicle(logged_in_browser, live_server, admin_user, checklist):
page = pages.EditEventChecklist(logged_in_browser.driver, live_server.url, pk=checklist.pk).open()
small_ec(page, admin_user)
page.add_vehicle()
assert len(page.vehicles) == 1
vehicle_name = 'Brian'
page.vehicles[0].name.set_value(vehicle_name)
# Appears we're moving too fast for javascript...
t.sleep(1)
page.vehicles[0].vehicle.search(admin_user.first_name)
t.sleep(1)
page.submit()
assert page.success
# Check data is correct
checklist.refresh_from_db()
vehicle = models.EventChecklistVehicle.objects.get(checklist=checklist.pk)
assert vehicle_name == vehicle.vehicle
self.page.earthing = True
self.page.rcds = True
self.page.supply_test = True
self.page.pat = True
self.page.submit()
self.assertTrue(self.page.success)
# TODO Test validation of end before start
def test_ec_create_crew(logged_in_browser, live_server, admin_user, checklist):
page = pages.EditEventChecklist(logged_in_browser.driver, live_server.url, pk=checklist.pk).open()
small_ec(page, admin_user)
page.add_crew()
assert len(page.crew) == 1
role = "MIC"
start_time = timezone.make_aware(datetime.datetime(2015, 1, 1, 9, 0))
end_time = timezone.make_aware(datetime.datetime(2015, 1, 1, 10, 30))
crew = page.crew[0]
t.sleep(2)
crew.crewmember.search(admin_user.first_name)
t.sleep(2)
crew.role.set_value(role)
crew.start_time.set_value(start_time)
crew.end_time.set_value(end_time)
page.submit()
assert page.success
# Check data is correct
crew_obj = models.EventChecklistCrew.objects.get(checklist=checklist.pk)
assert admin_user.pk == crew_obj.crewmember.pk
assert role == crew_obj.role
assert start_time == crew_obj.start
assert end_time == crew_obj.end
def test_ec_create_medium(self):
self.page = pages.CreateEventChecklist(self.driver, self.live_server_url, event_id=self.testEvent3.pk).open()
self.page.safe_parking = True
self.page.safe_packing = True
self.page.exits = True
self.page.trip_hazard = True
self.page.warning_signs = True
self.page.ear_plugs = True
self.page.hs_location = "Death Valley"
self.page.extinguishers_location = "With the rest of the fire"
# If we do this first the search fails, for ... reasons
self.page.power_mic.search(self.profile.name)
self.page.power_mic.toggle()
self.assertFalse(self.page.power_mic.is_open)
# TODO Can I loop through all the boolean fields and test them at once?
def test_ra_creation(logged_in_browser, live_server, admin_user, basic_event):
page = pages.CreateRiskAssessment(logged_in_browser.driver, live_server.url, event_id=basic_event.pk).open()
# Gotta scroll to make the button clickable
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# Check there are no defaults
assert page.nonstandard_equipment is None
self.page.earthing = True
self.page.pat = True
self.page.source_rcd = True
self.page.labelling = True
self.page.fd_voltage_l1 = 240
self.page.fd_voltage_l2 = 235
self.page.fd_voltage_l3 = 0
self.page.fd_phase_rotation = True
self.page.fd_earth_fault = 666
self.page.fd_pssc = 1984
self.page.w1_description = "In the carpark, by the bins"
self.page.w1_polarity = True
self.page.w1_voltage = 240
self.page.w1_earth_fault = 333
# No database side validation, only HTML5.
page.nonstandard_equipment = False
page.nonstandard_use = False
page.contractors = False
page.other_companies = False
page.crew_fatigue = False
page.general_notes = "There are no notes."
page.big_power = False
page.outside = False
page.power_mic.search(admin_user.first_name)
page.generators = False
page.other_companies_power = False
page.nonstandard_equipment_power = False
page.multiple_electrical_environments = False
page.power_notes = "Remember to bring some power"
page.noise_monitoring = False
page.sound_notes = "Loud, but not too loud"
page.known_venue = False
page.safe_loading = False
page.safe_storage = False
page.area_outside_of_control = False
page.barrier_required = False
page.nonstandard_emergency_procedure = False
page.special_structures = False
# self.page.persons_responsible_structures = "Nobody and her cat, She"
self.page.submit()
self.assertTrue(self.page.success)
page.suspended_structures = True
# TODO Test for this proper
page.rigging_plan = "https://nottinghamtec.sharepoint.com/test/"
page.submit()
assert not page.success
def test_ec_create_extras(self):
eid = self.testEvent2.pk
self.page = pages.CreateEventChecklist(self.driver, self.live_server_url, event_id=eid).open()
self.page.add_vehicle()
self.page.add_crew()
page.suspended_structures = False
page.submit()
assert page.success
self.page.safe_parking = True
self.page.safe_packing = True
self.page.exits = True
self.page.trip_hazard = True
self.page.warning_signs = True
self.page.ear_plugs = True
self.page.hs_location = "The Moon"
self.page.extinguishers_location = "With the rest of the fire"
# If we do this first the search fails, for ... reasons
self.page.power_mic.search(self.profile.name)
self.page.power_mic.toggle()
self.assertFalse(self.page.power_mic.is_open)
vehicle_name = 'Brian'
self.driver.find_element(By.XPATH, '//*[@name="vehicle_-1"]').send_keys(vehicle_name)
driver = base_regions.BootstrapSelectElement(self.page, self.driver.find_element(By.XPATH, '//tr[@id="vehicles_-1"]//div[contains(@class, "bootstrap-select")]'))
driver.search(self.profile.name)
crew = self.profile
role = "MIC"
crew_select = base_regions.BootstrapSelectElement(self.page, self.driver.find_element(By.XPATH, '//tr[@id="crew_-1"]//div[contains(@class, "bootstrap-select")]'))
start_time = base_regions.DateTimePicker(self.page, self.driver.find_element(By.XPATH, '//*[@name="start_-1"]'))
end_time = base_regions.DateTimePicker(self.page, self.driver.find_element(By.XPATH, '//*[@name="end_-1"]'))
start_time.set_value(timezone.make_aware(datetime.datetime(2015, 1, 1, 9, 0)))
# TODO Test validation of end before start
end_time.set_value(timezone.make_aware(datetime.datetime(2015, 1, 1, 10, 30)))
crew_select.search(crew.name)
self.driver.find_element(By.XPATH, '//*[@name="role_-1"]').send_keys(role)
self.page.earthing = True
self.page.rcds = True
self.page.supply_test = True
self.page.pat = True
self.page.submit()
self.assertTrue(self.page.success)
checklist = models.EventChecklist.objects.get(event=eid)
vehicle = models.EventChecklistVehicle.objects.get(checklist=checklist.pk)
self.assertEqual(vehicle_name, vehicle.vehicle)
crew_obj = models.EventChecklistCrew.objects.get(checklist=checklist.pk)
self.assertEqual(crew.pk, crew_obj.crewmember.pk)
self.assertEqual(role, crew_obj.role)
def test_ra_no_duplicates(logged_in_browser, live_server, ra):
# Test that we can't make another one
page = pages.CreateRiskAssessment(logged_in_browser.driver, live_server.url, event_id=ra.event.pk).open()
assert 'edit' in logged_in_browser.url

View File

@@ -2,6 +2,7 @@ from datetime import date, timedelta, datetime, time
from decimal import *
import pytz
import pytest
from django.conf import settings
from django.test import TestCase
from reversion import revisions as reversion
@@ -9,110 +10,56 @@ from reversion import revisions as reversion
from RIGS import models
class ProfileTestCase(TestCase):
def test_str(self):
profile = models.Profile(first_name='Test', last_name='Case')
self.assertEqual(str(profile), 'Test Case')
profile.initials = 'TC'
self.assertEqual(str(profile), 'Test Case "TC"')
def assert_decimal_equality(d1, d2):
assert float(d1) == pytest.approx(float(d2))
class VatRateTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.rates = {
0: models.VatRate.objects.create(start_at='2014-03-01', rate=0.20, comment='test1'),
1: models.VatRate.objects.create(start_at='2016-03-01', rate=0.15, comment='test2'),
}
def test_find_correct(self):
r = models.VatRate.objects.find_rate('2015-03-01')
self.assertEqual(r, self.rates[0])
r = models.VatRate.objects.find_rate('2016-03-01')
self.assertEqual(r, self.rates[1])
def test_percent_correct(self):
self.assertEqual(self.rates[0].as_percent, 20)
def test_str():
profile = models.Profile(first_name='Test', last_name='Case')
assert str(profile) == 'Test Case'
profile.initials = 'TC'
assert str(profile) == 'Test Case "TC"'
class EventTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.all_events = set(range(1, 18))
cls.current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18)
cls.not_current_events = set(cls.all_events) - set(cls.current_events)
@pytest.mark.django_db
def test_find_correct(vat_rate):
new_rate = models.VatRate.objects.create(start_at='2016-03-01', rate=0.15, comment='test2')
r = models.VatRate.objects.find_rate('2015-03-01')
assert_decimal_equality(r.rate, vat_rate.rate)
r = models.VatRate.objects.find_rate('2016-03-01')
assert_decimal_equality(r.rate, new_rate.rate)
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com")
cls.events = {
# produce 7 normal events - 5 current
1: models.Event.objects.create(name="TE E1", start_date=date.today() + timedelta(days=6),
description="start future no end"),
2: models.Event.objects.create(name="TE E2", start_date=date.today(), description="start today no end"),
3: models.Event.objects.create(name="TE E3", start_date=date.today(), end_date=date.today(),
description="start today with end today"),
4: models.Event.objects.create(name="TE E4", start_date='2014-03-20', description="start past no end"),
5: models.Event.objects.create(name="TE E5", start_date='2014-03-20', end_date='2014-03-21',
description="start past with end past"),
6: models.Event.objects.create(name="TE E6", start_date=date.today() - timedelta(days=2),
end_date=date.today() + timedelta(days=2),
description="start past, end future"),
7: models.Event.objects.create(name="TE E7", start_date=date.today() + timedelta(days=2),
end_date=date.today() + timedelta(days=2),
description="start + end in future"),
def test_percent_correct(vat_rate):
assert vat_rate.as_percent == 20
# 2 cancelled - 1 current
8: models.Event.objects.create(name="TE E8", start_date=date.today() + timedelta(days=2),
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
description="cancelled in future"),
9: models.Event.objects.create(name="TE E9", start_date=date.today() - timedelta(days=1),
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
description="cancelled and started"),
# 5 dry hire - 3 current
10: models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True,
description="dryhire today"),
11: models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True,
checked_in_by=cls.profile,
description="dryhire today, checked in"),
12: models.Event.objects.create(name="TE E12", start_date=date.today() - timedelta(days=1), dry_hire=True,
status=models.Event.BOOKED, description="dryhire past"),
13: models.Event.objects.create(name="TE E13", start_date=date.today() - timedelta(days=2), dry_hire=True,
checked_in_by=cls.profile, description="dryhire past checked in"),
14: models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True,
status=models.Event.CANCELLED, description="dryhire today cancelled"),
def test_related_vatrate(basic_event, vat_rate):
assert_decimal_equality(vat_rate.rate, basic_event.vat_rate.rate)
# 4 non rig - 3 current
15: models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False,
description="non rig today"),
16: models.Event.objects.create(name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False,
description="non rig tomorrow"),
17: models.Event.objects.create(name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False,
description="non rig yesterday"),
18: models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False,
status=models.Event.CANCELLED,
description="non rig today cancelled"),
}
def test_count(self):
# Santiy check we have the expected events created
self.assertEqual(models.Event.objects.count(), 18, "Incorrect number of events, check setup")
class EventTest():
def test_count(many_events):
# Sanity check we have the expected events created
assert models.Event.objects.count() == 18
def test_rig_count(self):
def test_rig_count(many_events):
# Changed to not include unreturned dry hires in rig count
self.assertEqual(models.Event.objects.rig_count(), 7)
assert models.Event.objects.rig_count() == 7
def test_current_events(self):
def test_current_events(many_events):
all_events = set(range(1, 18))
current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18)
not_current_events = set(cls.all_events) - set(cls.current_events)
current_events = models.Event.objects.current_events()
self.assertEqual(len(current_events), len(self.current_events))
for eid in self.current_events:
self.assertIn(models.Event.objects.get(name="TE E%d" % eid), current_events)
assert len(current_events) == len(self.current_events)
for eid in current_events:
assert models.Event.objects.get(name="TE E%d" % eid) in current_events
for eid in self.not_current_events:
self.assertNotIn(models.Event.objects.get(name="TE E%d" % eid), current_events)
for eid in not_current_events:
assert models.Event.objects.get(name="TE E%d" % eid) not in current_events
def test_related_venue(self):
def test_related(many_events):
v1 = models.Venue.objects.create(name="TE V1")
v2 = models.Venue.objects.create(name="TE V2")
@@ -127,16 +74,13 @@ class EventTestCase(TestCase):
e2.append(event)
event.save()
self.assertCountEqual(e1, v1.latest_events)
self.assertCountEqual(e2, v2.latest_events)
assert set(e1) == set(v1.latest_events)
assert set(e2) == set(v2.latest_events)
# Cleanup
v1.delete()
v2.delete()
for (key, event) in self.events.items():
event.venue = None
def test_related_vatrate(self):
self.assertEqual(self.vatrate, models.Event.objects.all()[0].vat_rate)
def test_related_person(self):
def test_related_person(many_events):
p1 = models.Person.objects.create(name="TE P1")
p2 = models.Person.objects.create(name="TE P2")
@@ -151,13 +95,13 @@ class EventTestCase(TestCase):
e2.append(event)
event.save()
self.assertCountEqual(e1, p1.latest_events)
self.assertCountEqual(e2, p2.latest_events)
assert set(e1) == set(p1.latest_events)
assert set(e2) == set(p2.latest_events)
for (key, event) in self.events.items():
event.person = None
p1.delete()
p2.delete()
def test_related_organisation(self):
def test_related_organisation(many_events):
o1 = models.Organisation.objects.create(name="TE O1")
o2 = models.Organisation.objects.create(name="TE O2")
@@ -172,13 +116,13 @@ class EventTestCase(TestCase):
e2.append(event)
event.save()
self.assertCountEqual(e1, o1.latest_events)
self.assertCountEqual(e2, o2.latest_events)
assert set(e1) == set(o1.latest_events)
assert set(e1) == set(o2.latest_events)
for (key, event) in self.events.items():
event.organisation = None
def test_organisation_person_join(self):
def test_organisation_person_join(many_events):
p1 = models.Person.objects.create(name="TE P1")
p2 = models.Person.objects.create(name="TE P2")
o1 = models.Organisation.objects.create(name="TE O1")
@@ -202,105 +146,109 @@ class EventTestCase(TestCase):
events = models.Event.objects.all()
# Check person's organisations
self.assertIn((o1, 2), p1.organisations)
self.assertIn((o2, 1), p1.organisations)
self.assertIn((o1, 2), p2.organisations)
self.assertEqual(len(p2.organisations), 1)
assert (o1, 2) in p1.organisations
assert (o2, 1) in p1.organisations
assert (o1, 2) in p2.organisations
assert len(p2.organisations) == 1
# Check organisation's persons
self.assertIn((p1, 2), o1.persons)
self.assertIn((p2, 2), o1.persons)
self.assertIn((p1, 1), o2.persons)
self.assertEqual(len(o2.persons), 1)
assert (p1, 2) in o1.persons
assert (p2, 2) in o1.persons
assert (p1, 1) in o2.persons
assert len(o2.persons) == 1
def test_cancelled_property(self):
edit = self.events[1]
def test_cancelled_property(many_events):
edit = many_events[1]
edit.status = models.Event.CANCELLED
edit.save()
event = models.Event.objects.get(pk=edit.pk)
self.assertEqual(event.status, models.Event.CANCELLED)
self.assertTrue(event.cancelled)
assert event.status == models.Event.CANCELLED
assert event.cancelled
event.status = models.Event.PROVISIONAL
event.save()
def test_confirmed_property(self):
edit = self.events[1]
def test_confirmed_property(many_events):
edit = many_events[1]
edit.status = models.Event.CONFIRMED
edit.save()
event = models.Event.objects.get(pk=edit.pk)
self.assertEqual(event.status, models.Event.CONFIRMED)
self.assertTrue(event.confirmed)
assert event.status == models.Event.CONFIRMED
assert event.confirmed
event.status = models.Event.PROVISIONAL
event.save()
def test_earliest_time(self):
event = models.Event(name="TE ET", start_date=date(2016, 0o1, 0o1))
# Just a start date
self.assertEqual(event.earliest_time, date(2016, 0o1, 0o1))
def test_earliest_time():
event = models.Event(name="TE ET", start_date=date(2016, 0o1, 0o1))
# With start time
event.start_time = time(9, 00)
self.assertEqual(event.earliest_time, self.create_datetime(2016, 1, 1, 9, 00))
# Just a start date
assert event.earliest_time == date(2016, 0o1, 0o1)
# With access time
event.access_at = self.create_datetime(2015, 12, 0o3, 9, 57)
self.assertEqual(event.earliest_time, event.access_at)
# With start time
event.start_time = time(9, 00)
assert event.earliest_time == create_datetime(2016, 1, 1, 9, 00)
# With meet time
event.meet_at = self.create_datetime(2015, 12, 0o3, 9, 55)
self.assertEqual(event.earliest_time, event.meet_at)
# With access time
event.access_at = create_datetime(2015, 12, 0o3, 9, 57)
assert event.earliest_time == event.access_at
# Check order isn't important
event.start_date = date(2015, 12, 0o3)
self.assertEqual(event.earliest_time, self.create_datetime(2015, 12, 0o3, 9, 00))
# With meet time
event.meet_at = create_datetime(2015, 12, 0o3, 9, 55)
assert event.earliest_time == event.meet_at
def test_latest_time(self):
event = models.Event(name="TE LT", start_date=date(2016, 0o1, 0o1))
# Check order isn't important
event.start_date = date(2015, 12, 0o3)
assert event.earliest_time == create_datetime(2015, 12, 0o3, 9, 00)
# Just start date
self.assertEqual(event.latest_time, event.start_date)
# Just end date
event.end_date = date(2016, 1, 2)
self.assertEqual(event.latest_time, event.end_date)
def test_latest_time():
event = models.Event(name="TE LT", start_date=date(2016, 0o1, 0o1))
# With end time
event.end_time = time(23, 00)
self.assertEqual(event.latest_time, self.create_datetime(2016, 1, 2, 23, 00))
# Just start date
assert event.latest_time == event.start_date
def test_in_bounds(self):
manager = models.Event.objects
events = [
manager.create(name="TE IB0", start_date='2016-01-02'), # yes no
manager.create(name="TE IB1", start_date='2015-12-31', end_date='2016-01-04'),
# Just end date
event.end_date = date(2016, 1, 2)
assert event.latest_time == event.end_date
# basic checks
manager.create(name='TE IB2', start_date='2016-01-02', end_date='2016-01-04'),
manager.create(name='TE IB3', start_date='2015-12-31', end_date='2016-01-03'),
manager.create(name='TE IB4', start_date='2016-01-04',
access_at=self.create_datetime(2016, 0o1, 0o3, 00, 00)),
manager.create(name='TE IB5', start_date='2016-01-04',
meet_at=self.create_datetime(2016, 0o1, 0o2, 00, 00)),
# With end time
event.end_time = time(23, 00)
assert event.latest_time == create_datetime(2016, 1, 2, 23, 00)
# negative check
manager.create(name='TE IB6', start_date='2015-12-31', end_date='2016-01-01'),
]
in_bounds = manager.events_in_bounds(self.create_datetime(2016, 1, 2, 0, 0),
self.create_datetime(2016, 1, 3, 0, 0))
self.assertIn(events[0], in_bounds)
self.assertIn(events[1], in_bounds)
self.assertIn(events[2], in_bounds)
self.assertIn(events[3], in_bounds)
self.assertIn(events[4], in_bounds)
self.assertIn(events[5], in_bounds)
def test_in_bounds():
manager = models.Event.objects
events = [
manager.create(name="TE IB0", start_date='2016-01-02'), # yes no
manager.create(name="TE IB1", start_date='2015-12-31', end_date='2016-01-04'),
self.assertNotIn(events[6], in_bounds)
# basic checks
manager.create(name='TE IB2', start_date='2016-01-02', end_date='2016-01-04'),
manager.create(name='TE IB3', start_date='2015-12-31', end_date='2016-01-03'),
manager.create(name='TE IB4', start_date='2016-01-04',
access_at=create_datetime(2016, 0o1, 0o3, 00, 00)),
manager.create(name='TE IB5', start_date='2016-01-04',
meet_at=create_datetime(2016, 0o1, 0o2, 00, 00)),
def create_datetime(self, year, month, day, hour, min):
tz = pytz.timezone(settings.TIME_ZONE)
return tz.localize(datetime(year, month, day, hour, min))
# negative check
manager.create(name='TE IB6', start_date='2015-12-31', end_date='2016-01-01'),
]
in_bounds = manager.events_in_bounds(create_datetime(2016, 1, 2, 0, 0),
create_datetime(2016, 1, 3, 0, 0))
assert events[0] in in_bounds
assert events[1], in_bounds
assert events[2], in_bounds
assert events[3], in_bounds
assert events[4], in_bounds
assert events[5], in_bounds
assert events[6] not in in_bounds
def create_datetime(year, month, day, hour, minute):
tz = pytz.timezone(settings.TIME_ZONE)
return tz.localize(datetime(year, month, day, hour, minute))
class EventItemTestCase(TestCase):
@@ -331,7 +279,6 @@ class EventItemTestCase(TestCase):
class EventPricingTestCase(TestCase):
def setUp(self):
models.VatRate.objects.create(rate=0.20, comment="TP V1", start_at='2013-01-01')
models.VatRate.objects.create(rate=0.10, comment="TP V2", start_at=date.today() - timedelta(days=1))
self.e1 = models.Event.objects.create(name="TP E1", start_date=date.today() - timedelta(days=2))
self.e2 = models.Event.objects.create(name="TP E2", start_date=date.today())
@@ -364,7 +311,6 @@ class EventPricingTestCase(TestCase):
class EventAuthorisationTestCase(TestCase):
def setUp(self):
models.VatRate.objects.create(rate=0.20, comment="TP V1", start_at='2013-01-01')
self.profile = models.Profile.objects.get_or_create(
first_name='Test',
last_name='TEC User',

View File

@@ -1,23 +1,27 @@
from datetime import date
from django.core.exceptions import ObjectDoesNotExist
from django.core.management import call_command
from django.test import TestCase
from django.test.utils import override_settings
from django.utils.safestring import SafeText
from RIGS.templatetags.markdown_tags import markdown_filter
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from pytest_django.asserts import assertRedirects, assertNotContains, assertContains
from PyRIGS.tests.base import assert_times_equal
from PyRIGS.tests.base import assert_times_almost_equal, assert_oembed, login
from RIGS import models
import pytest
pytestmark = pytest.mark.django_db
class TestAdminMergeObjects(TestCase):
@classmethod
def setUpTestData(cls):
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
is_active=True, is_staff=True)
cls.persons = {
1: models.Person.objects.create(name="Person 1"),
2: models.Person.objects.create(name="Person 2"),
@@ -168,9 +172,7 @@ class TestInvoiceDelete(TestCase):
def setUpTestData(cls):
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
is_active=True, is_staff=True)
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
cls.events = {
1: models.Event.objects.create(name="TE E1", start_date=date.today()),
2: models.Event.objects.create(name="TE E2", start_date=date.today())
@@ -201,7 +203,7 @@ class TestInvoiceDelete(TestCase):
self.assertTrue(models.Invoice.objects.get(pk=self.invoices[2].pk))
# Actually delete it
response = self.client.post(request_url, follow=True)
self.client.post(request_url, follow=True)
# Check the invoice is deleted
self.assertRaises(ObjectDoesNotExist, models.Invoice.objects.get, pk=self.invoices[2].pk)
@@ -216,7 +218,7 @@ class TestInvoiceDelete(TestCase):
self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk))
# Try to actually delete it
response = self.client.post(request_url, follow=True)
self.client.post(request_url, follow=True)
# Check this didn't work
self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk))
@@ -227,9 +229,6 @@ class TestPrintPaperwork(TestCase):
def setUpTestData(cls):
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
is_active=True, is_staff=True)
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
cls.events = {
1: models.Event.objects.create(name="TE E1", start_date=date.today(),
description="This is an event description\nthat for a very specific reason spans two lines."),
@@ -257,102 +256,50 @@ class TestPrintPaperwork(TestCase):
self.assertEqual(response.status_code, 200)
class TestEmbeddedViews(TestCase):
@classmethod
def setUpTestData(cls):
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
is_active=True, is_staff=True)
def test_login_redirect(client, django_user_model):
request_url = reverse('event_embed', kwargs={'pk': 1})
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
cls.events = {
1: models.Event.objects.create(name="TE E1", start_date=date.today()),
2: models.Event.objects.create(name="TE E2", start_date=date.today())
}
# Request the page and check it redirects
response = client.get(request_url, follow=True)
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
cls.invoices = {
1: models.Invoice.objects.create(event=cls.events[1]),
2: models.Invoice.objects.create(event=cls.events[2])
}
# Now login
login(client, django_user_model)
cls.payments = {
1: models.Payment.objects.create(invoice=cls.invoices[1], date=date.today(), amount=12.34,
method=models.Payment.CASH)
}
def setUp(self):
self.profile.set_password('testuser')
self.profile.save()
def testLoginRedirect(self):
request_url = reverse('event_embed', kwargs={'pk': 1})
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
# Request the page and check it redirects
response = self.client.get(request_url, follow=True)
self.assertRedirects(response, expected_url, status_code=302, target_status_code=200)
# Now login
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
# And check that it no longer redirects
response = self.client.get(request_url, follow=True)
self.assertEqual(len(response.redirect_chain), 0)
def testLoginCookieWarning(self):
login_url = reverse('login_embed')
response = self.client.post(login_url, follow=True)
self.assertContains(response, "Cookies do not seem to be enabled")
def testXFrameHeaders(self):
event_url = reverse('event_embed', kwargs={'pk': 1})
login_url = reverse('login_embed')
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
response = self.client.get(event_url, follow=True)
with self.assertRaises(KeyError):
response._headers["X-Frame-Options"]
response = self.client.get(login_url, follow=True)
with self.assertRaises(KeyError):
response._headers["X-Frame-Options"]
def testOEmbed(self):
event_url = reverse('event_detail', kwargs={'pk': 1})
event_embed_url = reverse('event_embed', kwargs={'pk': 1})
oembed_url = reverse('event_oembed', kwargs={'pk': 1})
alt_oembed_url = reverse('event_oembed', kwargs={'pk': 999})
alt_event_embed_url = reverse('event_embed', kwargs={'pk': 999})
# Test the meta tag is in place
response = self.client.get(event_url, follow=True, HTTP_HOST='example.com')
self.assertContains(response, '<link rel="alternate" type="application/json+oembed"')
self.assertContains(response, oembed_url)
# Test that the JSON exists
response = self.client.get(oembed_url, follow=True, HTTP_HOST='example.com')
self.assertEqual(response.status_code, 200)
self.assertContains(response, event_embed_url)
# Should also work for non-existant events
response = self.client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
self.assertEqual(response.status_code, 200)
self.assertContains(response, alt_event_embed_url)
# And check that it no longer redirects
response = client.get(request_url, follow=True)
assert len(response.redirect_chain) == 0
class TestSampleDataGenerator(TestCase):
@override_settings(DEBUG=True)
def test_generate_sample_data(self):
# Run the management command and check there are no exceptions
call_command('generateSampleRIGSData')
def test_login_cookie_warning(client):
login_url = reverse('login_embed')
response = client.post(login_url, follow=True)
assertContains(response, "Cookies do not seem to be enabled")
# Check there are lots of events
self.assertTrue(models.Event.objects.all().count() > 100)
def test_production_exception(self):
from django.core.management.base import CommandError
def test_xframe_headers(admin_client, basic_event):
event_url = reverse('event_embed', kwargs={'pk': basic_event.pk})
login_url = reverse('login_embed')
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleRIGSData')
response = admin_client.get(event_url, follow=True)
with pytest.raises(KeyError):
response.headers["X-Frame-Options"]
response = admin_client.get(login_url, follow=True)
with pytest.raises(KeyError):
response.headers["X-Frame-Options"]
def test_oembed(client, basic_event):
event_url = reverse('event_detail', kwargs={'pk': basic_event.pk})
event_embed_url = reverse('event_embed', kwargs={'pk': basic_event.pk})
oembed_url = reverse('event_oembed', kwargs={'pk': basic_event.pk})
alt_oembed_url = reverse('event_oembed', kwargs={'pk': 999})
alt_event_embed_url = reverse('event_embed', kwargs={'pk': 999})
assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url)
def search(client, url, found, notfound, arguments):
@@ -391,45 +338,11 @@ def test_search(admin_client):
['name', 'id', 'address'])
def setup_for_hs():
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
venue = models.Venue.objects.create(name="Venue 1")
return venue, {
1: models.Event.objects.create(name="TE E1", start_date=date.today(),
description="This is an event description\nthat for a very specific reason spans two lines.",
venue=venue),
2: models.Event.objects.create(name="TE E2", start_date=date.today()),
}
def create_ra(usr):
venue, events = setup_for_hs()
return models.RiskAssessment.objects.create(event=events[1], nonstandard_equipment=False, nonstandard_use=False,
contractors=False, other_companies=False, crew_fatigue=False,
big_power=False, power_mic=usr, generators=False,
other_companies_power=False, nonstandard_equipment_power=False,
multiple_electrical_environments=False, noise_monitoring=False,
known_venue=True, safe_loading=True, safe_storage=True,
area_outside_of_control=True, barrier_required=True,
nonstandard_emergency_procedure=True, special_structures=False,
suspended_structures=False, outside=False)
def create_checklist(usr):
venue, events = setup_for_hs()
return models.EventChecklist.objects.create(event=events[1], power_mic=usr, safe_parking=False,
safe_packing=False, exits=False, trip_hazard=False, warning_signs=False,
ear_plugs=False, hs_location="Locked away safely",
extinguishers_location="Somewhere, I forgot", earthing=False, pat=False,
date=timezone.now(), venue=venue)
def test_list(admin_client):
venue, events = setup_for_hs()
def test_hs_list(admin_client, basic_event):
request_url = reverse('hs_list')
response = admin_client.get(request_url, follow=True)
assertContains(response, events[1].name)
assertContains(response, events[2].name)
assertContains(response, basic_event.name)
# assertContains(response, events[2].name)
assertContains(response, 'Create')
@@ -439,21 +352,229 @@ def review(client, profile, obj, request_url):
obj.refresh_from_db()
assertContains(response, 'Reviewed by')
assertContains(response, profile.name)
assert_times_equal(time, obj.reviewed_at)
assert_times_almost_equal(time, obj.reviewed_at)
def test_ra_review(admin_client, admin_user):
review(admin_client, admin_user, create_ra(admin_user), 'ra_review')
def test_ra_review(admin_client, admin_user, ra):
review(admin_client, admin_user, ra, 'ra_review')
def test_checklist_review(admin_client, admin_user):
review(admin_client, admin_user, create_checklist(admin_user), 'ec_review')
def test_checklist_review(admin_client, admin_user, checklist):
review(admin_client, admin_user, checklist, 'ec_review')
def test_ra_redirect(admin_client, admin_user):
ra = create_ra(admin_user)
def test_ra_redirect(admin_client, admin_user, ra):
request_url = reverse('event_ra', kwargs={'pk': ra.event.pk})
expected_url = reverse('ra_edit', kwargs={'pk': ra.pk})
response = admin_client.get(request_url, follow=True)
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
class TestMarkdownTemplateTags(TestCase):
markdown = """
An h1 header
============
Paragraphs are separated by a blank line.
2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists
look like:
* this one
* that one
* the other one
Note that --- not considering the asterisk --- the actual text
content starts at 4-columns in.
> Block quotes are
> written like so.
>
> They can span multiple paragraphs,
> if you like.
Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all
in chapters 12--14"). Three dots ... will be converted to an ellipsis.
Unicode is supported.
An h2 header
------------
Here's a numbered list:
1. first item
2. second item
3. third item
Note again how the actual text starts at 4 columns in (4 characters
from the left side). Here's a code sample:
# Let me re-iterate ...
for i in 1 .. 10 { do-something(i) }
As you probably guessed, indented 4 spaces. By the way, instead of
indenting the block, you can use delimited blocks, if you like:
~~~
define foobar() {
print "Welcome to flavor country!";
}
~~~
(which makes copying & pasting easier). You can optionally mark the
delimited block for Pandoc to syntax highlight it:
~~~python
import time
# Quick, count to ten!
for i in range(10):
# (but not *too* quick)
time.sleep(0.5)
print i
~~~
### An h3 header ###
Now a nested list:
1. First, get these ingredients:
* carrots
* celery
* lentils
2. Boil some water.
3. Dump everything in the pot and follow
this algorithm:
find wooden spoon
uncover pot
stir
cover pot
balance wooden spoon precariously on pot handle
wait 10 minutes
goto first step (or shut off burner when done)
Do not bump wooden spoon or it will fall.
Notice again how text always lines up on 4-space indents (including
that last line which continues item 3 above).
Here's a link to [a website](http://foo.bar). Here's a footnote [^1].
[^1]: Footnote text goes here.
Tables can look like this:
size material color
---- ------------ ------------
9 leather brown
10 hemp canvas natural
11 glass transparent
Table: Shoes, their sizes, and what they're made of
(The above is the caption for the table.) Pandoc also supports
multi-line tables:
-------- -----------------------
keyword text
-------- -----------------------
red Sunsets, apples, and
other red or reddish
things.
green Leaves, grass, frogs
and other things it's
not easy being.
-------- -----------------------
A horizontal rule follows.
***
Here's a definition list:
apples
: Good for making applesauce.
oranges
: Citrus!
tomatoes
: There's no "e" in tomatoe.
Again, text is indented 4 spaces. (Put a blank line between each
term/definition pair to spread things out more.)
Here's a "line block":
| Line one
| Line too
| Line tree
and images can be specified like so:
![example image](example-image.jpg "An exemplary image")
Inline math equations go in like so: $\\omega = d\\phi / dt$. Display
math should get its own line and be put in in double-dollarsigns:
$$I = \\int \rho R^{2} dV$$
And note that you can backslash-escape any punctuation characters
which you wish to be displayed literally, ex.: \\`foo\\`, \\*bar\\*, etc.
"""
def test_html_safe(self):
html = markdown_filter(self.markdown)
self.assertIsInstance(html, SafeText)
def test_img_strip(self):
rml = markdown_filter(self.markdown, 'rml')
self.assertNotIn("<img", rml)
def test_code(self):
rml = markdown_filter(self.markdown, 'rml')
self.assertIn('<font face="Courier">monospace</font>', rml)
def test_blockquote(self):
rml = markdown_filter(self.markdown, 'rml')
self.assertIn("<pre>\nBlock quotes", rml)
def test_lists(self):
rml = markdown_filter(self.markdown, 'rml')
self.assertIn("<li><para>second item</para></li>", rml) # <ol>
self.assertIn("<li><para>that one</para></li>", rml) # <ul>
def test_in_print(self):
event = models.Event.objects.create(
name="MD Print Test",
description=self.markdown,
start_date='2016-01-01',
)
user = models.Profile.objects.create(
username='RML test',
is_superuser=True, # Don't care about permissions
is_active=True,
)
user.set_password('rmltester')
user.save()
self.assertTrue(self.client.login(username=user.username, password='rmltester'))
response = self.client.get(reverse('event_print', kwargs={'pk': event.pk}))
self.assertEqual(response.status_code, 200)
# By the time we have a PDF it should be larger than the original by some margin
# RML hard fails if something doesn't work
self.assertGreater(len(response.content), len(self.markdown))
def test_nonetype(self):
html = markdown_filter(None)
self.assertIsNone(html)
def test_linebreaks(self):
html = markdown_filter(self.markdown)
self.assertIn("Itemized lists<br/>\nlook like", html)

View File

@@ -1,5 +1,4 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import login_required
from django.urls import path, re_path
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import RedirectView
@@ -62,7 +61,7 @@ urlpatterns = [
path('event/<int:pk>/embed/',
xframe_options_exempt(login_required(login_url='/user/login/embed/')(rigboard.EventEmbed.as_view())),
name='event_embed'),
path('event/<int:pk>/oembed_json/', rigboard.EventOembed.as_view(),
path('event/<int:pk>/oembed_json/', rigboard.EventOEmbed.as_view(),
name='event_oembed'),
path('event/<int:pk>/print/', permission_required_with_403('RIGS.view_event')(rigboard.EventPrint.as_view()),
name='event_print'),
@@ -133,6 +132,8 @@ urlpatterns = [
name='event_authorise_preview'),
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', rigboard.EventAuthorise.as_view(),
name='event_authorise'),
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/preview/$', rigboard.EventAuthorise.as_view(preview=True),
name='event_authorise_form_preview'),
# ICS Calendar - API key authentication
re_path(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),

View File

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

View File

@@ -2,8 +2,7 @@
"name": "PyRIGS",
"description": "",
"scripts": {
"postdeploy": "python manage.py migrate && python manage.py generateSampleData",
"heroku-cleanup": "rm -rf node_modules pipeline"
"postdeploy": "python manage.py migrate && python manage.py generateSampleData"
},
"stack": "heroku-20",
"env": {
@@ -52,7 +51,7 @@
"url": "heroku/nodejs"
},
{
"url": "heroku/python"
"url": "https://github.com/nottinghamtec/heroku-buildpack-python"
}
]
}

8
assets/apps.py Normal file
View File

@@ -0,0 +1,8 @@
from django.apps import AppConfig
class AssetsAppConfig(AppConfig):
name = 'assets'
def ready(self):
import assets.signals

24
assets/converters.py Normal file
View File

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

View File

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

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