Compare commits

...

170 Commits

Author SHA1 Message Date
6b19d0e8b8 Fix has_required_levels logic being backward 2022-01-03 15:31:54 +00:00
3e8cfe4f11 Repair confirmation logic 2022-01-03 15:28:17 +00:00
7a70270dfd Add ability to view other users progress on a level
That's kind of important huh :p
2022-01-03 14:59:30 +00:00
5160eb7f78 Add confirm button stuff 2022-01-03 14:38:43 +00:00
99e05d91bb Simpler training level list display 2022-01-02 20:55:32 +00:00
f094ace862 Make level description a text field 2022-01-02 20:53:33 +00:00
945fb393c0 Display prerequisite item requirements on level detail 2022-01-02 20:31:23 +00:00
c6157d3e2b Various fixes 2022-01-02 19:15:44 +00:00
2767777d0e Add ability to edit past training records 2022-01-02 15:39:10 +00:00
19e6585e26 Fix importer not working for notes 2022-01-02 15:03:11 +00:00
747575b968 Add ability to search detailed training record 2022-01-02 14:52:07 +00:00
046d0e461d Fix is_supervisor returning true if user has any levels
Whoops!
2022-01-02 11:15:03 +00:00
b73b8401b6 Add ordering to training level qualification 2022-01-02 11:06:58 +00:00
3fe388af26 Fix importer trying to set pk for qualifications
This doesn't work because of the old DB structure
2022-01-02 11:05:08 +00:00
22dc83d595 Do not display qualified levels also as started 2022-01-02 10:37:48 +00:00
70d4c42676 Much versioning work 2022-01-01 19:53:03 +00:00
0727a23236 Some reversion fiddling 2021-12-31 18:23:38 +00:00
f0b3a6daf3 Filter for active training items
Can't easily filter by supervisor, its not a database field, argh...
2021-12-29 13:07:30 +00:00
3c5f6da363 Fix selectpickers disappearing on modal errors 2021-12-29 12:48:34 +00:00
ee9be86465 Do not display items on trainee detail
That's what the detailed view is for...

And definitely nowt to do with my horrifically optimised SQL
2021-12-28 22:23:17 +00:00
5554edf977 Cleanup 2021-12-28 21:58:56 +00:00
14b73f6f50 Somewhat optimised SQL on level detail 2021-12-28 21:35:21 +00:00
732affa0b2 SQL optimisation of detailed training record 2021-12-28 12:13:08 +00:00
7c830ee7e5 Fix sorting of items
W.T.F past self. Char field for a reference number?!
2021-12-28 11:46:52 +00:00
d47d00d79b Rework item list display 2021-12-28 11:36:46 +00:00
3b5b3b84d4 Significant improvements to level list
Added search
Ordered by qualification count
Added display for technician qualifications
2021-12-27 14:59:30 +00:00
aa8be6a6d0 Rework level list display 2021-12-23 13:29:04 +00:00
640362c203 Importer sets up level heirarchy 2021-12-23 13:28:56 +00:00
71a8823ac2 Markdown support on level desc 2021-12-23 11:31:44 +00:00
c6de3dc9e2 Merge branch 'master' into training 2021-12-22 21:52:04 +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
8696bf5d94 SQL query optimisation 2021-12-22 11:09:54 +00:00
0d9bf89180 Merge branch 'master' into training
# Conflicts:
#	templates/base.html
2021-12-21 19:53:17 +00:00
f4f2fbdc03 Tweak training level display 2021-12-21 19:07:39 +00:00
67aaada9e8 Import confirmation date for training level qualifications 2021-12-21 19:05:25 +00:00
fcae39c93c Add constraint that training items must have unique reference numbers 2021-12-21 15:51:44 +00:00
d1970edfb3 Change display of 'users with this level' 2021-12-21 15:12:11 +00:00
ce5efff268 Initial work on requirements importer 2021-12-21 13:38:00 +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
522837c64e Importer works. Doesn't yet compensate for crap data quality. 2021-10-27 16:42:52 +01:00
e78decdf92 finshed inport old db untested 2021-10-27 14:04:01 +01:00
84a3c9db24 made database importer untested 2021-10-27 13:18:28 +01:00
280a1d9604 begin work on perms 2021-10-22 16:13:08 +01:00
a184bbfa26 Much template wrangling 2021-10-22 15:57:20 +01:00
4416e5bfcb Add RevisionMixin in the right places 2021-10-20 21:15:59 +01:00
21276bcca0 You may not confirm your own training 2021-10-20 21:06:59 +01:00
bc465d67e9 Convert requirement addition to a modal 2021-10-20 21:02:19 +01:00
a644735cd6 Merge branch 'master' into training 2021-10-20 20:22:59 +01:00
8a2b107516 FIX: View event button on event checklist detail 2021-10-20 20:22:35 +01:00
10326f884f Fix the modal fuckery 2021-10-20 20:15:13 +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
0a0c9f15af Common competencies also do not count for being a supervisor 2021-10-09 10:38:40 +01:00
dbb9e3e530 Add 'is van driver' to trainee list, haulage super doesn't count as a supervisor 2021-10-09 10:29:03 +01:00
55558d1a4a Merge branch 'master' into training
# Conflicts:
#	RIGS/templates/risk_assessment_form.html
#	templates/base.html
2021-10-08 18:40:39 +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
081c33ebc8 Various tweaks 2021-09-13 01:07:17 +01:00
75410db752 Refactor is_supervisor 2021-09-13 00:49:02 +01:00
06c6b9a36e Change homepage links to match header ones 2021-09-12 20:26:05 +01:00
13b1cea28b Fancy training level list layout 2021-09-12 18:08:13 +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
cddb76bf7e Order training items by number 2021-09-08 20:44:02 +01:00
f4f1fb66a2 Leaderboard of qualifications obtained 2021-09-08 20:44:02 +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
d80aeca01f Add training level list
Plus various other fettling
2021-09-03 22:34:25 +01:00
6de3cb5d8c Allow filling out of electrical checks for large events 2021-09-02 12:11:05 +01:00
45dfe2db51 Work on trainee reversion 2021-09-02 10:23:53 +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
de5997b9da Reversion working for training level 2021-08-29 22:19:30 +01:00
4a121964dc Start training navbar 2021-08-21 11:42:31 +01:00
df5e4c8e0a Level detail tweaking 2021-08-21 01:44:26 +01:00
3601c14ab7 Oops 2021-08-21 01:36:35 +01:00
adde6496f5 Add loads more sample training items
Hopefully the generator won't make levels with no requirements anymore now
2021-08-21 00:45:35 +01:00
ad734d94b2 Display pre pre requisite levels on level detail 2021-08-21 00:32:04 +01:00
7d3ada822d Common competencies sample data 2021-08-21 00:28:24 +01:00
732af53fda Groundwork stuff for common competencies + other fixes 2021-08-21 00:09:00 +01:00
4fb0529cc0 Modalify the training record addition form 2021-08-20 21:52:33 +01:00
aa23b1cd09 Add sharepoint link to new homepage
Good at scope, me
2021-08-20 21:52:17 +01:00
0c4228da57 Add a view for a trainee's item record 2021-08-20 14:26:32 +01:00
246a52d19e Don't try and create existing level qualifications 2021-08-20 13:48:30 +01:00
8b10aaf700 Display users with level on level detail page 2021-08-20 12:48:54 +01:00
4d0d4f02aa Generate a sample supervisor 2021-08-20 12:38:30 +01:00
af987c1ebb Make TrainingLevelRequirement the correct level of unique
Also updates generateSampleData to match
2021-08-19 18:47:38 +01:00
d406a911bb Initial refactoring of profile detail 2021-08-19 18:27:52 +01:00
63c5a68933 Goddamnit 2021-08-19 18:12:15 +01:00
66f7f830db Forgot that needed migrations generating 2021-08-19 18:08:57 +01:00
9590c2066d Validate that only supervisors may be supervisors 2021-08-19 16:19:46 +01:00
8b48b02ca7 Force trainingitemqualifications to be unique 2021-08-19 16:00:31 +01:00
68e7ec2a0d Merge branch 'master' into training 2021-08-19 15:49:16 +01:00
11636809ca Add link to subhire insurance form on event detail 2021-08-19 15:48:56 +01:00
5779ebdf7e Merge branch 'master' into training
# Conflicts:
#	templates/base.html
2021-08-17 21:35:10 +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
be648c20d5 Level confirmation works 2021-07-29 23:41:35 +01:00
b6ef7c1d89 Force traininglevelqualifications to be unique 2021-07-29 23:15:09 +01:00
85f40b358a Some attempts at optimising SQL queries
New high score!
2021-07-29 22:49:27 +01:00
2698798035 Percentage complete works
Ain't half slow though!
2021-07-16 04:05:55 +01:00
dbaab5cf8c Autofire of traininglevelqualification basically works 2021-07-16 02:58:42 +01:00
0a9f82e480 Fettling with level granting logic
Untested as all of my forms broke I guess
2021-07-07 17:38:44 +01:00
54f2bd36bd UI for editing training level requirements 2021-07-06 22:10:15 +01:00
e836195fef mild polishing 2021-07-06 14:51:43 +01:00
68a424d62b Some sample data and UX work 2021-07-06 12:16:43 +01:00
5e15b8bb59 Basic UX for adding requirements to training levels 2021-07-06 11:37:04 +01:00
d26c1b535e item ui vaguely working 2021-07-06 00:09:46 +01:00
dff5ac2308 Whee broken HEAD 2021-07-05 23:24:13 +01:00
a3729fa930 Session log form work 2021-07-05 18:24:24 +01:00
458a734331 Machine switch 2021-07-01 09:50:13 +01:00
b1646d556c Start work on sample data command 2021-06-30 15:56:28 +01:00
f8624d3b7a Restructure based on actual thought put in by @mattysmith22 2021-06-30 15:17:00 +01:00
f6836fdab6 Merge branch 'master' into training 2021-06-29 17:17:48 +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
b3949f2903 Initial sketching 2021-06-29 17:13:36 +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
192 changed files with 8829 additions and 11941 deletions

View File

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

View File

@@ -10,38 +10,44 @@ jobs:
build:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
runs-on: ubuntu-latest
# strategy:
# matrix:
# browser: ['chrome']
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 coveralls django_coverage_plugin pytest-cov
pip install --upgrade --upgrade-strategy eager -r requirements.txt
python manage.py collectstatic --noinput
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: '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
run: pytest --cov -n 8
run: pipenv run pytest -n auto -vv --cov
- uses: actions/upload-artifact@v2
if: failure()
with:
@@ -49,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

96
Pipfile Normal file
View File

@@ -0,0 +1,96 @@
[[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"
importlib-metadata = "~=3.4.0"
lxml = "~=4.6.3"
Markdown = "~=3.3.3"
msgpack = "~=1.0.2"
pep517 = "~=0.9.1"
Pillow = "~=8.3.2"
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 = "*"
[dev-packages]
selenium = "~=3.141.0"
pycodestyle = "*"
coveralls = "*"
django-coverage-plugin = "*"
pytest-cov = "*"
pytest-django = "*"
pluggy = "*"
pytest-splinter = "*"
pytest = "*"
pytest-xdist = {extras = [ "psutil",], version = "*"}
PyPOM = {extras = [ "splinter",], version = "*"}
[requires]
python_version = "3.9"

1526
Pipfile.lock generated Normal file

File diff suppressed because it is too large Load Diff

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,17 +61,16 @@ 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',
@@ -87,19 +81,19 @@ 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'
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# 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'),
}
}
@@ -177,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'
@@ -190,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
@@ -232,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': {
@@ -269,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,30 +11,22 @@ 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 = env('BROWSER', default="chrome")
if browser == "firefox":
options = webdriver.FirefoxOptions()
options.headless = True
driver = webdriver.Firefox(options=options)
driver.set_window_position(0, 0)
# Firefox is pissy about out of bounds otherwise
driver.set_window_size(3840, 2160)
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)
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
@@ -60,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:
@@ -83,5 +76,30 @@ def screenshot_failure_cls(cls):
return cls
def assert_times_equal(first_time, second_time):
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,6 +71,7 @@ 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()
@@ -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()

View File

@@ -1,11 +1,26 @@
from PyRIGS import urls
from assets.tests.test_unit import create_asset_one
import pytest
from django.urls import URLPattern, URLResolver, reverse
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 assertContains, assertRedirects, assertTemplateUsed, assertInHTML
from pytest_django.asserts import assertRedirects, assertContains, assertNotContains
from pytest_django.asserts import assertTemplateUsed, assertInHTML
pytestmark = pytest.mark.django_db
from PyRIGS import urls
from RIGS.models import Event
from assets.models import Asset
from django.db import connection
import pytest
from django.core.management import call_command
from django.template.defaultfilters import striptags
from django.urls.exceptions import NoReverseMatch
from RIGS.models import Event
from assets.models import Asset
from django.db import connection
from django.test import TestCase
from django.test.utils import override_settings
def find_urls_recursive(patterns):
@@ -14,7 +29,7 @@ def find_urls_recursive(patterns):
if isinstance(url, URLResolver):
urls_to_check += find_urls_recursive(url.url_patterns)
elif isinstance(url, URLPattern):
# Skip some thinks that actually don't need auth (mainly OEmbed JSONs that are essentially just a redirect)
# 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
@@ -22,7 +37,6 @@ def find_urls_recursive(patterns):
def get_request_url(url):
pattern = str(url.pattern)
request_url = ""
try:
kwargz = {}
if ":pk>" in pattern:
@@ -34,32 +48,98 @@ def get_request_url(url):
print("Couldn't test url " + pattern)
def test_unauthenticated(client): # Nothing should be available to the unauthenticated
create_asset_one()
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)
@pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData',
'deleteSampleData'])
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
class TestSampleDataGenerator(TestCase):
@override_settings(DEBUG=True)
def setUp(self):
call_command('generateSampleData')
def test_unauthenticated(self): # Nothing should be available to the unauthenticated
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 = self.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:
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
assertRedirects(response, expected_url)
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)
def test_page_titles(self):
assert self.client.login(username='superuser', password='superuser')
for url in filter((lambda u: "embed" not in u.name), find_urls_recursive(urls.urlpatterns)):
request_url = get_request_url(url)
response = self.client.get(request_url)
if hasattr(response, "context_data") and "page_title" in response.context_data:
expected_title = striptags(response.context_data["page_title"])
assertInHTML('<title>{} | Rig Information Gathering System'.format(expected_title),
response.content.decode())
print("{} | {}".format(request_url, expected_title)) # If test fails, tell me where!
self.client.logout()
def test_page_titles(admin_client):
create_asset_one()
for url in filter((lambda u: "embed" not in u.name), find_urls_recursive(urls.urlpatterns)):
request_url = get_request_url(url)
response = admin_client.get(request_url)
if hasattr(response, "context_data") and "page_title" in response.context_data:
expected_title = response.context_data["page_title"]
# try:
assertInHTML('<title>{} | Rig Information Gathering System'.format(expected_title), response.content.decode())
print("{} | {}".format(request_url, expected_title)) # If test fails, tell me where!
# except:
# print(response.content.decode(), file=open('output.html', 'w'))
def test_basic_access(self):
assert self.client.login(username="basic", password="basic")
url = reverse('asset_list')
response = self.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 = self.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 = self.client.get(request_url, follow=True)
assert response.status_code == 403
request_url = reverse('supplier_create')
response = self.client.get(request_url, follow=True)
assert response.status_code == 403
request_url = reverse('supplier_update', kwargs={'pk': 1})
response = self.client.get(request_url, follow=True)
assert response.status_code == 403
self.client.logout()
def test_keyholder_access(self):
assert self.client.login(username="keyholder", password="keyholder")
url = reverse('asset_list')
response = self.client.get(url)
# Check edit and duplicate buttons shown in list
assertContains(response, 'Edit')
assertContains(response, 'Duplicate')
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = self.client.get(url)
assertContains(response, 'Purchase Details')
assertContains(response, 'View Revision History')
self.client.logout()

View File

@@ -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'),
@@ -27,13 +28,14 @@ urlpatterns = [
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 = [
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,6 +101,9 @@ class SecureAPIRequest(generic.View):
for field in fields:
q = Q(**{field + "__icontains": part})
qs.append(q)
for filter in filters:
q = Q(**{field: True})
qs.append(q)
queries.append(reduce(operator.or_, qs))
# Build the data response list
@@ -230,15 +239,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

@@ -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
@@ -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):
@@ -55,7 +42,13 @@ class InvoiceDetail(generic.DetailView):
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['page_title'] = "Invoice {} ({}) ".format(self.object.display_id, self.object.invoice_date.strftime("%d/%m/%Y"))
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 +60,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 +85,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 +97,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):
@@ -172,30 +159,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 +187,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):
@@ -242,7 +213,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

@@ -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,41 @@
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)
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

@@ -17,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):
@@ -34,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",
@@ -62,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",
@@ -109,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",
@@ -145,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",
@@ -257,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',
@@ -274,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():
@@ -282,70 +193,71 @@ 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=newEvent, supervisor_consulted=bool(random.getrandbits(1)), nonstandard_equipment=bool(random.getrandbits(1)),
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)),
@@ -366,8 +278,15 @@ class Command(BaseCommand):
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=newEvent, 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)),
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

@@ -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

@@ -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

@@ -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,15 @@ 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)
reversion_hide = True
@classmethod
def make_api_key(cls):
@@ -51,7 +54,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,8 +67,6 @@ class Profile(AbstractUser):
def __str__(self):
return self.name
# TODO move to versioning - currently get import errors with that
class RevisionMixin(object):
@property
@@ -102,12 +103,12 @@ class RevisionMixin(object):
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 +134,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 +171,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 +179,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:
@@ -211,12 +211,12 @@ class VatRate(models.Model, RevisionMixin):
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 +229,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 +268,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 +310,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 +325,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,22 +333,23 @@ 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)
if self.pk:
if self.is_rig:
return str("N%05d" % self.pk)
else:
return self.pk
else:
return self.pk
return "????"
# Calculated values
"""
@@ -346,7 +358,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 +372,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,7 +471,7 @@ 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)
@@ -490,7 +505,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,7 +520,7 @@ class EventItem(models.Model, RevisionMixin):
ordering = ['order']
def __str__(self):
return str(self.event.pk) + "." + str(self.order) + ": " + self.event.name + " | " + self.name
return "{}.{}: {} | {}".format(self.event_id, self.order, self.event.name, self.name)
@property
def activity_feed_string(self):
@@ -517,19 +532,36 @@ 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)
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'])
class Invoice(models.Model, RevisionMixin):
event = models.OneToOneField('Event', on_delete=models.CASCADE)
@@ -538,6 +570,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 +596,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 +631,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,10 +666,9 @@ 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,
@@ -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'
@@ -40,7 +41,7 @@ class RigboardIndex(generic.TemplateView):
context = super(RigboardIndex, self).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
@@ -59,29 +60,24 @@ 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(EventDetail, self).get_context_data(**kwargs)
title = "{} | {}".format(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
@@ -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
@@ -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)
@@ -284,6 +275,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()
@@ -311,6 +303,7 @@ class EventAuthorise(generic.UpdateView):
context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name)
if self.event.dry_hire:
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
context['preview'] = self.preview
return context
def get(self, request, *args, **kwargs):
@@ -359,7 +352,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
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 +390,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,8 +405,7 @@ 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')
css = finders.find('css/email.css')
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
assert isinstance(response, HttpResponse)
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
@@ -427,4 +419,5 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
'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: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 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,28 +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 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' %}
@@ -134,14 +128,14 @@
<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><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 %}
@@ -202,7 +196,7 @@
<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=""/></td>
<td style="min-width: 15ch"><input name="role_new" type="text" class="form-control" value="{{ i.role }}" disabled="true"/></td>
@@ -212,7 +206,7 @@
{% 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 %}
@@ -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 %}
@@ -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' }}</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' }}</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,100 +1,89 @@
{% 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>
<h3>
<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" }}
{{ object.start_time|date:"H:i" }}
{% endif %}
{% if object.end_date or object.has_end_time %}
&ndash;
&ndash;
{% endif %}
{% if object.end_date and object.end_date != object.start_date %}
{{ object.end_date|date:"D d/m/Y" }}
{{ object.end_date|date:"D d/m/Y" }}
{% endif %}
{% if object.has_end_time %}
{{ object.end_time|date:"H:i" }}
{{ object.end_time|date:"H:i" }}
{% endif %}
</small>
</h3>
<div class="row">
<div class="col-xs-6">
<p>
<strong>Status:</strong>
{{ object.get_status_display }}
</p>
<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 %}
{% else %}
<strong>Non-Rig</strong>
{% endif %}
</p>
<p>
<strong>MIC:</strong>
{% if object.mic %}
{{object.mic.name}}
{% else %}
None
{% endif %}
</p>
</div>
<div class="col-xs-6">
{% if object.meet_at %}
<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>
{% endif %}
<p>
<strong>Last updated:</strong>
{{ object.last_edited_at }} by "{{ object.last_edited_by.initials }}"
</p>
</div>
</div>
{% if object.description %}
</h3>
{% include 'partials/event_status.html' %}
<div class="row ml-2">
<div class="col-xs-6 pr-2">
<p>
<strong>Description: </strong>
{{ object.description|linebreaksbr }}
{% 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 %}
{% else %}
<strong>Non-Rig</strong>
{% endif %}
</p>
{% endif %}
<p>
<strong>MIC:</strong>
{% if object.mic %}
{{object.mic.name}}
{% else %}
None
{% endif %}
</p>
</div>
<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>
{% endif %}
{% if object.access_at %}
<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>
{{ object.last_edited_at }} by "{{ object.last_edited_by.initials }}"
</p>
</div>
</div>
</div>
{% if object.description %}
<p>
<strong>Description: </strong>
{{ object.description|linebreaksbr }}
</p>
{% endif %}
{% 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>

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</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 '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

@@ -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

@@ -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,5 +1,4 @@
{% 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 %}
@@ -7,7 +6,6 @@
{% 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="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,58 +95,64 @@
</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>

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
@@ -173,7 +173,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 +212,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
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

@@ -51,7 +51,7 @@
"url": "heroku/nodejs"
},
{
"url": "heroku/python"
"url": "https://github.com/nottinghamtec/heroku-buildpack-python"
}
]
}

View File

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

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

8
assets/converters.py Normal file
View File

@@ -0,0 +1,8 @@
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()

View File

@@ -45,7 +45,7 @@ class CableTypeForm(forms.ModelForm):
model = models.CableType
fields = '__all__'
def clean(self):
def clean(self): # TODO Does unique_together work better than this?
form_data = self.cleaned_data
queryset = models.CableType.objects.filter(Q(plug=form_data['plug']) & Q(socket=form_data['socket']) & Q(circuits=form_data['circuits']) & Q(cores=form_data['cores']))
# Being identical to itself shouldn't count...

View File

@@ -1,23 +0,0 @@
from django.core.management.base import BaseCommand, CommandError
from assets import models
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)
def delete_objects(self, model):
for object in model.objects.all():
object.delete()

View File

@@ -1,17 +1,26 @@
import random
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 as rigsmodels
from assets import models
from assets.models import get_available_asset_id
class Command(BaseCommand):
help = 'Creates some sample data for testing'
categories = []
statuses = []
suppliers = []
connectors = []
assets = []
def handle(self, *args, **kwargs):
print("Generating sample assets data")
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
@@ -19,57 +28,43 @@ class Command(BaseCommand):
random.seed('Some object to see the random number generator')
self.create_profile()
self.create_categories()
self.create_statuses()
self.create_suppliers()
self.create_assets()
self.create_connectors()
self.create_cables()
# Make sure that there's at least one profile if this command is run standalone
def create_profile(self):
name = "Fred Johnson"
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()]))
with transaction.atomic():
self.create_categories()
self.create_statuses()
self.create_suppliers()
self.create_assets()
self.create_connectors()
self.create_cables()
print("Done generating sample assets data")
def create_categories(self):
categories = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging']
for cat in categories:
models.AssetCategory.objects.create(name=cat)
choices = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging']
for cat in choices:
self.categories.append(models.AssetCategory.objects.create(name=cat))
def create_statuses(self):
statuses = [('In Service', True, 'success'), ('Lost', False, 'warning'), ('Binned', False, 'danger'), ('Sold', False, 'danger'), ('Broken', False, 'warning')]
for stat in statuses:
models.AssetStatus.objects.create(name=stat[0], should_show=stat[1], display_class=stat[2])
choices = [('In Service', True, 'success'), ('Lost', False, 'warning'), ('Binned', False, 'danger'), ('Sold', False, 'danger'), ('Broken', False, 'warning')]
for stat in choices:
self.statuses.append(models.AssetStatus.objects.create(name=stat[0], should_show=stat[1], display_class=stat[2]))
def create_suppliers(self):
suppliers = ["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", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
choices = ["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", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
"Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
with reversion.create_revision():
for supplier in suppliers:
reversion.set_user(random.choice(rigsmodels.Profile.objects.all()))
models.Supplier.objects.create(name=supplier)
for supplier in choices:
self.suppliers.append(models.Supplier.objects.create(name=supplier))
def create_assets(self):
asset_description = ['Large cable', 'Shiny thing', 'New lights', 'Really expensive microphone', 'Box of fuse flaps', 'Expensive tool we didn\'t agree to buy', 'Cable drums', 'Boring amount of tape', 'Video stuff no one knows how to use', 'More amplifiers', 'Heatshrink']
categories = models.AssetCategory.objects.all()
statuses = models.AssetStatus.objects.all()
suppliers = models.Supplier.objects.all()
with reversion.create_revision():
for i in range(100):
for i in range(100):
with reversion.create_revision():
reversion.set_user(random.choice(rigsmodels.Profile.objects.all()))
asset_id = str(get_available_asset_id())
asset = models.Asset(
asset_id='{}'.format(models.Asset.get_available_asset_id()),
asset_id=asset_id,
description=random.choice(asset_description),
category=random.choice(categories),
status=random.choice(statuses),
category=random.choice(self.categories),
status=random.choice(self.statuses),
date_acquired=timezone.now().date()
)
@@ -77,53 +72,11 @@ class Command(BaseCommand):
asset.parent = models.Asset.objects.order_by('?').first()
if i % 3 == 0:
asset.purchased_from = random.choice(suppliers)
asset.purchased_from = random.choice(self.suppliers)
asset.clean()
asset.save()
def create_cables(self):
asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep']
asset_prefixes = ["C", "C4P", "CBNC", "CDMX", "CDV", "CRCD", "CSOCA", "CXLR"]
csas = [0.75, 1.00, 1.25, 2.5, 4]
lengths = [1, 2, 5, 10, 15, 20, 25, 30, 50, 100]
cores = [3, 5]
circuits = [1, 2, 3, 6]
categories = models.AssetCategory.objects.all()
statuses = models.AssetStatus.objects.all()
suppliers = models.Supplier.objects.all()
connectors = models.Connector.objects.all()
for i in range(len(connectors)):
models.CableType.objects.create(plug=random.choice(connectors), socket=random.choice(connectors), circuits=random.choice(circuits), cores=random.choice(cores))
for i in range(100):
asset = models.Asset(
asset_id='{}'.format(models.Asset.get_available_asset_id()),
description=random.choice(asset_description),
category=random.choice(categories),
status=random.choice(statuses),
date_acquired=timezone.now().date(),
is_cable=True,
cable_type=random.choice(models.CableType.objects.all()),
csa=random.choice(csas),
length=random.choice(lengths),
)
if i % 5 == 0:
prefix = random.choice(asset_prefixes)
asset.asset_id = prefix + str(models.Asset.get_available_asset_id(wanted_prefix=prefix))
if i % 4 == 0:
asset.parent = models.Asset.objects.order_by('?').first()
if i % 3 == 0:
asset.purchased_from = random.choice(suppliers)
asset.clean()
asset.save()
def create_connectors(self):
connectors = [
{"description": "13A UK", "current_rating": 13, "voltage_rating": 230, "num_pins": 3},
@@ -134,3 +87,43 @@ class Command(BaseCommand):
for connector in connectors:
conn = models.Connector.objects.create(** connector)
conn.save()
self.connectors.append(conn)
def create_cables(self):
asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep']
asset_prefixes = ["C", "C4P", "CBNC", "CDMX", "CDV", "CRCD", "CSOCA", "CXLR"]
csas = [0.75, 1.00, 1.25, 2.5, 4]
lengths = [1, 2, 5, 10, 15, 20, 25, 30, 50, 100]
cores = [3, 5]
circuits = [1, 2, 3, 6]
types = []
for i in range(len(self.connectors)):
types.append(models.CableType.objects.create(plug=random.choice(self.connectors), socket=random.choice(self.connectors), circuits=random.choice(circuits), cores=random.choice(cores)))
for i in range(100):
prefix = random.choice(asset_prefixes)
asset_id = str(get_available_asset_id(wanted_prefix=prefix))
asset_id = prefix + asset_id
asset = models.Asset(
asset_id=asset_id,
description=random.choice(asset_description),
category=random.choice(self.categories),
status=random.choice(self.statuses),
date_acquired=timezone.now().date(),
is_cable=True,
cable_type=random.choice(types),
csa=random.choice(csas),
length=random.choice(lengths),
)
if i % 4 == 0:
asset.parent = models.Asset.objects.order_by('?').first()
if i % 3 == 0:
asset.purchased_from = random.choice(self.suppliers)
asset.clean()
asset.save()

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.1.5 on 2021-02-08 16:02
from django.db import migrations
def add_default(apps, schema_editor):
CableType = apps.get_model('assets', 'CableType')
Connector = apps.get_model('assets', 'Connector')
for cable_type in CableType.objects.all():
if cable_type.plug is None:
cable_type.plug = Connector.objects.first()
if cable_type.socket is None:
cable_type.socket = Connector.objects.first()
cable_type.save()
class Migration(migrations.Migration):
dependencies = [
('assets', '0018_auto_20200415_1940'),
]
operations = [
migrations.RunPython(add_default, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.7 on 2021-03-02 12:01
from django.db import migrations
def postgres_migration_prep(apps, schema_editor):
model = apps.get_model("assets", "Supplier")
fields = ["address", "email", "notes", "phone"]
for field in fields:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
class Migration(migrations.Migration):
dependencies = [
('assets', '0019_fix_cabletype'),
]
operations = [
migrations.RunPython(postgres_migration_prep, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,50 @@
# Generated by Django 3.1.7 on 2021-03-02 12:04
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0020_auto_20210302_1201'),
]
operations = [
migrations.AlterField(
model_name='assetstatus',
name='display_class',
field=models.CharField(blank=True, default='', help_text='HTML class to be appended to alter display of assets with this status, such as in the list.', max_length=80),
preserve_default=False,
),
migrations.AlterField(
model_name='cabletype',
name='plug',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plug', to='assets.connector'),
),
migrations.AlterField(
model_name='cabletype',
name='socket',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='socket', to='assets.connector'),
),
migrations.AlterField(
model_name='supplier',
name='address',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='supplier',
name='email',
field=models.EmailField(blank=True, default='', max_length=254),
),
migrations.AlterField(
model_name='supplier',
name='notes',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='supplier',
name='phone',
field=models.CharField(blank=True, default='', max_length=15),
),
]

View File

@@ -2,8 +2,6 @@ import re
from django.core.exceptions import ValidationError
from django.db import models, connection
from django.db.models.signals import pre_save
from django.dispatch.dispatcher import receiver
from django.urls import reverse
from reversion import revisions as reversion
from reversion.models import Version
@@ -12,46 +10,46 @@ from RIGS.models import RevisionMixin, Profile
class AssetCategory(models.Model):
name = models.CharField(max_length=80)
class Meta:
verbose_name = 'Asset Category'
verbose_name_plural = 'Asset Categories'
ordering = ['name']
name = models.CharField(max_length=80)
def __str__(self):
return self.name
class AssetStatus(models.Model):
name = models.CharField(max_length=80)
should_show = models.BooleanField(
default=True, help_text="Should this be shown by default in the asset list.")
display_class = models.CharField(max_length=80, blank=True, help_text="HTML class to be appended to alter display of assets with this status, such as in the list.")
class Meta:
verbose_name = 'Asset Status'
verbose_name_plural = 'Asset Statuses'
ordering = ['name']
name = models.CharField(max_length=80)
should_show = models.BooleanField(
default=True, help_text="Should this be shown by default in the asset list.")
display_class = models.CharField(max_length=80, blank=True, null=True, help_text="HTML class to be appended to alter display of assets with this status, such as in the list.")
def __str__(self):
return self.name
@reversion.register
class Supplier(models.Model, RevisionMixin):
name = models.CharField(max_length=80)
phone = models.CharField(max_length=15, blank=True, default="")
email = models.EmailField(blank=True, default="")
address = models.TextField(blank=True, default="")
notes = models.TextField(blank=True, default="")
class Meta:
ordering = ['name']
name = models.CharField(max_length=80)
phone = models.CharField(max_length=15, blank=True, null=True)
email = models.EmailField(blank=True, null=True)
address = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, null=True)
def get_absolute_url(self):
return reverse('supplier_list')
return reverse('supplier_detail', kwargs={'pk': self.pk})
def __str__(self):
return self.name
@@ -67,17 +65,16 @@ class Connector(models.Model):
return self.description
# Things are nullable that shouldn't be because I didn't properly fix the data structure when moving this to its own model...
class CableType(models.Model):
class Meta:
ordering = ['plug', 'socket', '-circuits']
circuits = models.IntegerField(default=1)
cores = models.IntegerField(default=3)
plug = models.ForeignKey(Connector, on_delete=models.CASCADE,
related_name='plug', null=True)
related_name='plug')
socket = models.ForeignKey(Connector, on_delete=models.CASCADE,
related_name='socket', null=True)
related_name='socket')
class Meta:
ordering = ['plug', 'socket', '-circuits']
def __str__(self):
if self.plug and self.socket:
@@ -85,15 +82,31 @@ class CableType(models.Model):
else:
return "Unknown"
def get_absolute_url(self):
return reverse('cable_type_detail', kwargs={'pk': self.pk})
def get_available_asset_id(wanted_prefix=""):
sql = """
SELECT a.asset_id_number+1
FROM assets_asset a
LEFT OUTER JOIN assets_asset b ON
(a.asset_id_number + 1 = b.asset_id_number AND
a.asset_id_prefix = b.asset_id_prefix)
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s;
"""
with connection.cursor() as cursor:
cursor.execute(sql, [9000, wanted_prefix])
row = cursor.fetchone()
if row is None or row[0] is None:
return 9000
else:
return row[0]
cursor.close()
@reversion.register
class Asset(models.Model, RevisionMixin):
class Meta:
ordering = ['asset_id_prefix', 'asset_id_number']
permissions = [
('asset_finance', 'Can see financial data for assets')
]
parent = models.ForeignKey(to='self', related_name='asset_parent',
blank=True, null=True, on_delete=models.SET_NULL)
asset_id = models.CharField(max_length=15, unique=True)
@@ -127,32 +140,18 @@ class Asset(models.Model, RevisionMixin):
reversion_perm = 'assets.asset_finance'
def get_available_asset_id(wanted_prefix=""):
sql = """
SELECT a.asset_id_number+1
FROM assets_asset a
LEFT OUTER JOIN assets_asset b ON
(a.asset_id_number + 1 = b.asset_id_number AND
a.asset_id_prefix = b.asset_id_prefix)
WHERE b.asset_id IS NULL AND a.asset_id_number >= %s AND a.asset_id_prefix = %s;
"""
with connection.cursor() as cursor:
cursor.execute(sql, [9000, wanted_prefix])
row = cursor.fetchone()
if row is None or row[0] is None:
return 9000
else:
return row[0]
class Meta:
ordering = ['asset_id_prefix', 'asset_id_number']
permissions = [
('asset_finance', 'Can see financial data for assets')
]
def __str__(self):
return "{} | {}".format(self.asset_id, self.description)
def get_absolute_url(self):
return reverse('asset_detail', kwargs={'pk': self.asset_id})
def __str__(self):
out = str(self.asset_id) + ' - ' + self.description
if self.is_cable and self.cable_type is not None:
out += '{} - {}m - {}'.format(self.cable_type.plug, self.length, self.cable_type.socket)
return out
def clean(self):
errdict = {}
if self.date_sold and self.date_acquired > self.date_sold:
@@ -188,14 +187,3 @@ class Asset(models.Model, RevisionMixin):
@property
def display_id(self):
return str(self.asset_id)
@receiver(pre_save, sender=Asset)
def pre_save_asset(sender, instance, **kwargs):
"""Automatically fills in hidden members on database access"""
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
if asset_search is None:
instance.asset_id += "1"
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
instance.asset_id_prefix = asset_search.group(1)
instance.asset_id_number = int(asset_search.group(2))

15
assets/signals.py Normal file
View File

@@ -0,0 +1,15 @@
import re
from django.db.models.signals import pre_save
from django.dispatch.dispatcher import receiver
from .models import Asset
@receiver(pre_save, sender=Asset)
def pre_save_asset(sender, instance, **kwargs):
"""Automatically fills in hidden members on database access"""
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
if asset_search is None:
instance.asset_id += "1"
asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", instance.asset_id)
instance.asset_id_prefix = asset_search.group(1)
instance.asset_id_number = int(asset_search.group(2))

View File

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

View File

@@ -4,9 +4,6 @@
{% load widget_tweaks %}
{% block js %}
<script src="{% static 'js/jquery-ui.js' %}"></script>
<script src="{% static "js/interaction.js" %}"></script>
<script src="{% static "js/modal.js" %}"></script>
<script>
$('document').ready(function(){
$('#asset-search-form').submit(function () {
@@ -15,9 +12,7 @@
});
$('#searchButton').click(function (e) {
e.preventDefault();
var url = "{% url 'asset_audit' None %}";
var id = $("#{{form.q.id_for_label}}").val();
url = url.replace('None', id);
var url = "{% url 'asset_audit' None %}".replace('None', $("#{{form.q.id_for_label}}").val();
$.ajax({
url: url,
success: function(){
@@ -49,7 +44,7 @@
<span>Asset with that ID does not exist!</span>
</div>
<form id="asset-search-form" class="mb-3" method="POST">
<form id="asset-search-form" class="mb-3" method="GET">
<div class="form-group form-row">
<h3>Audit Asset:</h3>
<div class="input-group input-group-lg">

View File

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

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