Compare commits

..

144 Commits

Author SHA1 Message Date
github-actions[bot]
dfaeb41080 Merge dependabot/npm_and_yarn/engine.io-and-socket.io-6.4.2 into combine-prs-branch1 2023-05-12 07:03:36 +00:00
github-actions[bot]
16671c6e1e Merge dependabot/pip/django-3.2.19 into combine-prs-branch1 2023-05-12 07:03:36 +00:00
dependabot[bot]
0cd75cb313 Build(deps): Bump django from 3.2.18 to 3.2.19
Bumps [django](https://github.com/django/django) from 3.2.18 to 3.2.19.
- [Commits](https://github.com/django/django/compare/3.2.18...3.2.19)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-09 22:20:19 +00:00
dependabot[bot]
b2c16cefad Build(deps): Bump engine.io and socket.io
Bumps [engine.io](https://github.com/socketio/engine.io) and [socket.io](https://github.com/socketio/socket.io). These dependencies needed to be updated together.

Updates `engine.io` from 6.2.1 to 6.4.2
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/6.2.1...6.4.2)

Updates `socket.io` from 4.5.3 to 4.6.1
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/4.5.3...4.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-04 00:56:05 +00:00
b1a2859f1b Do not export inactive training items 2023-03-03 11:15:06 +00:00
dc71c2de62 "Passed out" -> "Competency assessed" 2023-03-03 11:12:49 +00:00
8863d86ed0 Add description field to TrainingItems (#523)
* Add 'description' field to TrainingItems

Renamed existing field to name, removed the dummy property.

* Initial version of training item export view

* Fix line length issue and better spacing on exported PDF

* Added export button to item list

* pep8

* Implement code doctor tweaks

* Attempt to fix odd deployment issue

* Pad headers slightly

* Fix page numbering
2023-02-22 21:07:43 +00:00
dependabot[bot]
6550ed2318 Build(deps): Bump django from 3.2.17 to 3.2.18 (#522)
Bumps [django](https://github.com/django/django) from 3.2.17 to 3.2.18.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.17...3.2.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-16 11:10:11 +00:00
dependabot[bot]
c9759a6339 Build(deps): Bump qs and browser-sync (#521)
Bumps [qs](https://github.com/ljharb/qs) to 6.11.0 and updates ancestor dependency [browser-sync](https://github.com/BrowserSync/browser-sync). These dependencies need to be updated together.


Updates `qs` from 6.2.3 to 6.11.0
- [Release notes](https://github.com/ljharb/qs/releases)
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.2.3...v6.11.0)

Updates `browser-sync` from 2.27.10 to 2.27.11
- [Release notes](https://github.com/BrowserSync/browser-sync/releases)
- [Changelog](https://github.com/BrowserSync/browser-sync/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BrowserSync/browser-sync/compare/v2.27.10...v2.27.11)

---
updated-dependencies:
- dependency-name: qs
  dependency-type: indirect
- dependency-name: browser-sync
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-09 14:56:22 +00:00
dependabot[bot]
b637c4e452 Build(deps): Bump http-cache-semantics from 4.1.0 to 4.1.1 (#520)
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-04 10:20:18 +00:00
dependabot[bot]
8986b94b07 Build(deps): Bump django from 3.2.16 to 3.2.17 (#519)
Bumps [django](https://github.com/django/django) from 3.2.16 to 3.2.17.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.16...3.2.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-04 10:20:04 +00:00
86c033ba97 Update django.yml 2022-12-11 14:18:05 +00:00
52fd662340 Update django.yml 2022-12-11 14:13:47 +00:00
9818ed995f Update django.yml 2022-12-11 14:13:33 +00:00
5178614d71 Update django.yml
Rollback runner for now
2022-12-11 01:01:01 +00:00
a7bf990666 FIX: Do not specify minor version of python in CI 2022-12-11 00:51:19 +00:00
a4a28a6130 Add nickname field to assets
Seems necessary given all the lights and some of the amps and distros have names :-)
2022-12-11 00:29:30 +00:00
e3d8cf8978 Dependency update 2022-12-11 00:26:24 +00:00
626779ef25 Turn off RA reminders for non-rigs 2022-12-05 21:30:24 +00:00
fa1dc31639 FIX: Only require login for viewing RAs/ECs 2022-11-26 12:58:15 +00:00
d69543e309 FIX: Only require login to view training profiles
Previously required a specific permission only granted to keyholders
2022-11-26 12:54:42 +00:00
dependabot[bot]
a24e6d4495 Build(deps): Bump pillow from 9.0.1 to 9.3.0 (#516)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.0.1 to 9.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.0.1...9.3.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-22 17:33:42 +00:00
dependabot[bot]
fa5792914a Build(deps): Bump engine.io from 6.2.0 to 6.2.1 (#515)
Bumps [engine.io](https://github.com/socketio/engine.io) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1)

---
updated-dependencies:
- dependency-name: engine.io
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-22 17:33:14 +00:00
0117091f3e FEAT #513: Implement email reminders to complete risk assessments (#514)
* FEAT #513: Implement email reminders to complete risk assessments

* Add missing f-string tag
2022-11-19 15:34:15 +00:00
37101d3340 Update lockfile 2022-11-18 14:29:44 +00:00
de4bed92a4 Build(deps): Generally update JS & Python deps (#512) 2022-11-17 12:06:21 +00:00
dependabot[bot]
3767923175 Build(deps): Bump yargs-parser and yargs (#507)
Bumps [yargs-parser](https://github.com/yargs/yargs-parser) and [yargs](https://github.com/yargs/yargs). These dependencies needed to be updated together.

Updates `yargs-parser` from 5.0.0-security.0 to 20.2.9
- [Release notes](https://github.com/yargs/yargs-parser/releases)
- [Changelog](https://github.com/yargs/yargs-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs-parser/commits/yargs-parser-v20.2.9)

Updates `yargs` from 7.1.1 to 15.4.1
- [Release notes](https://github.com/yargs/yargs/releases)
- [Changelog](https://github.com/yargs/yargs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs/commits/v15.4.1)

---
updated-dependencies:
- dependency-name: yargs-parser
  dependency-type: indirect
- dependency-name: yargs
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-15 15:52:49 +00:00
dependabot[bot]
1d77cf95d3 Build(deps): Bump scss-tokenizer and node-sass (#506)
Bumps [scss-tokenizer](https://github.com/sasstools/scss-tokenizer) to 0.4.3 and updates ancestor dependency [node-sass](https://github.com/sass/node-sass). These dependencies need to be updated together.


Updates `scss-tokenizer` from 0.3.0 to 0.4.3
- [Release notes](https://github.com/sasstools/scss-tokenizer/releases)
- [Commits](https://github.com/sasstools/scss-tokenizer/compare/v0.3.0...v0.4.3)

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

---
updated-dependencies:
- dependency-name: scss-tokenizer
  dependency-type: indirect
- dependency-name: node-sass
  dependency-type: direct:production
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-15 15:44:25 +00:00
dependabot[bot]
1f21d0b265 Build(deps): Bump engine.io and browser-sync (#508)
Bumps [engine.io](https://github.com/socketio/engine.io) to 6.2.0 and updates ancestor dependency [browser-sync](https://github.com/BrowserSync/browser-sync). These dependencies need to be updated together.


Updates `engine.io` from 3.5.0 to 6.2.0
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/3.5.0...6.2.0)

Updates `browser-sync` from 2.27.7 to 2.27.10
- [Release notes](https://github.com/BrowserSync/browser-sync/releases)
- [Changelog](https://github.com/BrowserSync/browser-sync/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BrowserSync/browser-sync/compare/v2.27.7...v2.27.10)

---
updated-dependencies:
- dependency-name: engine.io
  dependency-type: indirect
- dependency-name: browser-sync
  dependency-type: direct:development
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-15 15:33:50 +00:00
7846a6d31e Rename combine-prs to combine-prs.yml 2022-11-14 18:20:57 +00:00
d28b73a0b8 Create combine-prs
Copied from here: https://github.com/hrvey/combine-prs-workflow
2022-11-14 16:30:55 +00:00
dependabot[bot]
5c2e8b391c Build(deps): Bump moment from 2.29.2 to 2.29.4 (#505)
Bumps [moment](https://github.com/moment/moment) from 2.29.2 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.2...2.29.4)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-14 15:08:05 +00:00
dependabot[bot]
548bc1df81 Build(deps): Bump socket.io-parser from 3.3.2 to 3.3.3 (#503)
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/3.3.2...3.3.3)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-13 16:17:08 +00:00
dependabot[bot]
c1d2bce8fb Build(deps): Bump minimatch from 3.0.4 to 3.0.8 (#504)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.0.8.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.0.8)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-13 16:16:51 +00:00
c71beab278 Change: Only supervisors have edit access to the training database 2022-10-24 16:23:46 +01:00
259932a548 FIX #502: Possibility to choose 'no selection' in session log form
Ref #501...may help/fix this...uncertain yet. Need to finish writing the relevant test!
2022-10-23 10:56:54 +01:00
7526485837 FEAT: Add periodic cleanup command
Currently performs two functions:
1. Inactivates users that have not logged in for at least one year. Closes #478 (Need to circle back round to full deletion SoonTM)
2. Ensures the supervisor database flag is set correctly for each user

This is run automatically by the Heroku Scheduler addon at midnight daily.
2022-10-21 00:05:20 +01:00
39ed5aefb4 Set printed PDF title == filename
Should fix #497
2022-10-18 12:48:25 +01:00
e7e760de2e Fix copy to clipboard buttons on authorisation request form 2022-10-15 17:58:07 +01:00
9091197639 Minor audit fix 2022-06-02 15:36:53 +01:00
4f4baa62c1 Fix tests 2022-05-30 22:33:55 +01:00
b9f8621e1a Migrations fix 2022-05-26 16:23:51 +01:00
4b1dc37a7f Rename 'salvage value' to 'replacement cost'
This more accurately reflects historical use of the field, and what the insurers actually want. Ref #439
2022-05-26 10:29:53 +01:00
dependabot[bot]
9273ca35cf Build(deps): Bump django from 3.2.12 to 3.2.13 (#493)
Bumps [django](https://github.com/django/django) from 3.2.12 to 3.2.13.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.12...3.2.13)

---
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>
2022-05-16 18:35:20 +01:00
dependabot[bot]
4a4b7fa30d Build(deps): Bump pypdf2 from 1.26.0 to 1.27.5 (#492)
Bumps [pypdf2](https://github.com/py-pdf/PyPDF2) from 1.26.0 to 1.27.5.
- [Release notes](https://github.com/py-pdf/PyPDF2/releases)
- [Changelog](https://github.com/py-pdf/PyPDF2/blob/main/CHANGELOG)
- [Commits](https://github.com/py-pdf/PyPDF2/compare/1.26.0...1.27.5)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Arona Jones <aj@aronajones.com>
2022-05-16 18:14:04 +01:00
a44a532c7d Create manual deployment Github Action 2022-05-16 16:54:47 +01:00
3a2e5c943b Fix typo II 2022-05-16 16:19:26 +01:00
426a9088cc Fix typo 2022-05-03 16:33:30 +01:00
dependabot[bot]
1369a2f978 Build(deps): Bump moment from 2.29.1 to 2.29.2 (#491)
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-13 09:43:32 +01:00
38eafbced3 Only require item prerequisites on competency assessed 2022-03-07 16:43:00 +00:00
900002bf71 Modalise training item qualification edit
Also fixed some stuff
2022-03-06 18:30:03 +00:00
2869c9fcc3 FIX(T): 500 error editing training item qualifications
For the second time with this piece of functionality...how did that ever work?
2022-03-06 17:30:11 +00:00
00eb4e0e27 FEAT(T): Add ability to view users passed out in a certain training item 2022-03-03 19:31:04 +00:00
23e17b0e34 FIX(T): Training level requirement changes spamming recent changes 2022-03-03 19:14:20 +00:00
bf268a4566 Minor branding tweak 2022-03-03 19:10:05 +00:00
dedb8d81fe FEAT(T): Add ability to log items at various depths during a session
Also fixes inability to search by reference number
2022-03-01 18:36:35 +00:00
7d785f4f1b Hotfix: Unable to add training item in requirements form 2022-02-27 22:16:39 +00:00
5eb113156b FEAT(T): First version of the 'session log' form 2022-02-27 21:20:34 +00:00
ab03ad081a Markdownify event description in tables
TODO: Restrict max size of headers so they can't go larger than other elements in the table.
2022-02-24 20:20:57 +00:00
cd5889f60e Wrong place... 2022-02-24 14:52:52 +00:00
f18bf3b077 Add total asset number to list page 2022-02-24 14:52:19 +00:00
3d36d986a4 Add basic validation of item prerequisites
Currently throws the worlds most unhelpful error message...
2022-02-23 16:01:00 +00:00
41f5a23ef0 Redesign label sheet to have variable (csa based) label size 2022-02-22 23:24:27 +00:00
09f48f740d Redesign cable label to increase asset number readability 2022-02-22 20:54:38 +00:00
805d77af20 Move access/meet times to above start/end in event list 2022-02-22 15:00:39 +00:00
fabab87e23 Add moved templates to git
Not sure why that wasn't automatic.
2022-02-16 15:21:57 +00:00
a95779e04e FEAT: Add ability to generate RA printouts 2022-02-16 15:01:38 +00:00
24e6ba540d Embolden item headers on paperwork
Closes #481
2022-02-16 13:14:53 +00:00
14d3522b81 Do not require supervisor consultation for big power
Only requires power tech
2022-02-16 13:14:25 +00:00
e4cfaba57d Markdown enable generic note fields 2022-02-15 12:13:50 +00:00
d9664422c5 Fix asset sample data command 2022-02-15 02:40:20 +00:00
27bb3f1d8e Port/fix asset tests 2022-02-15 02:28:19 +00:00
151ac8b3bd Add fallback initial asset id 2022-02-15 01:58:40 +00:00
c2dcd86d5d Calendar changes
Closes #437
(Probably) Closes #469 but also puts us out of spec...
2022-02-15 01:00:47 +00:00
6c14b30c13 Disable auth request button for authorised events
Overrides and closes #377
2022-02-15 00:29:29 +00:00
5215af349a Tweak asset ID autogeneration
Closes #443, admittedly with a different approach.
2022-02-15 00:18:19 +00:00
a5e888fef5 Make salvage value a required field on asset
Closes #439
2022-02-15 00:18:19 +00:00
dependabot[bot]
2ae4e4142c Build(deps): Bump follow-redirects from 1.14.7 to 1.14.8 (#489)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-15 00:18:13 +00:00
8799f822bb HOTFIX: Supplier admin broken 2022-02-14 16:27:02 +00:00
2dd3d306b4 Remove a bunch of rounded corners
Closer to the Forum UI, and also just generally less 2015
2022-02-14 15:45:11 +00:00
042004e1ae Some cable list work 2022-02-14 15:28:43 +00:00
733ea69cc5 Add tooling for mass editing 2022-02-14 15:14:00 +00:00
bbea47e8ec Change purchased_from options so supplier deletion does not delete assets 2022-02-14 15:13:55 +00:00
c4aafbd7e5 First version of the cable list 2022-02-14 11:50:16 +00:00
ccdc13df93 FIX #486: Make training item edit work 2022-02-14 11:19:44 +00:00
aa19ceaf18 FIX #488: Unable to filter detailed training record by item ID 2022-02-14 11:14:32 +00:00
05d280172d Order detailed training record by reference number 2022-02-13 11:52:41 +00:00
2f51b7b1d3 pep8 2022-02-08 17:29:38 +00:00
8d1edb54ea HOTFIX Trainee Search broken 2022-02-08 17:22:09 +00:00
54c90a7be4 Refactor search logic to a create an 'omnisearch' (#484) 2022-02-08 15:01:01 +00:00
3e1e0079d8 Fix traininglevelqualification display
Closes #470 for the time being
2022-02-03 18:47:16 +00:00
b6952aeb52 Various fixings 2022-01-30 12:31:54 +00:00
d33a4231fb Fix selectpickers showing up slightly wonky 2022-01-30 12:14:26 +00:00
8dea6aeab0 Allow to search training items by (full) reference number) 2022-01-30 12:03:40 +00:00
34c03e379d Switch to using competency assessed for leaderboard 2022-01-30 11:23:54 +00:00
988fb78b45 Rewrite admin merge functionality. Should close #473 2022-01-29 14:58:15 +00:00
eda314c092 Test work, some CSS fixes, mild reversion pokage 2022-01-27 00:52:12 +00:00
8ef520619a Potential test fix 2022-01-26 20:32:22 +00:00
95931f86b4 Add link to H&S reporter in quick links and fix some layout issues 2022-01-26 20:00:14 +00:00
cc2cb5c4d1 Fix tests 2022-01-26 19:49:18 +00:00
3ae507b469 Filter trainees for active approved users
Closes #477
2022-01-25 13:08:33 +00:00
33754eed60 System for allowing certain TrainingCategories to be trained by certain levels, regardless of supervisor status
I.e. the haulage department, ref #482. As generic as I can make it I think.
2022-01-25 13:04:26 +00:00
15ab626593 HOTFIX: Version string broken on paperwork generation
Why the hell didn't the tests catch that?
2022-01-25 12:30:37 +00:00
7bc47b446c Add functionality to filter trainee list by is_supervisor
Closes #479
2022-01-25 10:53:25 +00:00
83b287a418 Refactor merge logic to allow merging of users. Closes #473. 2022-01-25 10:29:46 +00:00
3b9848d457 Set user on level confirmation 2022-01-24 22:36:53 +00:00
308d0c697e Fix (probably) reversion for trainingitemqualification 2022-01-24 22:32:12 +00:00
f243a589fa Remove duplicate RevisionMixin 2022-01-24 21:07:32 +00:00
79c90ac92c PATCH: Bullets in paperwork hard crashing 2022-01-24 15:51:57 +00:00
8244287a64 FIX: inability to scroll modals on dark theme
What. The. Hell.
2022-01-24 15:51:57 +00:00
da4d62729b Properly folderise rigboard views 2022-01-24 13:49:11 +00:00
f8a48798de Hide index page images on mobile 2022-01-20 18:35:30 +00:00
fc817fa9b5 Swap to EasyMDE
Various other fixes too
2022-01-20 11:31:23 +00:00
b04a168f01 Remove flatpickr polyfill
Firefox now supports it natively...finally! Closes #475
2022-01-20 11:04:15 +00:00
cc6992976e Fix #476 broken darktheme embeds
One tiny step toward #419 as well ;p
2022-01-20 10:50:41 +00:00
a556b17d2d Rip out analytics
Closes #423
2022-01-20 10:38:02 +00:00
f9e38338dc Fix error when trying to load detailed item list 2022-01-20 10:35:52 +00:00
ce83ae6dd1 Refactor asset search to use SecureAPIRequest
Closes #474 and #465
2022-01-19 19:19:50 +00:00
9e1d54dc02 FIX #472: Filter common competencies out of get_levels_of_depth 2022-01-19 18:38:37 +00:00
375b0af2fd Delete Dockerfile 2022-01-19 13:20:03 +00:00
imgbot[bot]
0354662864 [ImgBot] Optimize images (#471)
*Total -- 8,936.02kb -> 7,990.11kb (10.59%)

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

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>

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

* Improved MD support.

Add some styling for images in MD

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

* Add processing for <ul> in RML

* Add OL processing to RML

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

* Remove rml formatting in event_detail

* Improve handling of code blocks in RML

* Add MD to rigboard

Reduce MD title sizes as they were offensively large

* Add parsing of markdown when editing event items

* Improved list handling in RML

* Add tests for markdown support.

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

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

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

* Add failing test for markdown processing none

* Fix for failing test in e0d56e

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

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

Pass tests in ef3de607c3

* Enable GH flavour linebreaks in JS rendered markdown

* Made RML bullets pretty :)

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

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

* FIX: Re-enable markdown on paperwork

Strikethrough is broken in all sorts of places for whatever reason

* FEAT: Markdown support on asset comments

* FIX: Prevent js injection through markdown fields

* Initial fixes

* Basic dark theme for simplemde

* Swap to locally delivered SimpleMDE

* Region for selenium testing of SimpleMDE

Bleh, Javascript all around

* Tests passing!

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

* Pep8 fixes

* Fallback for null HCapatcha sitekey

I.e. when we're on a branch

* Fix item description print being broken

* Actually fix sitekey problem

* Fixes for using markdown in asset comments

* Properly initialise markdown on asset comments

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

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

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

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

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-19 12:11:43 +00:00
0e64021f01 Pin >8 ver of postcss 2021-12-19 11:49:58 +00:00
2eb87a51f8 Update for gulp-sass change 2021-12-19 11:39:26 +00:00
30fac1d1b9 NPM Dependency update 2021-12-19 11:26:55 +00:00
c4fec483ae FIX/CHNG: Clients may see line prices on event auth form
Not sure why they couldn't previous, its not like we only quote totals...
2021-12-13 12:13:47 +00:00
3028fb92d9 Fix up the copy button stuff 2021-12-13 12:07:42 +00:00
215697ba64 Change navbar logo link when authenticated to RIGS 2021-11-23 09:22:06 +00:00
d966bddfd7 QOL: Add copy to clipboard buttons to emails on event auth request modal 2021-11-07 13:40:04 +00:00
0d5e48b89c Patching tests always feels a little like cheating 2021-11-07 12:57:31 +00:00
bd2c94d3e3 FIX: Wrong display on event checklist detail 2021-11-07 12:48:07 +00:00
192 changed files with 11962 additions and 7862 deletions

151
.github/workflows/combine-prs.yml vendored Normal file
View File

@@ -0,0 +1,151 @@
name: 'Combine PRs'
# Controls when the action will run - in this case triggered manually
on:
workflow_dispatch:
inputs:
branchPrefix:
description: 'Branch prefix to find combinable PRs based on'
required: true
default: 'dependabot'
mustBeGreen:
description: 'Only combine PRs that are green (status is success)'
required: true
default: true
combineBranchName:
description: 'Name of the branch to combine PRs into'
required: true
default: 'combine-prs-branch'
ignoreLabel:
description: 'Exclude PRs with this label'
required: true
default: 'nocombine'
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "combine-prs"
combine-prs:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/github-script@v6
id: create-combined-pr
name: Create Combined PR
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', {
owner: context.repo.owner,
repo: context.repo.repo
});
let branchesAndPRStrings = [];
let baseBranch = null;
let baseBranchSHA = null;
for (const pull of pulls) {
const branch = pull['head']['ref'];
console.log('Pull for branch: ' + branch);
if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) {
console.log('Branch matched prefix: ' + branch);
let statusOK = true;
if(${{ github.event.inputs.mustBeGreen }}) {
console.log('Checking green status: ' + branch);
const stateQuery = `query($owner: String!, $repo: String!, $pull_number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number:$pull_number) {
commits(last: 1) {
nodes {
commit {
statusCheckRollup {
state
}
}
}
}
}
}
}`
const vars = {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pull['number']
};
const result = await github.graphql(stateQuery, vars);
const [{ commit }] = result.repository.pullRequest.commits.nodes;
const state = commit.statusCheckRollup.state
console.log('Validating status: ' + state);
if(state != 'SUCCESS') {
console.log('Discarding ' + branch + ' with status ' + state);
statusOK = false;
}
}
console.log('Checking labels: ' + branch);
const labels = pull['labels'];
for(const label of labels) {
const labelName = label['name'];
console.log('Checking label: ' + labelName);
if(labelName == '${{ github.event.inputs.ignoreLabel }}') {
console.log('Discarding ' + branch + ' with label ' + labelName);
statusOK = false;
}
}
if (statusOK) {
console.log('Adding branch to array: ' + branch);
const prString = '#' + pull['number'] + ' ' + pull['title'];
branchesAndPRStrings.push({ branch, prString });
baseBranch = pull['base']['ref'];
baseBranchSHA = pull['base']['sha'];
}
}
}
if (branchesAndPRStrings.length == 0) {
core.setFailed('No PRs/branches matched criteria');
return;
}
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}',
sha: baseBranchSHA
});
} catch (error) {
console.log(error);
core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?');
return;
}
let combinedPRs = [];
let mergeFailedPRs = [];
for(const { branch, prString } of branchesAndPRStrings) {
try {
await github.rest.repos.merge({
owner: context.repo.owner,
repo: context.repo.repo,
base: '${{ github.event.inputs.combineBranchName }}',
head: branch,
});
console.log('Merged branch ' + branch);
combinedPRs.push(prString);
} catch (error) {
console.log('Failed to merge branch ' + branch);
mergeFailedPRs.push(prString);
}
}
console.log('Creating combined PR');
const combinedPRsString = combinedPRs.join('\n');
let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString;
if(mergeFailedPRs.length > 0) {
const mergeFailedPRsString = mergeFailedPRs.join('\n');
body += '\n\n⚠ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString
}
await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Combined PR',
head: '${{ github.event.inputs.combineBranchName }}',
base: baseBranch,
body: body
});

14
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: Manual Deploy
on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: akhileshns/heroku-deploy@v3.12.12 # This is the action
with:
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
heroku_app_name: "pyrigs" #Must be unique in Heroku
heroku_email: "aj@aronajones.com"

View File

@@ -13,26 +13,20 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: 3.9.1
- uses: actions/cache@v2
id: pcache
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
restore-keys: |
${{ runner.os }}-pipenv-
python-version: 3.9
cache: 'pipenv'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip pipenv
python3 -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
uses: actions/cache@v3
with:
path: 'pipeline/built_assets'
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
@@ -43,9 +37,9 @@ jobs:
- name: Basic Checks
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
pipenv run python3 manage.py check
pipenv run python3 manage.py makemigrations --check --dry-run
pipenv run python3 manage.py collectstatic --noinput
- name: Run Tests
run: pipenv run pytest -n auto -vv --cov
- uses: actions/upload-artifact@v2

1
.gitignore vendored
View File

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

View File

@@ -1,12 +0,0 @@
FROM python:3.6
WORKDIR /app
ADD . /app
RUN pip install -r requirements.txt && \
python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

28
Pipfile
View File

@@ -11,7 +11,6 @@ asgiref = "~=3.3.1"
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"
@@ -19,38 +18,35 @@ cssselect = "~=1.1.0"
cssutils = "~=1.0.2"
dj-database-url = "~=0.5.0"
dj-static = "~=0.0.6"
Django = "~=3.1.12"
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"
envparse = "*"
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"
Pillow = "~=9.3.0"
premailer = "~=3.7.0"
progress = "~=1.5"
psutil = "~=5.8.0"
psycopg2 = "~=2.8.6"
Pygments = "~=2.7.4"
pyparsing = "~=2.4.7"
PyPDF2 = "~=1.26.0"
PyPOM = "~=2.2.0"
PyPDF2 = "~=1.27.5"
PyPOM = "~=2.2.4"
python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21"
pytz = "~=2020.5"
reportlab = "~=3.5.59"
reportlab = "*"
requests = "~=2.25.1"
retrying = "~=1.3.3"
simplejson = "~=3.17.2"
@@ -64,7 +60,6 @@ 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"
@@ -78,10 +73,16 @@ sentry-sdk = "*"
diff-match-patch = "*"
python-barcode = "*"
django-hCaptcha = "*"
importlib-metadata = "*"
django-hcaptcha = "*"
"z3c.rml" = "*"
pikepdf = "*"
django-queryable-properties = "*"
django-mass-edit = "*"
selenium = "~=3.141.0"
[dev-packages]
selenium = "~=3.141.0"
pycodestyle = "*"
pycodestyle = "~=2.9.1"
coveralls = "*"
django-coverage-plugin = "*"
pytest-cov = "*"
@@ -89,6 +90,7 @@ pytest-django = "*"
pluggy = "*"
pytest-splinter = "*"
pytest = "*"
pytest-reverse = "*"
[requires]
python_version = "3.9"

1259
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,9 +9,8 @@ from RIGS import models
def get_oembed(login_url, request, oembed_view, kwargs):
context = {}
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'],
reverse(oembed_view, kwargs=kwargs))
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
context['oembed_url'] = f"{request.scheme}://{request.META['HTTP_HOST']}{reverse(oembed_view, kwargs=kwargs)}"
context['login_url'] = f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}"
resp = render(request, 'login_redirect.html', context=context)
return resp
@@ -25,7 +24,7 @@ def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
if oembed_view is not None:
return get_oembed(login_url, request, oembed_view, kwargs)
else:
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
_checklogin.__doc__ = view_func.__doc__
_checklogin.__dict__ = view_func.__dict__
@@ -55,7 +54,7 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
if oembed_view is not None:
return get_oembed(login_url, request, oembed_view, kwargs)
else:
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
else:
resp = render(request, '403.html')
resp.status_code = 403

View File

@@ -42,8 +42,9 @@ if not DEBUG:
INTERNAL_IPS = ['127.0.0.1']
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
DOMAIN = env('DOMAIN', default='example.com')
ADMINS = [('IT Manager', f'it@{DOMAIN}'), ('Arona Jones', f'arona.jones@{DOMAIN}')]
if DEBUG:
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
@@ -61,12 +62,14 @@ INSTALLED_APPS = (
'users',
'RIGS',
'assets',
'training',
'debug_toolbar',
'registration',
'reversion',
'widget_tweaks',
'hcaptcha',
'massadmin',
)
MIDDLEWARE = (
@@ -187,12 +190,8 @@ LOGOUT_URL = '/user/logout/'
ACCOUNT_ACTIVATION_DAYS = 7
# CAPTCHA settings
if DEBUG or CI:
HCAPTCHA_SITEKEY = '10000000-ffff-ffff-ffff-000000000001'
HCAPTCHA_SECRET = '0x0000000000000000000000000000000000000000'
else:
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY')
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET')
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001')
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000')
# Email
EMAILER_TEST = False
@@ -263,3 +262,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

@@ -84,7 +84,7 @@ class BootstrapSelectElement(Region):
return [self.BootstrapSelectOption(self, i) for i in options]
def set_option(self, name, selected):
options = list((x for x in self.options if x.name == name))
options = [x for x in self.options if x.name == name]
assert len(options) == 1
options[0].set_selected(selected)
@@ -117,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.children[1].CodeMirror.getDoc().getValue();", self.root.get_attribute("id"))
def set_value(self, value):
self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
class CheckBox(Region):
def toggle(self):
self.root.click()
@@ -136,7 +145,7 @@ class RadioSelect(Region): # Currently only works for yes/no radio selects
value = "0"
else:
value = "1"
self.find_element(By.XPATH, "//label[@for='{}_{}']".format(self.root.get_attribute("id"), value)).click()
self.find_element(By.XPATH, f"//label[@for='{self.root.get_attribute('id')}_{value}']").click()
@property
def value(self):

View File

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

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'),
@@ -22,10 +23,12 @@ urlpatterns = [
name="api_secure"),
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
path('search/', login_required(views.Search.as_view()), name='search'),
path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'),
path('', include('users.urls')),
path('admin/', include('massadmin.urls')),
path('admin/', admin.site.urls),
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
]

View File

@@ -1,32 +1,52 @@
import datetime
import operator
from functools import reduce
import re
import urllib.error
import urllib.parse
import urllib.request
import simplejson
from functools import reduce
from itertools import chain
from io import BytesIO
from PyPDF2 import PdfFileMerger, PdfFileReader
from z3c.rml import rml2pdf
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.core import serializers
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.http import HttpResponse
from django.http import HttpResponse, JsonResponse
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 django.template.loader import get_template
from django.utils import timezone
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'
def get_related(form, context): # Get some other objects to include in the form. Used when there are errors but also nice and quick.
for field, model in form.related_models.items():
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
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):
context = super(Index, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['rig_count'] = models.Event.objects.rig_count()
return context
@@ -38,7 +58,9 @@ class SecureAPIRequest(generic.View):
'organisation': models.Organisation,
'profile': models.Profile,
'event': models.Event,
'supplier': asset_models.Supplier
'asset': asset_models.Asset,
'supplier': asset_models.Supplier,
'training_item': training_models.TrainingItem,
}
perms = {
@@ -47,7 +69,9 @@ class SecureAPIRequest(generic.View):
'organisation': 'RIGS.view_organisation',
'profile': 'RIGS.view_profile',
'event': None,
'supplier': None
'asset': None,
'supplier': None,
'training_item': None,
}
'''
@@ -75,6 +99,9 @@ class SecureAPIRequest(generic.View):
fields = request.GET.get('fields', None)
if fields:
fields = fields.split(",")
filters = request.GET.get('filters', [])
if filters:
filters = filters.split(",")
# Supply data for one record
if pk:
@@ -95,8 +122,13 @@ class SecureAPIRequest(generic.View):
for field in fields:
q = Q(**{field + "__icontains": part})
qs.append(q)
queries.append(reduce(operator.or_, qs))
for f in filters:
q = Q(**{f: True})
queries.append(q)
# Build the data response list
results = []
query = reduce(operator.and_, queries)
@@ -108,14 +140,13 @@ class SecureAPIRequest(generic.View):
'text': o.name,
}
try: # See if there is a valid update URL
data['update'] = reverse("%s_update" % model, kwargs={'pk': o.pk})
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
except NoReverseMatch:
pass
results.append(data)
# return a data response
json = simplejson.dumps(results)
return HttpResponse(json, content_type="application/json") # Always json
return JsonResponse(results, safe=False)
start = request.GET.get('start', None)
end = request.GET.get('end', None)
@@ -140,8 +171,7 @@ class SecureAPIRequest(generic.View):
}
results.append(data)
json = simplejson.dumps(results)
return HttpResponse(json, content_type="application/json") # Always json
return JsonResponse(results, safe=False)
return HttpResponse(model)
@@ -165,27 +195,14 @@ class GenericListView(generic.ListView):
paginate_by = 20
def get_context_data(self, **kwargs):
context = super(GenericListView, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['page_title'] = self.model.__name__ + "s"
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
def get_queryset(self):
q = self.request.GET.get('q', "")
filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(
phone__startswith=q) | Q(phone__endswith=q)
# try and parse an int
try:
val = int(q)
filter = filter | Q(pk=val)
except: # noqa
# not an integer
pass
object_list = self.model.objects.filter(filter)
object_list = self.model.objects.search(query=self.request.GET.get('q', ""))
orderBy = self.request.GET.get('orderBy', "name")
if orderBy != "":
@@ -197,8 +214,8 @@ class GenericDetailView(generic.DetailView):
template_name = "generic_detail.html"
def get_context_data(self, **kwargs):
context = super(GenericDetailView, self).get_context_data(**kwargs)
context['page_title'] = "{} | {}".format(self.model.__name__, self.object.name)
context = super().get_context_data(**kwargs)
context['page_title'] = f"{self.model.__name__} | {self.object.name}"
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
@@ -208,8 +225,8 @@ class GenericUpdateView(generic.UpdateView):
template_name = "generic_form.html"
def get_context_data(self, **kwargs):
context = super(GenericUpdateView, self).get_context_data(**kwargs)
context['page_title'] = "Edit {}".format(self.model.__name__)
context = super().get_context_data(**kwargs)
context['page_title'] = f"Edit {self.model.__name__}"
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
@@ -219,13 +236,60 @@ class GenericCreateView(generic.CreateView):
template_name = "generic_form.html"
def get_context_data(self, **kwargs):
context = super(GenericCreateView, self).get_context_data(**kwargs)
context['page_title'] = "Create {}".format(self.model.__name__)
context = super().get_context_data(**kwargs)
context['page_title'] = f"Create {self.model.__name__}"
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
class Search(generic.ListView):
template_name = 'search_results.html'
paginate_by = 20
count = 0
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['count'] = self.count or 0
context['query'] = self.request.GET.get('q')
context['page_title'] = f"{context['count']} search results for <b>{context['query']}</b>"
return context
def get_queryset(self):
request = self.request
query = request.GET.get('q', None)
if query is not None:
event_results = models.Event.objects.search(query)
person_results = models.Person.objects.search(query)
organisation_results = models.Organisation.objects.search(query)
venue_results = models.Venue.objects.search(query)
invoice_results = models.Invoice.objects.search(query)
asset_results = asset_models.Asset.objects.search(query)
supplier_results = asset_models.Supplier.objects.search(query)
trainee_results = training_models.Trainee.objects.search(query)
training_item_results = training_models.TrainingItem.objects.search(query)
# combine querysets
queryset_chain = chain(
event_results,
person_results,
organisation_results,
venue_results,
invoice_results,
asset_results,
supplier_results,
trainee_results,
training_item_results,
)
qs = sorted(queryset_chain,
key=lambda instance: instance.pk,
reverse=True)
self.count = len(qs) # since qs is actually a list
return qs
return models.Event.objects.none() # just an empty queryset as default
class SearchHelp(generic.TemplateView):
template_name = 'search_help.html'
@@ -245,14 +309,72 @@ class CloseModal(generic.TemplateView):
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)
full_url = f"{request.scheme}://{request.META['HTTP_HOST']}{embed_url}"
data = {
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
'html': f'<iframe src="{full_url}" frameborder="0" width="100%" height="250"></iframe>',
'version': '1.0',
'type': 'rich',
'height': '250'
}
json = simplejson.JSONEncoderForHTML().encode(data)
return HttpResponse(json, content_type="application/json")
return JsonResponse(data)
def get_info_string(user):
user_str = f"by {user.name} " if user else ""
time = timezone.now().strftime('%d/%m/%Y %H:%I')
return f"[Paperwork generated {user_str}on {time}"
def render_pdf_response(template, context, append_terms):
merger = PdfFileMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
if append_terms:
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(BytesIO(terms.read()))
merged = BytesIO()
merger.write(merged)
response = HttpResponse(content_type='application/pdf')
f = context['filename']
response['Content-Disposition'] = f'filename="{f}"'
response.write(merged.getvalue())
return response
class PrintView(generic.View):
append_terms = False
def get_context_data(self, **kwargs):
obj = get_object_or_404(self.model, pk=self.kwargs['pk'])
object_name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', obj.name)
context = {
'object': obj,
'current_user': self.request.user,
'object_name': object_name,
'info_string': get_info_string(self.request.user) + f"- {obj.current_version_id}]",
}
return context
def get(self, request, pk):
return render_pdf_response(get_template(self.template_name), self.get_context_data(), self.append_terms)
class PrintListView(generic.ListView):
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['current_user'] = self.request.user
context['info_string'] = get_info_string(self.request.user) + "]"
return context
def get(self, request):
self.object_list = self.get_queryset()
return render_pdf_response(get_template(self.template_name), self.get_context_data(), False)

View File

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

View File

@@ -8,30 +8,153 @@ from django.db.models import Count
from django.forms import ModelForm
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.db import IntegrityError
from reversion import revisions as reversion
from reversion.admin import VersionAdmin
from RIGS import models
from users import forms as user_forms
# Register your models here.
admin.site.register(models.VatRate, VersionAdmin)
admin.site.register(models.Event, VersionAdmin)
admin.site.register(models.EventItem, VersionAdmin)
admin.site.register(models.Invoice, VersionAdmin)
def approve_user(modeladmin, request, queryset):
queryset.update(is_approved=True)
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
def merge_model_instances(primary_object, alias_objects):
"""
Merge several model instances into one, the `primary_object`.
Use this function to merge model objects and migrate all of the related
fields from the alias objects the primary object.
"""
# get related fields
related_fields = list(filter(
lambda x: x.is_relation is True,
primary_object._meta.get_fields()))
many_to_many_fields = list(filter(
lambda x: x.many_to_many is True, related_fields))
related_fields = list(filter(
lambda x: x.many_to_many is False, related_fields))
# Loop through all alias objects and migrate their references to the
# primary object
deleted_objects = []
deleted_objects_count = 0
for alias_object in alias_objects:
# Migrate all foreign key references from alias object to primary
# object.
for many_to_many_field in many_to_many_fields:
alias_varname = many_to_many_field.name
related_objects = getattr(alias_object, alias_varname)
for obj in related_objects.all():
try:
# Handle regular M2M relationships.
getattr(alias_object, alias_varname).remove(obj)
getattr(primary_object, alias_varname).add(obj)
except AttributeError:
# Handle M2M relationships with a 'through' model.
# This does not delete the 'through model.
# TODO: Allow the user to delete a duplicate 'through' model.
through_model = getattr(alias_object, alias_varname).through
kwargs = {
many_to_many_field.m2m_reverse_field_name(): obj,
many_to_many_field.m2m_field_name(): alias_object,
}
through_model_instances = through_model.objects.filter(**kwargs)
for instance in through_model_instances:
# Re-attach the through model to the primary_object
setattr(
instance,
many_to_many_field.m2m_field_name(),
primary_object)
instance.save()
# TODO: Here, try to delete duplicate instances that are
# disallowed by a unique_together constraint
for related_field in related_fields:
if related_field.one_to_many:
with transaction.atomic():
try:
alias_varname = related_field.get_accessor_name()
related_objects = getattr(alias_object, alias_varname)
for obj in related_objects.all():
field_name = related_field.field.name
setattr(obj, field_name, primary_object)
obj.save()
except IntegrityError:
pass # Skip to avoid integrity error from unique_together
elif related_field.one_to_one or related_field.many_to_one:
alias_varname = related_field.name
if hasattr(alias_object, alias_varname):
related_object = getattr(alias_object, alias_varname)
primary_related_object = getattr(primary_object, alias_varname)
if primary_related_object is None:
setattr(primary_object, alias_varname, related_object)
primary_object.save()
elif related_field.one_to_one:
related_object.delete()
if alias_object.id:
deleted_objects += [alias_object]
alias_object.delete()
deleted_objects_count += 1
return primary_object, deleted_objects, deleted_objects_count
approve_user.short_description = "Approve selected users"
class AssociateAdmin(VersionAdmin):
search_fields = ['id', 'name']
list_display_links = ['id', 'name']
actions = ['merge']
def get_queryset(self, request):
return super().get_queryset(request).annotate(event_count=Count('event'))
def number_of_events(self, obj):
return obj.latest_events.count()
number_of_events.admin_order_field = 'event_count'
def merge(self, request, queryset):
if request.POST.get('post'): # Has the user confirmed which is the master record?
try:
master_object_pk = request.POST.get('master')
master_object = queryset.get(pk=master_object_pk)
except ObjectDoesNotExist:
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
return
primary_object, deleted_objects, deleted_objects_count = merge_model_instances(master_object, queryset.exclude(pk=master_object_pk).all())
reversion.set_comment('Merging Objects')
self.message_user(request, f"Objects successfully merged. {deleted_objects_count} old objects deleted.")
else: # Present the confirmation screen
class TempForm(ModelForm):
class Meta:
model = queryset.model
fields = self.merge_fields
forms = []
for obj in queryset:
forms.append(TempForm(instance=obj))
context = {
'title': _("Are you sure?"),
'queryset': queryset,
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
'forms': forms
}
return TemplateResponse(request, 'admin_associate_merge.html', context)
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin):
# Don't know how to add 'is_approved' whilst preserving the default list...
list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
class ProfileAdmin(UserAdmin, AssociateAdmin):
list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events')
list_display_links = ['username']
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {
@@ -49,62 +172,12 @@ class ProfileAdmin(UserAdmin):
)
form = user_forms.ProfileChangeForm
add_form = user_forms.ProfileCreationForm
actions = [approve_user]
actions = ['approve_user', 'merge']
merge_fields = ['username', 'first_name', 'last_name', 'initials', 'email', 'phone', 'is_supervisor']
class AssociateAdmin(VersionAdmin):
list_display = ('id', 'name', 'number_of_events')
search_fields = ['id', 'name']
list_display_links = ['id', 'name']
actions = ['merge']
merge_fields = ['name']
def get_queryset(self, request):
return super(AssociateAdmin, self).get_queryset(request).annotate(event_count=Count('event'))
def number_of_events(self, obj):
return obj.latest_events.count()
number_of_events.admin_order_field = 'event_count'
def merge(self, request, queryset):
if request.POST.get('post'): # Has the user confirmed which is the master record?
try:
masterObjectPk = request.POST.get('master')
masterObject = queryset.get(pk=masterObjectPk)
except ObjectDoesNotExist:
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
return
with transaction.atomic(), reversion.create_revision():
for obj in queryset.exclude(pk=masterObjectPk):
events = obj.event_set.all()
for event in events:
masterObject.event_set.add(event)
obj.delete()
reversion.set_comment('Merging Objects')
self.message_user(request, "Objects successfully merged.")
return
else: # Present the confirmation screen
class TempForm(ModelForm):
class Meta:
model = queryset.model
fields = self.merge_fields
forms = []
for obj in queryset:
forms.append(TempForm(instance=obj))
context = {
'title': _("Are you sure?"),
'queryset': queryset,
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
'forms': forms
}
return TemplateResponse(request, 'admin_associate_merge.html', context)
def approve_user(modeladmin, request, queryset):
queryset.update(is_approved=True)
@admin.register(models.Person)

View File

@@ -8,6 +8,7 @@ from django.utils import timezone
from reversion import revisions as reversion
from RIGS import models
from training.models import TrainingLevel
# Override the django form defaults to use the HTML date/time/datetime UI elements
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
@@ -96,10 +97,10 @@ class EventForm(forms.ModelForm):
raise forms.ValidationError(
'You haven\'t provided any client contact details. Please add a person or organisation.',
code='contact')
return super(EventForm, self).clean()
return super().clean()
def save(self, commit=True):
m = super(EventForm, self).save(commit=False)
m = super().save(commit=False)
if (commit):
m.save()
@@ -138,7 +139,7 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
def __init__(self, **kwargs):
super(InternalClientEventAuthorisationForm, self).__init__(**kwargs)
super().__init__(**kwargs)
self.fields['uni_id'].required = True
self.fields['account_code'].required = True
@@ -152,8 +153,12 @@ class EventAuthorisationRequestForm(forms.Form):
class EventRiskAssessmentForm(forms.ModelForm):
related_models = {
'power_mic': models.Profile,
}
def __init__(self, *args, **kwargs):
super(EventRiskAssessmentForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
if str(name) == 'supervisor_consulted':
field.widget = forms.CheckboxInput()
@@ -164,13 +169,16 @@ class EventRiskAssessmentForm(forms.ModelForm):
], attrs={'class': 'custom-control-input', 'required': 'true'})
def clean(self):
if self.cleaned_data.get('big_power'):
if not self.cleaned_data.get('power_mic').level_qualifications.filter(level__department=TrainingLevel.POWER).exists():
self.add_error('power_mic', forms.ValidationError("Your Power MIC must be a Power Technician.", code="power_tech_required"))
# Check expected values
unexpected_values = []
for field, value in models.RiskAssessment.expected_values.items():
if self.cleaned_data.get(field) != value:
unexpected_values.append("<li>{}</li>".format(self._meta.model._meta.get_field(field).help_text))
unexpected_values.append(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>")
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
raise forms.ValidationError("Your answers to these questions: <ul>{}</ul> require consulting with a supervisor.".format(''.join([str(elem) for elem in unexpected_values])), code='unusual_answers')
raise forms.ValidationError(f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.", code='unusual_answers')
return super(EventRiskAssessmentForm, self).clean()
class Meta:
@@ -181,7 +189,7 @@ class EventRiskAssessmentForm(forms.ModelForm):
class EventChecklistForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(EventChecklistForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.fields['date'].widget.format = '%Y-%m-%d'
for name, field in self.fields.items():
if field.__class__ == forms.NullBooleanField:
@@ -209,7 +217,7 @@ class EventChecklistForm(forms.ModelForm):
for key in vehicles:
pk = int(key.split('_')[1])
driver_key = 'driver_' + str(pk)
if(self.data[driver_key] == ''):
if (self.data[driver_key] == ''):
raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch')
else:
try:
@@ -231,9 +239,9 @@ class EventChecklistForm(forms.ModelForm):
pk = int(key.split('_')[1])
for field in other_fields:
value = self.data['{}_{}'.format(field, pk)]
value = self.data[f'{field}_{pk}']
if value == '':
raise forms.ValidationError('Add a {} to crewmember {}'.format(field, pk), code='{}_mismatch'.format(field))
raise forms.ValidationError(f'Add a {field} to crewmember {pk}', code=f'{field}_mismatch')
try:
item = models.EventChecklistCrew.objects.get(pk=pk)

View File

@@ -3,6 +3,7 @@ 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):
@@ -31,6 +32,11 @@ class Command(BaseCommand):
self.delete_objects(rigsmodels.Payment)
self.delete_objects(rigsmodels.RiskAssessment)
self.delete_objects(rigsmodels.EventChecklist)
self.delete_objects(tmodels.TrainingCategory)
self.delete_objects(tmodels.TrainingItem)
self.delete_objects(tmodels.TrainingLevel)
self.delete_objects(tmodels.TrainingItemQualification)
self.delete_objects(tmodels.TrainingLevelRequirement)
def delete_objects(self, model):
for obj in model.objects.all():

View File

@@ -12,3 +12,4 @@ class Command(BaseCommand):
call_command('generateSampleUserData')
call_command('generateSampleRIGSData')
call_command('generateSampleAssetsData')
call_command('generateSampleTrainingData')

View File

@@ -21,6 +21,7 @@ class Command(BaseCommand):
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):
@@ -35,6 +36,7 @@ class Command(BaseCommand):
self.setup_organisations()
self.setup_venues()
self.setup_events()
print("Done generating rigboard data")
def setup_people(self):
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe",

View File

@@ -0,0 +1,38 @@
import premailer
import datetime
from django.template.loader import get_template
from django.contrib.staticfiles import finders
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.core.mail import EmailMultiAlternatives
from django.utils import timezone
from django.urls import reverse
from RIGS import models
class Command(BaseCommand):
help = 'Sends email reminders as required. Triggered daily through heroku-scheduler in production.'
def handle(self, *args, **options):
events = models.Event.objects.current_events().select_related('riskassessment')
for event in events:
earliest_time = event.earliest_time if isinstance(event.earliest_time, datetime.datetime) else timezone.make_aware(datetime.datetime.combine(event.earliest_time, datetime.time(00, 00)))
# 48 hours = 172800 seconds
if event.is_rig and not event.cancelled and not event.dry_hire and (earliest_time - timezone.now()).total_seconds() <= 172800 and not hasattr(event, 'riskassessment'):
context = {
"event": event,
"url": "https://" + settings.DOMAIN + reverse('event_ra', kwargs={'pk': event.pk})
}
target = event.mic.email if event.mic else f"productions@{settings.DOMAIN}"
msg = EmailMultiAlternatives(
f"{event} - Risk Assessment Incomplete",
get_template("email/ra_reminder.txt").render(context),
to=[target],
reply_to=[f"h.s.manager@{settings.DOMAIN}"],
)
css = finders.find('css/email.css')
html = premailer.Premailer(get_template("email/ra_reminder.html").render(context), external_styles=css).transform()
msg.attach_alternative(html, 'text/html')
msg.send()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-10-20 23:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0044_profile_is_supervisor'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='is_approved',
field=models.BooleanField(default=False, help_text='Designates whether a staff member has approved this user.', verbose_name='Approval Status'),
),
]

View File

@@ -8,6 +8,7 @@ from urllib.parse import urlparse
import pytz
from django import forms
from django.db.models import Q, F
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
@@ -17,16 +18,31 @@ from django.utils import timezone
from django.utils.functional import cached_property
from reversion import revisions as reversion
from reversion.models import Version
from versioning.versioning import RevisionMixin
def filter_by_pk(filt, query):
# try and parse an int
try:
val = int(query)
filt = filt | Q(pk=val)
except: # noqa
# not an integer
pass
return filt
class Profile(AbstractUser):
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
initials = models.CharField(max_length=5, null=True, blank=False)
phone = models.CharField(max_length=13, blank=True, default='')
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
is_approved = models.BooleanField(default=False)
is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.")
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
last_emailed = models.DateTimeField(blank=True, null=True)
dark_theme = models.BooleanField(default=False)
is_supervisor = models.BooleanField(default=False)
reversion_hide = True
@classmethod
def make_api_key(cls):
@@ -47,7 +63,7 @@ class Profile(AbstractUser):
def name(self):
name = self.get_full_name()
if self.initials:
name += ' "{}"'.format(self.initials)
name += f' "{self.initials}"'
return name
@property
@@ -65,51 +81,29 @@ class Profile(AbstractUser):
def __str__(self):
return self.name
# TODO move to versioning - currently get import errors with that
class ContactableManager(models.Manager):
def search(self, query=None):
qs = self.get_queryset()
if query is not None:
or_lookup = Q(name__icontains=query) | Q(email__icontains=query) | Q(address__icontains=query) | Q(notes__icontains=query) | Q(
phone__startswith=query) | Q(phone__endswith=query)
class RevisionMixin(object):
@property
def is_first_version(self):
versions = Version.objects.get_for_object(self)
return len(versions) == 1
or_lookup = filter_by_pk(or_lookup, query)
@property
def current_version(self):
version = Version.objects.get_for_object(self).select_related('revision').first()
return version
@property
def last_edited_at(self):
version = self.current_version
if version is None:
return None
return version.revision.date_created
@property
def last_edited_by(self):
version = self.current_version
if version is None:
return None
return version.revision.user
@property
def current_version_id(self):
version = self.current_version
if version is None:
return None
return "V{0} | R{1}".format(version.pk, version.revision.pk)
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
class Person(models.Model, RevisionMixin):
name = models.CharField(max_length=50)
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='')
objects = ContactableManager()
def __str__(self):
string = self.name
if self.notes is not None:
@@ -141,12 +135,12 @@ class Organisation(models.Model, RevisionMixin):
name = models.CharField(max_length=50)
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='')
union_account = models.BooleanField(default=False)
objects = ContactableManager()
def __str__(self):
string = self.name
if self.notes is not None:
@@ -206,7 +200,7 @@ class VatRate(models.Model, RevisionMixin):
get_latest_by = 'start_at'
def __str__(self):
return self.comment + " " + str(self.start_at) + " @ " + str(self.as_percent) + "%"
return f"{self.comment} {self.start_at} @ {self.as_percent}%"
class Venue(models.Model, RevisionMixin):
@@ -215,9 +209,10 @@ class Venue(models.Model, RevisionMixin):
email = models.EmailField(blank=True, default='')
three_phase_available = models.BooleanField(default=False)
notes = models.TextField(blank=True, default='')
address = models.TextField(blank=True, default='')
objects = ContactableManager()
def __str__(self):
string = self.name
if self.notes and len(self.notes) > 0:
@@ -291,6 +286,23 @@ class EventManager(models.Manager):
return events
def search(self, query=None):
qs = self.get_queryset()
if query is not None:
or_lookup = Q(name__icontains=query) | Q(description__icontains=query) | Q(notes__icontains=query)
or_lookup = filter_by_pk(or_lookup, query)
try:
if query[0] == "N":
val = int(query[1:])
or_lookup = Q(pk=val) # If string is N###### then do a simple PK filter
except: # noqa
pass
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
@reversion.register(follow=['items'])
class Event(models.Model, RevisionMixin):
@@ -345,11 +357,9 @@ class Event(models.Model, RevisionMixin):
def display_id(self):
if self.pk:
if self.is_rig:
return str("N%05d" % self.pk)
else:
return self.pk
else:
return "????"
return f"N{self.pk:05d}"
return self.pk
return "????"
# Calculated values
"""
@@ -474,7 +484,7 @@ class Event(models.Model, RevisionMixin):
return reverse('event_detail', kwargs={'pk': self.pk})
def __str__(self):
return "{}: {}".format(self.display_id, self.name)
return f"{self.display_id}: {self.name}"
def clean(self):
errdict = {}
@@ -520,11 +530,11 @@ class EventItem(models.Model, RevisionMixin):
ordering = ['order']
def __str__(self):
return "{}.{}: {} | {}".format(self.event_id, self.order, self.event.name, self.name)
return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
@property
def activity_feed_string(self):
return str("item {}".format(self.name))
return f"item {self.name}"
@reversion.register
@@ -542,7 +552,7 @@ class EventAuthorisation(models.Model, RevisionMixin):
@property
def activity_feed_string(self):
return "{} (requested by {})".format(self.event.display_id, self.sent_by.initials)
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
class InvoiceManager(models.Manager):
@@ -561,6 +571,34 @@ class InvoiceManager(models.Manager):
query = self.raw(sql)
return query
def search(self, query=None):
qs = self.get_queryset()
if query is not None:
or_lookup = Q(event__name__icontains=query)
or_lookup = filter_by_pk(or_lookup, query)
# try and parse an int
try:
val = int(query)
or_lookup = or_lookup | Q(event__pk=val)
except: # noqa
# not an integer
pass
try:
if query[0] == "N":
val = int(query[1:])
or_lookup = Q(event__pk=val) # If string is Nxxxxx then filter by event number
elif query[0] == "#":
val = int(query[1:])
or_lookup = Q(pk=val) # If string is #xxxxx then filter by invoice number
except: # noqa
pass
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
@reversion.register(follow=['payment_set'])
class Invoice(models.Model, RevisionMixin):
@@ -600,14 +638,14 @@ class Invoice(models.Model, RevisionMixin):
@property
def activity_feed_string(self):
return "#{} for Event {}".format(self.display_id, self.event.display_id)
return f"{self.display_id} for Event {self.event.display_id}"
def __str__(self):
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
return f"{self.display_id}: {self.event}{self.balance:.2f})"
@property
def display_id(self):
return "{:05d}".format(self.pk)
return f"#{self.pk:05d}"
class Meta:
ordering = ['-invoice_date']
@@ -636,11 +674,11 @@ class Payment(models.Model, RevisionMixin):
reversion_hide = True
def __str__(self):
return "%s: %d" % (self.get_method_display(), self.amount)
return f"{self.get_method_display()}: {self.amount}"
@property
def activity_feed_string(self):
return str("payment of £{}".format(self.amount))
return f"payment of £{self.amount}"
def validate_url(value):
@@ -670,7 +708,6 @@ class RiskAssessment(models.Model, RevisionMixin):
# Power
big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
# If yes to the above two, you must answer...
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)")
outside = models.BooleanField(help_text="Is the event outdoors?")
@@ -713,7 +750,7 @@ class RiskAssessment(models.Model, RevisionMixin):
'contractors': False,
'other_companies': False,
'crew_fatigue': False,
'big_power': False,
# 'big_power': False Doesn't require checking with a super either way
'generators': False,
'other_companies_power': False,
'nonstandard_equipment_power': False,
@@ -755,15 +792,22 @@ class RiskAssessment(models.Model, RevisionMixin):
else:
return self.SMALL[0]
def get_event_size_display(self):
return self.SIZES[self.event_size][1] + " Event"
@property
def activity_feed_string(self):
return str(self.event)
@property
def name(self):
return str(self)
def get_absolute_url(self):
return reverse('ra_detail', kwargs={'pk': self.pk})
def __str__(self):
return "%i - %s" % (self.pk, self.event)
return f"{self.pk} | {self.event}"
@reversion.register(follow=['vehicles', 'crew'])
@@ -845,7 +889,7 @@ class EventChecklist(models.Model, RevisionMixin):
return reverse('ec_detail', kwargs={'pk': self.pk})
def __str__(self):
return "%i - %s" % (self.pk, self.event)
return f"{self.pk} - {self.event}"
@reversion.register
@@ -857,7 +901,7 @@ class EventChecklistVehicle(models.Model, RevisionMixin):
reversion_hide = True
def __str__(self):
return "{} driven by {}".format(self.vehicle, str(self.driver))
return f"{self.vehicle} driven by {self.driver}"
@reversion.register
@@ -875,4 +919,4 @@ class EventChecklistCrew(models.Model, RevisionMixin):
raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.')
def __str__(self):
return "{} ({})".format(str(self.crewmember), self.role)
return f"{self.crewmember} ({self.role})"

View File

@@ -54,23 +54,23 @@ def send_eventauthorisation_success_email(instance):
elif instance.event.organisation is not None and instance.email == instance.event.organisation.email:
context['to_name'] = instance.event.organisation.name
subject = "N%05d | %s - Event Authorised" % (instance.event.pk, instance.event.name)
subject = f"{instance.event.display_id} | {instance.event.name} - Event Authorised"
client_email = EmailMultiAlternatives(
subject,
get_template("eventauthorisation_client_success.txt").render(context),
get_template("email/eventauthorisation_client_success.txt").render(context),
to=[instance.email],
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
)
css = finders.find('css/email.css')
html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
html = Premailer(get_template("email/eventauthorisation_client_success.html").render(context),
external_styles=css).transform()
client_email.attach_alternative(html, 'text/html')
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
client_email.attach(f'{instance.event.display_id} - {escapedEventName} - CONFIRMATION.pdf',
merged.getvalue(),
'application/pdf'
)
@@ -82,7 +82,7 @@ def send_eventauthorisation_success_email(instance):
mic_email = EmailMessage(
subject,
get_template("eventauthorisation_mic_success.txt").render(context),
get_template("email/eventauthorisation_mic_success.txt").render(context),
to=[mic_email_address]
)
@@ -116,13 +116,13 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
}
email = EmailMultiAlternatives(
"%s new users awaiting approval on RIGS" % (context['number_of_users']),
get_template("admin_awaiting_approval.txt").render(context),
f"{context['number_of_users']} new users awaiting approval on RIGS",
get_template("email/admin_awaiting_approval.txt").render(context),
to=[admin.email],
reply_to=[user.email],
)
css = finders.find('css/email.css')
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
html = Premailer(get_template("email/admin_awaiting_approval.html").render(context),
external_styles=css).transform()
email.attach_alternative(html, 'text/html')
email.send()

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 63 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 KiB

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE document SYSTEM "rml.dtd">
<document filename="{{filename}}">
<docinit>
<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>
<stylesheet>
<initialize>
<color id="LightGray" RGB="#D3D3D3"/>
<color id="DarkGray" RGB="#707070"/>
<color id="Brand" RGB="#3853a4"/>
</initialize>
<paraStyle name="style.para" fontName="OpenSans" />
<paraStyle name="blockPara" spaceAfter="5" spaceBefore="5"/>
<paraStyle name="style.Heading1" fontName="OpenSans" fontSize="16" leading="18" spaceAfter="0"/>
<paraStyle name="style.Heading2" fontName="OpenSans-Bold" fontSize="10" spaceAfter="2"/>
<paraStyle name="style.Heading3" fontName="OpenSans" fontSize="10" spaceAfter="0"/>
<paraStyle name="center" alignment="center"/>
<paraStyle name="page-head" alignment="center" fontName="OpenSans-Bold" fontSize="16" leading="18" spaceAfter="0"/>
<paraStyle name="style.event_description" fontName="OpenSans" textColor="DarkGray" />
<paraStyle name="style.item_description" fontName="OpenSans" textColor="DarkGray" leftIndent="10" />
<paraStyle name="style.specific_description" fontName="OpenSans" textColor="DarkGray" fontSize="10" />
<paraStyle name="style.times" fontName="OpenSans" fontSize="10" />
<paraStyle name="style.head_titles" fontName="OpenSans-Bold" fontSize="10" />
<paraStyle name="style.head_numbers" fontName="OpenSans" fontSize="10" />
<paraStyle name="style.emheader" fontName="OpenSans" textColor="White" fontSize="12" backColor="Brand" leading="20" borderPadding="4"/>
<paraStyle name="style.breakbefore" parent="emheader" pageBreakBefore="1"/>
<blockTableStyle id="eventSpecifics">
<blockValign value="top"/>
<lineStyle kind="LINEAFTER" colorName="LightGrey" start="0,0" stop="1,0" thickness="1"/>
</blockTableStyle>
<blockTableStyle id="headLayout">
<blockValign value="top"/>
</blockTableStyle>
<blockTableStyle id="eventDetails">
<blockValign value="top"/>
<blockTopPadding start="0,0" stop="-1,0" length="0"/>
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
</blockTableStyle>
<blockTableStyle id="itemTable">
<blockValign value="top"/>
<lineStyle kind="LINEBELOW" colorName="LightGrey" start="0,0" stop="-1,-1" thickness="1"/>
{#<lineStyle kind="box" colorName="black" thickness="1" start="0,0" stop="-1,-1"/>#}
</blockTableStyle>
<blockTableStyle id="totalTable">
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
<lineStyle kind="LINEBELOW" colorName="LightGrey" start="-2,0" stop="-1,-1" thickness="1"/>
{# <lineStyle cap="default" kind="grid" colorName="black" thickness="1" start="1,0" stop="-1,-1"/> #}
</blockTableStyle>
<blockTableStyle id="infoTable" keepWithNext="true">
<blockLeftPadding start="0,0" stop="-1,-1" length="0"/>
</blockTableStyle>
<blockTableStyle id="paymentTable">
<blockBackground colorName="LightGray" start="0,1" stop="3,1"/>
<blockFont name="OpenSans-Bold" start="0,1" stop="0,1"/>
<blockFont name="OpenSans-Bold" start="2,1" stop="2,1"/>
<lineStyle kind="outline" colorName="black" thickness="1" start="0,1" stop="3,1"/>
</blockTableStyle>
<blockTableStyle id="signatureTable">
<blockTopPadding length="20" />
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
<lineStyle kind="linebelow" start="1,0" stop="1,0" colorName="black"/>
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
</blockTableStyle>
<listStyle name="ol"
bulletFormat="%s."
bulletFontSize="10" />
<listStyle name="ul"
start="bulletchar"
bulletFontSize="10"/>
</stylesheet>
<template title="{{filename}}"> {# Note: page is 595x842 points (1 point=1/72in) #}
<pageTemplate id="Headed" >
<pageGraphics>
<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="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>
<setFont name="OpenSans" size="9"/>
<drawString x="137" y="760">Portland Building, University Park, Nottingham, NG7 2RD</drawString>
<drawString x="137" y="746">www.nottinghamtec.co.uk</drawString>
<drawString x="265" y="746">info@nottinghamtec.co.uk</drawString>
<drawString x="137" y="732">Phone: (0115) 846 8720</drawString>
<setFont name="OpenSans" size="10" />
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
<setFont name="OpenSans" size="7" />
<drawCenteredString x="302.5" y="26">
{{info_string}}
</drawCenteredString>
</pageGraphics>
<frame id="main" x1="50" y1="65" width="495" height="645"/>
</pageTemplate>
<pageTemplate id="Main">
<pageGraphics>
<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>
<setFont name="OpenSans" size="7" />
<drawCenteredString x="302.5" y="26">
{{info_string}}
</drawCenteredString>
</pageGraphics>
<frame id="main" x1="50" y1="65" width="495" height="727"/>
</pageTemplate>
</template>
<story firstPageTemplate="Headed">
<setNextFrame name="main"/>
<nextFrame/>
{% block content %}
{% endblock %}
</story>
</document>

View File

@@ -27,15 +27,12 @@
calendar = new FullCalendar.Calendar(calendarEl, {
themeSystem: 'bootstrap',
//defaultView: 'dayGridMonth', This is now default
aspectRatio: 1.5,
eventTimeFormat: {
'hour': '2-digit',
'minute': '2-digit',
'hour12': false
},
//nowIndicator: true,
//firstDay: 1,
headerToolbar: false,
editable: false,
dayMaxEventRows: true, // allow "more" link when too many events
@@ -58,8 +55,10 @@
};
$(doc).each(function() {
end = $(this).attr('latest')
allDay = false
if(end.indexOf("T") < 0){ //If latest does not contain a time
end = moment(end).add(1, 'days') //End date is non-inclusive, so add a day
end = moment(end + " 23:59").format("YYYY-MM-DD[T]HH:mm:ss")
allDay = true
}
thisEvent = {
@@ -67,7 +66,8 @@
'end': end,
'className': 'modal-href',
'title': $(this).attr('title'),
'url': $(this).attr('url')
'url': $(this).attr('url'),
'allDay': allDay
}
if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){

View File

@@ -0,0 +1,5 @@
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
Just to let you know your event N{{object.eventdisplay_id}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
The TEC Rig Information Gathering System

View File

@@ -0,0 +1,16 @@
{% extends 'base_client_email.html' %}
{% block content %}
<p>Hi {{event.mic.get_full_name|default_if_none:"Productions Manager"}},</p>
{% if event.mic %}
<p>Just to let you know your event {{event.display_id}} <em>requires<em> a pre-event risk assessment completing prior to the event. Please do so as soon as possible.</p>
{% else %}
<p>This is a reminder that event {{event.display_id}} requires a MIC assigning and a risk assessment completing.</p>
{% endif %}
<p>Fill it out here:</p>
<a href="{{url}}" class="btn btn-info"><span class="fas fa-paperclip"></span> Create Risk Assessment</a>
<p>TEC PA &amp; Lighting</p>
{% endblock %}

View File

@@ -0,0 +1,9 @@
Hi {{event.mic.get_full_name|default_if_none:"Productions Manager"}},
{% if event.mic %}
Just to let you know your event {{event.display_id}} requires a risk assessment completing prior to the event. Please do so as soon as possible.
{% else %}
This is a reminder that event {{event.display_id}} requires a MIC assigning and a risk assessment completing.
{% endif %}
The TEC Rig Information Gathering System

View File

@@ -1,5 +1,7 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load markdown_tags %}
{% block content %}
<div class="row my-3 py-3">
{% if not request.is_ajax %}
@@ -43,10 +45,10 @@
{% 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>

View File

@@ -8,11 +8,13 @@
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/easymde.min.js' %}"></script>
{% endblock %}
{% block js %}
@@ -21,8 +23,6 @@
<script src="{% static 'js/interaction.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
{% include 'partials/datetime-fix.html' %}
<script>
const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches;
$(document).ready(function () {
@@ -63,16 +63,26 @@
{% 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>
{% endblock %}
{% block content %}
{% include 'item_modal.html' %}
{% include 'partials/item_modal.html' %}
<form class="itemised_form" role="form" method="POST">
{% csrf_token %}
<div class="row">
@@ -112,7 +122,7 @@
<div class="col-sm-8">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}">
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}">
{% if person %}
<option value="{{form.person.value}}" selected="selected" data-update_url="{% url 'person_update' form.person.value %}">{{ person }}</option>
{% endif %}
@@ -139,7 +149,7 @@
<div class="col-sm-8">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.organisation.id_for_label }}" name="{{ form.organisation.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}" >
<select id="{{ form.organisation.id_for_label }}" name="{{ form.organisation.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}" >
{% if organisation %}
<option value="{{form.organisation.value}}" selected="selected" data-update_url="{% url 'organisation_update' form.organisation.value %}">{{ organisation }}</option>
{% endif %}
@@ -168,7 +178,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>
@@ -197,7 +207,7 @@
<div class="col-sm-8">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %}
<option value="{{form.venue.value}}" selected="selected" data-update_url="{% url 'venue_update' form.venue.value %}">{{ venue }}</option>
{% endif %}
@@ -267,10 +277,8 @@
</div>
<div class="form-group">
<div class="col-sm-offset-4 col-sm-8">
<div class="checkbox">
<label data-toggle="tooltip" title="Mark this event as a dry-hire, so it needs to be checked in at the end">
{% render_field form.dry_hire %}{{ form.dry_hire.label }}
</label>
{{ form.dry_hire.label }} {% render_field form.dry_hire %}
</div>
</div>
</div>
@@ -292,7 +300,7 @@
class="col-sm-4 col-form-label">{{ form.mic.label }}</label>
<div class="col-sm-8">
<select id="{{ form.mic.id_for_label }}" name="{{ form.mic.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
<select id="{{ form.mic.id_for_label }}" name="{{ form.mic.name }}" class="px-0 selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if mic %}
<option value="{{form.mic.value}}" selected="selected" >{{ mic.name }}</option>
{% endif %}
@@ -306,7 +314,7 @@
class="col-sm-4 col-form-label">{{ form.checked_in_by.label }}</label>
<div class="col-sm-8">
<select id="{{ form.checked_in_by.id_for_label }}" name="{{ form.checked_in_by.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
<select id="{{ form.checked_in_by.id_for_label }}" name="{{ form.checked_in_by.name }}" class="px-0 selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if checked_in_by %}
<option value="{{form.checked_in_by.value}}" selected="selected" >{{ checked_in_by.name }}</option>
{% endif %}
@@ -326,7 +334,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" %}
@@ -345,10 +353,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,128 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE document SYSTEM "rml.dtd">
<document filename="{{filename}}">
<docinit>
<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>
{% extends 'base_print.xml' %}
<stylesheet>
<initialize>
<color id="LightGray" RGB="#D3D3D3"/>
<color id="DarkGray" RGB="#707070"/>
</initialize>
<paraStyle name="style.para" fontName="OpenSans" />
<paraStyle name="blockPara" spaceAfter="5" spaceBefore="5"/>
<paraStyle name="style.Heading1" fontName="OpenSans" fontSize="16" leading="18" spaceAfter="0"/>
<paraStyle name="style.Heading2" fontName="OpenSans-Bold" fontSize="10" spaceAfter="2"/>
<paraStyle name="style.Heading3" fontName="OpenSans" fontSize="10" spaceAfter="0"/>
<paraStyle name="center" alignment="center"/>
<paraStyle name="page-head" alignment="center" fontName="OpenSans-Bold" fontSize="16" leading="18" spaceAfter="0"/>
<paraStyle name="style.event_description" fontName="OpenSans" textColor="DarkGray" />
<paraStyle name="style.item_description" fontName="OpenSans" textColor="DarkGray" leftIndent="10" />
<paraStyle name="style.specific_description" fontName="OpenSans" textColor="DarkGray" fontSize="10" />
<paraStyle name="style.times" fontName="OpenSans" fontSize="10" />
<paraStyle name="style.head_titles" fontName="OpenSans-Bold" fontSize="10" />
<paraStyle name="style.head_numbers" fontName="OpenSans" fontSize="10" />
<blockTableStyle id="eventSpecifics">
<blockValign value="top"/>
<lineStyle kind="LINEAFTER" colorName="LightGrey" start="0,0" stop="1,0" thickness="1"/>
</blockTableStyle>
<blockTableStyle id="headLayout">
<blockValign value="top"/>
</blockTableStyle>
<blockTableStyle id="eventDetails">
<blockValign value="top"/>
<blockTopPadding start="0,0" stop="-1,0" length="0"/>
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
</blockTableStyle>
<blockTableStyle id="itemTable">
<blockValign value="top"/>
<lineStyle kind="LINEBELOW" colorName="LightGrey" start="0,0" stop="-1,-1" thickness="1"/>
{#<lineStyle kind="box" colorName="black" thickness="1" start="0,0" stop="-1,-1"/>#}
</blockTableStyle>
<blockTableStyle id="totalTable">
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
<lineStyle kind="LINEBELOW" colorName="LightGrey" start="-2,0" stop="-1,-1" thickness="1"/>
{# <lineStyle cap="default" kind="grid" colorName="black" thickness="1" start="1,0" stop="-1,-1"/> #}
</blockTableStyle>
<blockTableStyle id="infoTable" keepWithNext="true">
<blockLeftPadding start="0,0" stop="-1,-1" length="0"/>
</blockTableStyle>
<blockTableStyle id="paymentTable">
<blockBackground colorName="LightGray" start="0,1" stop="3,1"/>
<blockFont name="OpenSans-Bold" start="0,1" stop="0,1"/>
<blockFont name="OpenSans-Bold" start="2,1" stop="2,1"/>
<lineStyle kind="outline" colorName="black" thickness="1" start="0,1" stop="3,1"/>
</blockTableStyle>
<blockTableStyle id="signatureTable">
<blockTopPadding length="20" />
<blockLeftPadding start="0,0" stop="0,-1" length="0"/>
<lineStyle kind="linebelow" start="1,0" stop="1,0" colorName="black"/>
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
</blockTableStyle>
</stylesheet>
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
<pageTemplate id="Headed" >
<pageGraphics>
<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="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>
<setFont name="OpenSans" size="9"/>
<drawString x="137" y="760">Portland Building, University Park, Nottingham, NG7 2RD</drawString>
<drawString x="137" y="746">www.nottinghamtec.co.uk</drawString>
<drawString x="265" y="746">info@nottinghamtec.co.uk</drawString>
<drawString x="137" y="732">Phone: (0115) 846 8720</drawString>
<setFont name="OpenSans" size="10" />
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
<setFont name="OpenSans" size="7" />
<drawCenteredString x="302.5" y="26">
[Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
</drawCenteredString>
</pageGraphics>
<frame id="main" x1="50" y1="65" width="495" height="645"/>
</pageTemplate>
<pageTemplate id="Main">
<pageGraphics>
<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>
<setFont name="OpenSans" size="7" />
<drawCenteredString x="302.5" y="26">
[Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
</drawCenteredString>
</pageGraphics>
<frame id="main" x1="50" y1="65" width="495" height="727"/>
</pageTemplate>
</template>
<story firstPageTemplate="Headed">
{% include "event_print_page.xml" %}
</story>
</document>
{% block content %}
{% include "event_print_page.xml" %}
{% endblock %}

View File

@@ -1,19 +1,17 @@
{% load markdown_tags %}
{% load filters %}
<setNextFrame name="main"/>
<nextFrame/>
<blockTable style="headLayout" colWidths="330,165">
<tr>
<td>
<h1><b>N{{ object.pk|stringformat:"05d" }}:</b> '{{ object.name }}'<small></small></h1>
<h1><b>N{{ object.pk|stringformat:"05d" }}:</b> '{{ object.name }}'</h1>
<para style="style.event_description">
<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>
@@ -180,15 +178,10 @@
{% for item in object.items.all %}
<tr>
<td>
<para>{{ item.name }}
{% if item.description %}
</para>
<para style="item_description">
<em>{{ item.description|linebreaksxml }}</em>
</para>
<para>
{% endif %}
</para>
<para><b>{{ item.name }}</b></para>
{% if item.description %}
{{ item.description|markdown:"rml" }}
{% endif %}
</td>
<td>£{{ item.cost|floatformat:2 }}</td>
<td>{{ item.quantity }}</td>
@@ -208,9 +201,7 @@
<tr>
<td>
{% if quote %}
<para>
This quote is valid for 30 days unless otherwise arranged.
</para>
<para>This quote is valid for 30 days unless otherwise arranged.</para>
{% endif %}
</td>
{% if object.vat > 0 %}

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,5 +0,0 @@
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
The TEC Rig Information Gathering System

View File

@@ -1,24 +1,33 @@
{% 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 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" class="pr-1">{{ 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" class="pr-1">{{ 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 %}
@@ -33,11 +42,20 @@
</form>
</div>
</div>
<script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/popover.js' %}"></script>
<script src="{% static 'js/clipboard.min.js' %}"></script>
<script>
$('#auth-request-form').on('submit', function () {
$('#auth-request-form button').attr('disabled', true);
});
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 %}

View File

@@ -114,7 +114,7 @@
<div class="card mb-3">
<div class="card-header">Power {% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
<div class="card-body">
{% if event.riskassessment.event_size == 0 %}
{% if object.event.riskassessment.event_size == 0 %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
<dd class="col-2">

View File

@@ -20,8 +20,6 @@
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
{% include 'partials/datetime-fix.html' %}
<script>
$(document).ready(function () {
$('button[data-action=add]').on('click', function (event) {

View File

@@ -0,0 +1,135 @@
{% extends 'base_print.xml' %}
{% load filters %}
{% block content %}
<spacer length="15"/>
<h1>Event Specific Risk Assessment for <strong>{{ object.event }}</strong></h1>
<spacer length="15"/>
<h2>Client: {{ object.event.person|default:object.event.organisation }} | Venue: {{ object.event.venue }} | MIC: {{ object.event.mic }}</h2>
<spacer length="15"/>
<hr/>
<blockTable colWidths="425,100" spaceAfter="15">
<tr>
<td colspan="2"><h3><strong>General</strong></h3></td>
</tr>
<tr>
<td><para>{{ object|help_text:'nonstandard_equipment'|striptags }}</para></td>
<td>{{ object.nonstandard_equipment|yesno|capfirst }}</td>
</tr>
<tr>
<td><para>{{ object|help_text:'nonstandard_use'|striptags }}</para></td>
<td>{{ object.nonstandard_use|yesno|capfirst }}</td>
</tr>
<tr>
<td><para>{{ object|help_text:'contractors'|striptags }}</para></td>
<td>{{ object.contractors|yesno|capfirst }}</td>
</tr>
<tr>
<td><para>{{ object|help_text:'other_companies'|striptags }}</para></td>
<td>{{ object.other_companies|yesno|capfirst }}</td>
</tr>
<tr>
<td><para>{{ object|help_text:'crew_fatigue'|striptags }}</para></td>
<td>{{ object.crew_fatigue|yesno|capfirst }}</td>
</tr>
<tr>
<td><para>{{ object|help_text:'general_notes'|striptags }}</para></td>
<td><para>{{ object.general_notes|default:'No' }}</para></td>
</tr>
<tr>
<td colspan="2"><h3><strong>Power</strong></h3><spacer length="4"/><para textColor="white" backColor={% if object.event_size == 0 %}"green"{% elif object.event_size == 1 %}"yellow"{% else %}"red"{% endif %} borderPadding="3">{{ object.get_event_size_display }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'big_power'|striptags }}</para></td>
<td><para>{{ object.big_power|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'power_mic'|striptags }}</para></td>
<td><para>{{ object.power_mic|default:object.event.mic }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'outside'|striptags }}</para></td>
<td><para>{{ object.outside|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'generators'|striptags }}</para></td>
<td><para>{{ object.generators|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'other_companies_power'|striptags }}</para></td>
<td><para>{{ object.other_companies_power|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'nonstandard_equipment_power'|striptags }}</para></td>
<td><para>{{ object.nonstandard_equipment_power|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'multiple_electrical_environments'|striptags }}</para></td>
<td><para>{{ object.multiple_electrical_environments|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'power_notes'|striptags }}</para></td>
<td><para>{{ object.power_notes|default:'No' }}</para></td>
</tr>
<tr>
<td colspan="2"><h3><strong>Sound</strong></h3></td>
</tr>
<tr>
<td><para>{{ object|help_text:'noise_monitoring'|striptags }}</para></td>
<td><para>{{ object.noise_monitoring|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'sound_notes'|striptags }}</para></td>
<td><para>{{ object.sound_notes|default:'No' }}</para></td>
</tr>
<tr>
<td colspan="2"><h3><strong>Site Details</strong></h3></td>
</tr>
<tr>
<td><para>{{ object|help_text:'known_venue'|striptags }}</para></td>
<td><para>{{ object.known_venue|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'safe_loading'|striptags }}</para></td>
<td><para>{{ object.safe_loading|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'safe_storage'|striptags }}</para></td>
<td><para>{{ object.safe_storage|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'area_outside_of_control'|striptags }}</para></td>
<td><para>{{ object.area_outside_of_control|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'barrier_required'|striptags }}</para></td>
<td><para>{{ object.barrier_required|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'nonstandard_emergency_procedure'|striptags }}</para></td>
<td><para>{{ object.nonstandard_emergency_procedure|yesno|capfirst }}</para></td>
</tr>
<tr>
<td colspan="2"><h3><strong>Structures</strong></h3></td>
</tr>
<tr>
<td><para>{{ object|help_text:'special_structures'|striptags }}</para></td>
<td><para>{{ object.special_structures|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'suspended_structures'|striptags }}</para></td>
<td><para>{{ object.suspended_structures|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para>{{ object|help_text:'persons_responsible_structures'|striptags }}</para></td>
<td><para>{{ object.persons_responsible_structures|default:'N/A' }}</para></td>
</tr>
</blockTable>
<spacer length="15"/>\
<hr/>
<spacer length="15"/>
<para><em>Assessment completed by {{ object.last_edited_by }} on {{ object.last_edited_at }}</em></para>
{% if object.reviewed_by %}
<para><em>Reviewed by {{ object.reviewed_by }} on {{ object.reviewed.at }}</em></para>
{% endif %}
{% endblock %}

View File

@@ -1,7 +1,5 @@
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load help_text from filters %}
{% load yesnoi from filters %}
{% load linkornone from filters %}
{% load filters %}
{% block content %}
<div class="row py-3">
@@ -47,7 +45,7 @@
</dd>
<dt class="col-sm-6">{{ object|help_text:'power_mic'|safe }}</dt>
<dd class="col-sm-6">
{{ object.power_mic.name|default:'None' }}
{{ object.power_mic.name|default:object.event.mic }}
</dd>
<dt class="col-sm-6">{{ object|help_text:'outside' }}</dt>
<dd class="col-sm-6">
@@ -144,7 +142,7 @@
</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 }}
{{ object.persons_responsible_structures|default:'N/A'|linebreaks }}
</dd>
<dt class="col-12">{{ object|help_text:'rigging_plan'|safe }}</dt>
<dd class="col-12">
@@ -157,6 +155,7 @@
</div>
</div>
<div class="col-12 text-right">
{% button 'print' 'ra_print' object.pk %}
<a href="{% url 'ra_edit' object.pk %}" class="btn btn-warning my-3"><span class="fas fa-edit"></span> <span
class="d-none d-sm-inline">Edit</span></a>
<a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View Event</a>

View File

@@ -5,18 +5,16 @@
{% load nice_errors from filters %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/selects.js' %}" async></script>
{% endblock %}
{% block js %}
{{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
function parseBool(str) {
@@ -100,9 +98,9 @@
<label for="{{ form.power_mic.id_for_label }}"
class="col col-form-label">{{ form.power_mic.help_text|safe }}</label>
<div class="col-6">
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if object.power_mic %}
<option value="{{object.power_mic.pk}}" selected="selected">{{ object.power_mic.name }}</option>
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if power_mic %}
<option value="{{form.power_mic.value}}" selected="selected">{{ power_mic }}</option>
{% endif %}
</select>
</div>

View File

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

View File

@@ -40,7 +40,7 @@
<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>
<dd class="col-sm-6">{{ object.organisation.union_account|yesno|capfirst }}</dd>
</dl>
</div>
</div>

View File

@@ -9,7 +9,7 @@
{% if event.internal %}
<a class="btn item-add modal-href event-authorise-request
{% if event.authorised %}
btn-success active
btn-success active disabled
{% 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 %}
@@ -18,7 +18,7 @@
btn-secondary
{% endif %}
"
href="{% url 'event_authorise_request' object.pk %}">
{% if event.authorised %}aria-disabled="true"{% else %}href="{% url 'event_authorise_request' object.pk %}"{% endif %}>
<span class="fas fa-paper-plane"></span>
<span class="d-none d-sm-inline">
{% if event.authorised %}

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">
@@ -14,7 +15,7 @@
{% if object.venue %}
<dt class="col-sm-6">Venue Notes</dt>
<dd class="col-sm-6">
{{ object.venue.notes }}{% if object.venue.three_phase_available %}<br>(Three phase available){%endif%}
{{ object.venue.notes|markdown }}{% if object.venue.three_phase_available %}<br>(Three phase available){%endif%}
</dd>
{% endif %}
@@ -46,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>

View File

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

View File

@@ -1,4 +1,5 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<div class="table-responsive">
<table class="table mb-0" id="event_table">
<thead>
@@ -29,7 +30,15 @@
<!---Number-->
<th scope="row" id="event_number">{{ event.display_id }}</th>
<!--Dates & Times-->
<td id="event_dates">
<td id="event_dates" style="text-align: justify;">
{% if not event.cancelled %}
{% if event.meet_at %}
<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 class="text-nowrap">Access: <strong>{{ event.access_at|date:"D d/m/Y H:i" }}</strong></span>
{% endif %}
{% endif %}
<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" }}
@@ -43,14 +52,6 @@
{% endif %}</strong>
</span>
{% endif %}
{% if not event.cancelled %}
{% if event.meet_at %}
<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 class="text-nowrap">Access: <strong>{{ event.access_at|date:" D d/m/Y H:i" }}</strong></span>
{% endif %}
{% endif %}
</td>
<!---Details-->
<td id="event_details" class="w-100">
@@ -74,7 +75,7 @@
</h5>
{% endif %}
{% if not event.cancelled and event.description %}
<p>{{ event.description|linebreaksbr }}</p>
<p>{{ event.description|markdown }}</p>
{% endif %}
{% include 'partials/event_status.html' %}
</td>

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,8 +1,9 @@
{% 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 %}

View File

@@ -23,7 +23,7 @@
</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 %}

View File

@@ -114,15 +114,13 @@ def orderby(request, field, attr):
return dict_.urlencode()
# Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
@register.filter(needs_autoescape=True)
@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
def get_field(obj, field, autoescape=True):
value = getattr(obj, field)
if(isinstance(value, bool)):
if (isinstance(value, bool)):
value = yesnoi(value, field in obj.inverted_fields)
elif(isinstance(value, str)):
elif (isinstance(value, str)):
value = truncatewords(value, 20)
return mark_safe(value)
@@ -146,7 +144,7 @@ def get_list(dictionary, key):
@register.filter
def profile_by_index(value):
if(value):
if (value):
return models.Profile.objects.get(pk=int(value))
else:
return ""
@@ -198,8 +196,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
text = "Edit"
elif type == 'print':
clazz += " btn-primary "
icon = "fa-print"
text = "Print"
icon = "fa-download"
text = "Export"
elif type == 'duplicate':
clazz += " btn-info "
icon = "fa-copy"
@@ -212,14 +210,18 @@ 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}
elif type == 'today':
return {'today': True, 'id': id}
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}
@register.simple_tag
@register.simple_tag # TODO Can these be done with annotation/aggregation?
def invoices_waiting():
return len(models.Event.objects.waiting_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', add_style=""):
# markdown library can't handle text=None
if text is None:
return text
html = markdown.markdown(text, extensions=['markdown.extensions.nl2br'])
# Convert format to RML
soup = BeautifulSoup(html, "html.parser")
# Prevent code injection
for script in soup('script'):
script.string = "Your script shall not pass!"
if input_format == 'html':
return mark_safe('<div class="markdown">' + str(soup) + '</div>')
elif input_format == 'rml':
# Image aren't supported so remove them
for img in soup('img'):
img.parent.extract()
# <code> should become <font>
for c in soup('code'):
c.name = 'font'
c['face'] = "Courier"
# blockquotes don't exist but we can still do something to show
for bq in soup('blockquote'):
bq.name = 'pre'
bq.string = bq.text
for alist in soup.find_all(['ul', 'ol']):
alist['style'] = alist.name
for li in alist.find_all('li', recursive=False):
text = li.find(text=True)
text.wrap(soup.new_tag('p'))
if alist.parent.name != 'li':
indent = soup.new_tag('indent')
indent['left'] = '0.6cm'
alist.wrap(indent)
# Paragraphs have a different tag
for p in soup('p'):
p.name = 'para'
return mark_safe(str(soup))

View File

@@ -96,7 +96,7 @@ class CreateEvent(FormPage):
_warning_selector = (By.XPATH, '/html/body/div[1]/div[1]')
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):
@@ -145,11 +145,11 @@ class CreateEvent(FormPage):
def add_person(self):
self.find_element(*self._add_person_selector).click()
return regions.Modal(self, self.driver.find_element_by_id('modal'))
return regions.Modal(self, self.driver.find_element(By.ID, 'modal'))
def add_event_item(self):
self.find_element(*self._add_item_selector).click()
element = self.driver.find_element_by_id('itemModal')
element = self.driver.find_element(By.ID, 'itemModal')
self.wait.until(EC.visibility_of(element))
return rigs_regions.ItemModal(self, element)

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'))
}

155
RIGS/tests/sample.md Normal file
View File

@@ -0,0 +1,155 @@
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.

View File

@@ -211,7 +211,7 @@ class TestEventCreate(BaseRigboardTest):
self.assertEqual("Test Item 1", testitem['name'])
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
total = self.driver.find_element_by_id('total')
total = self.driver.find_element(By.ID, 'total')
ActionChains(self.driver).move_to_element(total).perform()
# See new item appear in table
@@ -224,9 +224,9 @@ class TestEventCreate(BaseRigboardTest):
self.assertEqual('47.90', row.subtotal)
# Check totals TODO convert to page properties
self.assertEqual("47.90", self.driver.find_element_by_id('sumtotal').text)
self.assertIn("(TBC)", self.driver.find_element_by_id('vat-rate').text)
self.assertEqual("9.58", self.driver.find_element_by_id('vat').text)
self.assertEqual("47.90", self.driver.find_element(By.ID, 'sumtotal').text)
self.assertIn("(TBC)", self.driver.find_element(By.ID, 'vat-rate').text)
self.assertEqual("9.58", self.driver.find_element(By.ID, 'vat').text)
self.assertEqual("57.48", total.text)
self.page.submit()

View File

@@ -1,8 +1,13 @@
import os
import pytest
from datetime import date
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
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
@@ -10,8 +15,6 @@ from pytest_django.asserts import assertRedirects, assertNotContains, assertCont
from PyRIGS.tests.base import assert_times_almost_equal, assert_oembed, login
from RIGS import models
import pytest
pytestmark = pytest.mark.django_db
@@ -170,6 +173,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())
@@ -281,11 +285,11 @@ def test_xframe_headers(admin_client, basic_event):
response = admin_client.get(event_url, follow=True)
with pytest.raises(KeyError):
response._headers["X-Frame-Options"]
response.headers["X-Frame-Options"]
response = admin_client.get(login_url, follow=True)
with pytest.raises(KeyError):
response._headers["X-Frame-Options"]
response.headers["X-Frame-Options"]
def test_oembed(client, basic_event):
@@ -363,6 +367,60 @@ def test_checklist_review(admin_client, admin_user, checklist):
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 = open(os.path.join(settings.BASE_DIR, "RIGS/tests/sample.md")).read()
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',
)
event_item = models.EventItem.objects.create(event=event, name="TI I1", quantity=1, cost=1.00, order=1, description="* test \n * test \n * test")
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

@@ -5,7 +5,7 @@ from django.views.generic import RedirectView
from PyRIGS.decorators import (api_key_required, has_oembed,
permission_required_with_403)
from RIGS import finance, ical, rigboard, views, hs
from . import views
urlpatterns = [
# People
@@ -42,101 +42,102 @@ urlpatterns = [
name='venue_update'),
# Rigboard
path('rigboard/', login_required(rigboard.RigboardIndex.as_view()), name='rigboard'),
path('rigboard/calendar/', login_required()(rigboard.WebCalendar.as_view()),
path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'),
path('rigboard/calendar/', login_required()(views.WebCalendar.as_view()),
name='web_calendar'),
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/$',
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
login_required()(views.WebCalendar.as_view()), name='web_calendar'),
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$',
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
login_required()(views.WebCalendar.as_view()), name='web_calendar'),
path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
path('event/<int:pk>/', has_oembed(oembed_view="event_oembed")(rigboard.EventDetail.as_view()),
path('event/<int:pk>/', has_oembed(oembed_view="event_oembed")(views.EventDetail.as_view()),
name='event_detail'),
path('event/create/', permission_required_with_403('RIGS.add_event')(rigboard.EventCreate.as_view()),
path('event/create/', permission_required_with_403('RIGS.add_event')(views.EventCreate.as_view()),
name='event_create'),
path('event/archive/', login_required()(rigboard.EventArchive.as_view()),
path('event/archive/', login_required()(views.EventArchive.as_view()),
name='event_archive'),
path('event/<int:pk>/embed/',
xframe_options_exempt(login_required(login_url='/user/login/embed/')(rigboard.EventEmbed.as_view())),
xframe_options_exempt(login_required(login_url='/user/login/embed/')(views.EventEmbed.as_view())),
name='event_embed'),
path('event/<int:pk>/oembed_json/', rigboard.EventOEmbed.as_view(),
path('event/<int:pk>/oembed_json/', views.EventOEmbed.as_view(),
name='event_oembed'),
path('event/<int:pk>/print/', permission_required_with_403('RIGS.view_event')(rigboard.EventPrint.as_view()),
path('event/<int:pk>/print/', permission_required_with_403('RIGS.view_event')(views.EventPrint.as_view()),
name='event_print'),
path('event/<int:pk>/edit/', permission_required_with_403('RIGS.change_event')(rigboard.EventUpdate.as_view()),
path('event/<int:pk>/edit/', permission_required_with_403('RIGS.change_event')(views.EventUpdate.as_view()),
name='event_update'),
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(rigboard.EventDuplicate.as_view()),
path('event/<int:pk>/duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()),
name='event_duplicate'),
# Event H&S
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(hs.HSList.as_view()), name='hs_list'),
path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'),
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(hs.EventRiskAssessmentCreate.as_view()),
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()),
name='event_ra'),
path('event/ra/<int:pk>/', permission_required_with_403('RIGS.view_riskassessment')(hs.EventRiskAssessmentDetail.as_view()),
path('event/ra/<int:pk>/', login_required(views.EventRiskAssessmentDetail.as_view()),
name='ra_detail'),
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(hs.EventRiskAssessmentEdit.as_view()),
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
name='ra_edit'),
path('event/ra/list', permission_required_with_403('RIGS.view_riskassessment')(hs.EventRiskAssessmentList.as_view()),
path('event/ra/list', permission_required_with_403('RIGS.view_riskassessment')(views.EventRiskAssessmentList.as_view()),
name='ra_list'),
path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(hs.EventRiskAssessmentReview.as_view()),
path('event/ra/<int:pk>/review/', permission_required_with_403('RIGS.review_riskassessment')(views.EventRiskAssessmentReview.as_view()),
name='ra_review'),
path('event/ra/<int:pk>/print/', permission_required_with_403('RIGS.view_riskassessment')(views.RAPrint.as_view()), name='ra_print'),
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(hs.EventChecklistCreate.as_view()),
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
name='event_ec'),
path('event/checklist/<int:pk>/', permission_required_with_403('RIGS.view_eventchecklist')(hs.EventChecklistDetail.as_view()),
path('event/checklist/<int:pk>/', login_required(views.EventChecklistDetail.as_view()),
name='ec_detail'),
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(hs.EventChecklistEdit.as_view()),
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
name='ec_edit'),
path('event/checklist/list', permission_required_with_403('RIGS.view_eventchecklist')(hs.EventChecklistList.as_view()),
path('event/checklist/list', permission_required_with_403('RIGS.view_eventchecklist')(views.EventChecklistList.as_view()),
name='ec_list'),
path('event/checklist/<int:pk>/review/', permission_required_with_403('RIGS.review_eventchecklist')(hs.EventChecklistReview.as_view()),
path('event/checklist/<int:pk>/review/', permission_required_with_403('RIGS.review_eventchecklist')(views.EventChecklistReview.as_view()),
name='ec_review'),
# Finance
path('invoice/', permission_required_with_403('RIGS.view_invoice')(finance.InvoiceIndex.as_view()),
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
name='invoice_list'),
path('invoice/archive/', permission_required_with_403('RIGS.view_invoice')(finance.InvoiceArchive.as_view()),
path('invoice/archive/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceArchive.as_view()),
name='invoice_archive'),
path('invoice/waiting/', permission_required_with_403('RIGS.add_invoice')(finance.InvoiceWaiting.as_view()),
path('invoice/waiting/', permission_required_with_403('RIGS.add_invoice')(views.InvoiceWaiting.as_view()),
name='invoice_waiting'),
path('event/<int:pk>/invoice/', permission_required_with_403('RIGS.add_invoice')(finance.InvoiceEvent.as_view()),
path('event/<int:pk>/invoice/', permission_required_with_403('RIGS.add_invoice')(views.InvoiceEvent.as_view()),
name='invoice_event'),
path('event/<int:pk>/invoice/void', permission_required_with_403('RIGS.add_invoice')(finance.InvoiceEvent.as_view()),
path('event/<int:pk>/invoice/void', permission_required_with_403('RIGS.add_invoice')(views.InvoiceEvent.as_view()),
name='invoice_event_void', kwargs={'void': True}),
path('invoice/<int:pk>/', permission_required_with_403('RIGS.view_invoice')(finance.InvoiceDetail.as_view()),
path('invoice/<int:pk>/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceDetail.as_view()),
name='invoice_detail'),
path('invoice/<int:pk>/print/', permission_required_with_403('RIGS.view_invoice')(finance.InvoicePrint.as_view()),
path('invoice/<int:pk>/print/', permission_required_with_403('RIGS.view_invoice')(views.InvoicePrint.as_view()),
name='invoice_print'),
path('invoice/<int:pk>/void/', permission_required_with_403('RIGS.change_invoice')(finance.InvoiceVoid.as_view()),
path('invoice/<int:pk>/void/', permission_required_with_403('RIGS.change_invoice')(views.InvoiceVoid.as_view()),
name='invoice_void'),
path('invoice/<int:pk>/delete/',
permission_required_with_403('RIGS.change_invoice')(finance.InvoiceDelete.as_view()),
permission_required_with_403('RIGS.change_invoice')(views.InvoiceDelete.as_view()),
name='invoice_delete'),
path('payment/create/', permission_required_with_403('RIGS.add_payment')(finance.PaymentCreate.as_view()),
path('payment/create/', permission_required_with_403('RIGS.add_payment')(views.PaymentCreate.as_view()),
name='payment_create'),
path('payment/<int:pk>/delete/', permission_required_with_403('RIGS.add_payment')(finance.PaymentDelete.as_view()),
path('payment/<int:pk>/delete/', permission_required_with_403('RIGS.add_payment')(views.PaymentDelete.as_view()),
name='payment_delete'),
# Client event authorisation
path('event/<pk>/auth/',
permission_required_with_403('RIGS.change_event')(rigboard.EventAuthorisationRequest.as_view()),
permission_required_with_403('RIGS.change_event')(views.EventAuthorisationRequest.as_view()),
name='event_authorise_request'),
path('event/<int:pk>/auth/preview/',
permission_required_with_403('RIGS.change_event')(rigboard.EventAuthoriseRequestEmailPreview.as_view()),
permission_required_with_403('RIGS.change_event')(views.EventAuthoriseRequestEmailPreview.as_view()),
name='event_authorise_preview'),
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', rigboard.EventAuthorise.as_view(),
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', views.EventAuthorise.as_view(),
name='event_authorise'),
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/preview/$', rigboard.EventAuthorise.as_view(preview=True),
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/preview/$', views.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()),
re_path(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(views.CalendarICS()),
name="ics_calendar"),

5
RIGS/views/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .crud import *
from .finance import *
from .hs import *
from .ical import *
from .rigboard import *

View File

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

View File

@@ -24,11 +24,12 @@ class InvoiceIndex(generic.ListView):
template_name = 'invoice_list.html'
def get_context_data(self, **kwargs):
context = super(InvoiceIndex, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
total = 0
for i in context['object_list']:
total += i.balance
context['page_title'] = "Outstanding Invoices ({} Events, £{:.2f})".format(len(list(context['object_list'])), total)
event_count = len(list(context['object_list']))
context['page_title'] = f"Outstanding Invoices ({event_count} Events, £{total:.2f})"
context['description'] = "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger"
return context
@@ -41,8 +42,9 @@ class InvoiceDetail(generic.DetailView):
template_name = 'invoice_detail.html'
def get_context_data(self, **kwargs):
context = super(InvoiceDetail, self).get_context_data(**kwargs)
context['page_title'] = "Invoice {} ({}) ".format(self.object.display_id, self.object.invoice_date.strftime("%d/%m/%Y"))
context = super().get_context_data(**kwargs)
invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date})"
if self.object.void:
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
elif self.object.is_closed:
@@ -58,11 +60,14 @@ class InvoicePrint(generic.View):
object = invoice.event
template = get_template('event_print.xml')
name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
filename = f"Invoice {invoice.display_id} for {object.display_id} {name}.pdf"
context = {
'object': object,
'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))
'filename': filename
}
rml = template.render(context)
@@ -72,7 +77,7 @@ class InvoicePrint(generic.View):
pdfData = buffer.read()
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
response['Content-Disposition'] = f'filename="{filename}"'
response.write(pdfData)
return response
@@ -117,38 +122,13 @@ class InvoiceArchive(generic.ListView):
paginate_by = 25
def get_context_data(self, **kwargs):
context = super(InvoiceArchive, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['page_title'] = "Invoice Archive"
context['description'] = "This page displays all invoices: outstanding, paid, and void"
return context
def get_queryset(self):
q = self.request.GET.get('q', "")
filter = Q(event__name__icontains=q)
# try and parse an int
try:
val = int(q)
filter = filter | Q(pk=val)
filter = filter | Q(event__pk=val)
except: # noqa
# not an integer
pass
try:
if q[0] == "N":
val = int(q[1:])
filter = Q(event__pk=val) # If string is Nxxxxx then filter by event number
elif q[0] == "#":
val = int(q[1:])
filter = Q(pk=val) # If string is #xxxxx then filter by invoice number
except: # noqa
pass
object_list = self.model.objects.filter(filter).order_by('-invoice_date')
return object_list
return self.model.objects.search(self.request.GET.get('q')).order_by('-invoice_date')
class InvoiceWaiting(generic.ListView):
@@ -162,7 +142,7 @@ class InvoiceWaiting(generic.ListView):
objects = self.get_queryset()
for obj in objects:
total += obj.sum_total
context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(objects), total)
context['page_title'] = f"Events for Invoice ({len(objects)} Events, £{total:.2f})"
return context
def get_queryset(self):
@@ -196,7 +176,7 @@ class PaymentCreate(generic.CreateView):
template_name = 'payment_form.html'
def get_initial(self):
initial = super(generic.CreateView, self).get_initial()
initial = super().get_initial()
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
if invoicepk is None:
raise Http404()

View File

@@ -6,11 +6,13 @@ from django.views import generic
from reversion import revisions as reversion
from RIGS import models, forms
from RIGS.views.rigboard import get_related
from PyRIGS.views import PrintView
class EventRiskAssessmentCreate(generic.CreateView):
model = models.RiskAssessment
template_name = 'risk_assessment_form.html'
template_name = 'hs/risk_assessment_form.html'
form_class = forms.EventRiskAssessmentForm
def get(self, *args, **kwargs):
@@ -37,7 +39,8 @@ class EventRiskAssessmentCreate(generic.CreateView):
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
context['event'] = event
context['page_title'] = 'Create Risk Assessment for Event {}'.format(event.display_id)
context['page_title'] = f'Create Risk Assessment for Event {event.display_id}'
get_related(context['form'], context)
return context
def get_success_url(self):
@@ -46,7 +49,7 @@ class EventRiskAssessmentCreate(generic.CreateView):
class EventRiskAssessmentEdit(generic.UpdateView):
model = models.RiskAssessment
template_name = 'risk_assessment_form.html'
template_name = 'hs/risk_assessment_form.html'
form_class = forms.EventRiskAssessmentForm
def get_success_url(self):
@@ -62,24 +65,25 @@ class EventRiskAssessmentEdit(generic.UpdateView):
ra = models.RiskAssessment.objects.get(pk=rpk)
context['event'] = ra.event
context['edit'] = True
context['page_title'] = 'Edit Risk Assessment for Event {}'.format(ra.event.display_id)
context['page_title'] = f'Edit Risk Assessment for Event {ra.event.display_id}'
get_related(context['form'], context)
return context
class EventRiskAssessmentDetail(generic.DetailView):
model = models.RiskAssessment
template_name = 'risk_assessment_detail.html'
template_name = 'hs/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)
context['page_title'] = f"Risk Assessment for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
return context
class EventRiskAssessmentList(generic.ListView):
paginate_by = 20
model = models.RiskAssessment
template_name = 'hs_object_list.html'
template_name = 'hs/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')
@@ -108,17 +112,17 @@ class EventRiskAssessmentReview(generic.View):
class EventChecklistDetail(generic.DetailView):
model = models.EventChecklist
template_name = 'event_checklist_detail.html'
template_name = 'hs/event_checklist_detail.html'
def get_context_data(self, **kwargs):
context = super(EventChecklistDetail, self).get_context_data(**kwargs)
context['page_title'] = "Event Checklist for Event <a href='{}'>{} {}</a>".format(self.object.event.get_absolute_url(), self.object.event.display_id, self.object.event.name)
context['page_title'] = f"Event Checklist for Event <a href='{self.object.event.get_absolute_url()}'>{self.object.event.display_id} {self.object.event.name}</a>"
return context
class EventChecklistEdit(generic.UpdateView):
model = models.EventChecklist
template_name = 'event_checklist_form.html'
template_name = 'hs/event_checklist_form.html'
form_class = forms.EventChecklistForm
def get_success_url(self):
@@ -134,19 +138,14 @@ class EventChecklistEdit(generic.UpdateView):
ec = models.EventChecklist.objects.get(pk=pk)
context['event'] = ec.event
context['edit'] = True
context['page_title'] = 'Edit Event Checklist for Event {}'.format(ec.event.display_id)
form = context['form']
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
for field, model in form.related_models.items():
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
context['page_title'] = f'Edit Event Checklist for Event {ec.event.display_id}'
get_related(context['form'], context)
return context
class EventChecklistCreate(generic.CreateView):
model = models.EventChecklist
template_name = 'event_checklist_form.html'
template_name = 'hs/event_checklist_form.html'
form_class = forms.EventChecklistForm
# From both business logic and programming POVs, RAs must exist before ECs!
@@ -158,7 +157,7 @@ class EventChecklistCreate(generic.CreateView):
ra = models.RiskAssessment.objects.filter(event=event).first()
if ra is None:
messages.error(self.request, 'A Risk Assessment must exist prior to creating any Event Checklists for {}! Please create one now.'.format(event))
messages.error(self.request, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.')
return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
return super(EventChecklistCreate, self).get(self)
@@ -175,7 +174,7 @@ class EventChecklistCreate(generic.CreateView):
epk = self.kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
context['event'] = event
context['page_title'] = 'Create Event Checklist for Event {}'.format(event.display_id)
context['page_title'] = f'Create Event Checklist for Event {event.display_id}'
return context
def get_success_url(self):
@@ -185,7 +184,7 @@ class EventChecklistCreate(generic.CreateView):
class EventChecklistList(generic.ListView):
paginate_by = 20
model = models.EventChecklist
template_name = 'hs_object_list.html'
template_name = 'hs/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')
@@ -215,7 +214,7 @@ class EventChecklistReview(generic.View):
class HSList(generic.ListView):
paginate_by = 20
model = models.Event
template_name = 'hs_list.html'
template_name = 'hs/hs_list.html'
def get_queryset(self):
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
@@ -224,3 +223,13 @@ class HSList(generic.ListView):
context = super(HSList, self).get_context_data(**kwargs)
context['page_title'] = 'H&S Overview'
return context
class RAPrint(PrintView):
model = models.RiskAssessment
template_name = 'hs/ra_print.xml'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filename'] = f"EventSpecificRiskAssessment_for_{context['object'].event.display_id}.pdf"
return context

View File

@@ -93,7 +93,7 @@ class CalendarICS(ICalFeed):
title += item.name
# Add the status
title += ' (' + str(item.get_status_display()) + ')'
title += f' ({item.get_status_display()})'
return title
@@ -101,9 +101,8 @@ class CalendarICS(ICalFeed):
return item.earliest_time
def item_end_datetime(self, item):
if isinstance(item.latest_time, datetime.date): # Ical end_datetime is non-inclusive, so add a day
return item.latest_time + datetime.timedelta(days=1)
# if isinstance(item.latest_time, datetime.date): # Ical end_datetime is non-inclusive, so add a day
# return item.latest_time + datetime.timedelta(days=1)
return item.latest_time
def item_location(self, item):
@@ -115,13 +114,13 @@ class CalendarICS(ICalFeed):
tz = pytz.timezone(self.timezone)
desc = 'Rig ID = ' + str(item.pk) + '\n'
desc += 'Event = ' + item.name + '\n'
desc = f'Rig ID = {item.display_id}\n'
desc += f'Event = {item.name}\n'
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
if item.is_rig and item.person:
desc += 'Client = ' + item.person.name + (
(' for ' + item.organisation.name) if item.organisation else '') + '\n'
desc += 'Status = ' + str(item.get_status_display()) + '\n'
desc += f'Status = {item.get_status_display()}\n'
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
desc += '\n'
@@ -140,23 +139,18 @@ class CalendarICS(ICalFeed):
desc += '\n'
if item.description:
desc += 'Event Description:\n' + item.description + '\n\n'
desc += f'Event Description:\n{item.description}\n\n'
# if item.notes: // Need to add proper keyholder checks before this gets put back
# desc += 'Notes:\n'+item.notes+'\n\n'
base_url = "https://rigs.nottinghamtec.co.uk"
desc += 'URL = ' + base_url + str(item.get_absolute_url())
desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}'
return desc
def item_link(self, item):
# Make a link to the event in the web interface
# base_url = "https://pyrigs.nottinghamtec.co.uk"
return item.get_absolute_url()
# def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) - not really necessary though
# return ''
def item_updated(self, item): # some ical clients will display this
return item.last_edited_at

View File

@@ -1,14 +1,9 @@
import copy
import datetime
import re
import urllib.error
import urllib.parse
import urllib.request
from io import BytesIO
import premailer
import simplejson
from PyPDF2 import PdfFileMerger, PdfFileReader
from django.conf import settings
from django.contrib import messages
from django.contrib.staticfiles import finders
@@ -24,10 +19,9 @@ from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import generic
from z3c.rml import rml2pdf
from PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
from RIGS import models, forms
__author__ = 'ghost'
@@ -38,7 +32,7 @@ class RigboardIndex(generic.TemplateView):
def get_context_data(self, **kwargs):
# get super context
context = super(RigboardIndex, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
# call out method to get current events
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
@@ -50,22 +44,27 @@ class WebCalendar(generic.TemplateView):
template_name = 'calendar.html'
def get_context_data(self, **kwargs):
context = super(WebCalendar, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['view'] = kwargs.get('view', '')
context['date'] = kwargs.get('date', '')
# context['page_title'] = "Calendar"
return context
class EventDetail(generic.DetailView):
class EventDetail(generic.DetailView, ModalURLMixin):
template_name = 'event_detail.html'
model = models.Event
def get_context_data(self, **kwargs):
context = super(EventDetail, self).get_context_data(**kwargs)
title = "{} | {}".format(self.object.display_id, self.object.name)
context = super().get_context_data(**kwargs)
title = f"{self.object.display_id} | {self.object.name}"
if self.object.dry_hire:
title += " <span class='badge badge-secondary'>Dry Hire</span>"
context['page_title'] = title
if is_ajax(self.request):
context['override'] = "base_ajax.html"
else:
context['override'] = 'base_assets.html'
return context
@@ -84,7 +83,7 @@ class EventCreate(generic.CreateView):
template_name = 'event_form.html'
def get_context_data(self, **kwargs):
context = super(EventCreate, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['page_title'] = "New Event"
context['edit'] = True
context['currentVAT'] = models.VatRate.objects.current_rate()
@@ -93,11 +92,8 @@ class EventCreate(generic.CreateView):
if hasattr(form, 'items_json') and re.search(r'"-\d+"', form['items_json'].value()):
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
for field, model in form.related_models.items():
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
get_related(form, context)
return context
def get_success_url(self):
@@ -110,17 +106,13 @@ class EventUpdate(generic.UpdateView):
template_name = 'event_form.html'
def get_context_data(self, **kwargs):
context = super(EventUpdate, self).get_context_data(**kwargs)
context['page_title'] = "Event {}".format(self.object.display_id)
context = super().get_context_data(**kwargs)
context['page_title'] = f"Event {self.object.display_id}"
context['edit'] = True
form = context['form']
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
for field, model in form.related_models.items():
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
get_related(form, context)
return context
@@ -134,7 +126,7 @@ class EventUpdate(generic.UpdateView):
if hasattr(self.object, 'authorised'):
messages.warning(self.request,
'This event has already been authorised by the client, any changes to the price will require reauthorisation.')
return super(EventUpdate, self).render_to_response(context, **response_kwargs)
return super().render_to_response(context, **response_kwargs)
def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
@@ -142,7 +134,7 @@ class EventUpdate(generic.UpdateView):
class EventDuplicate(EventUpdate):
def get_object(self, queryset=None):
old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
old = super().get_object(queryset) # Get the object (the event you're duplicating)
new = copy.copy(old) # Make a copy of the object in memory
new.based_on = old # Make the new event based on the old event
new.purchase_order = None # Remove old PO
@@ -167,41 +159,22 @@ class EventDuplicate(EventUpdate):
return new
def get_context_data(self, **kwargs):
context = super(EventDuplicate, self).get_context_data(**kwargs)
context['page_title'] = "Duplicate of Event {}".format(self.object.display_id)
context = super().get_context_data(**kwargs)
context['page_title'] = f"Duplicate of Event {self.object.display_id}"
context["duplicate"] = True
return context
class EventPrint(generic.View):
def get(self, request, pk):
object = get_object_or_404(models.Event, pk=pk)
template = get_template('event_print.xml')
class EventPrint(PrintView):
model = models.Event
template_name = 'event_print.xml'
append_terms = True
merger = PdfFileMerger()
context = {
'object': object,
'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)
}
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(BytesIO(terms.read()))
merged = BytesIO()
merger.write(merged)
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
response.write(merged.getvalue())
return response
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['quote'] = True
context['filename'] = f"Event_{context['object'].display_id}_{context['object_name']}_{context['object'].start_date}.pdf"
return context
class EventArchive(generic.ListView):
@@ -210,9 +183,7 @@ class EventArchive(generic.ListView):
paginate_by = 25
def get_context_data(self, **kwargs):
# get super context
context = super(EventArchive, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['start'] = self.request.GET.get('start', None)
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
context['statuses'] = models.Event.EVENT_STATUS_CHOICES
@@ -236,37 +207,22 @@ class EventArchive(generic.ListView):
filter &= Q(start_date__gte=start)
q = self.request.GET.get('q', "")
objects = self.model.objects.all()
if q != "":
qfilter = Q(name__icontains=q) | Q(description__icontains=q) | Q(notes__icontains=q)
# try and parse an int
try:
val = int(q)
qfilter = qfilter | Q(pk=val)
except: # noqa not an integer
pass
try:
if q[0] == "N":
val = int(q[1:])
qfilter = Q(pk=val) # If string is N###### then do a simple PK filter
except: # noqa
pass
filter &= qfilter
if q:
objects = self.model.objects.search(q)
status = self.request.GET.getlist('status', "")
if len(status) > 0:
filter &= Q(status__in=status)
qs = self.model.objects.filter(filter).order_by('-start_date')
qs = objects.filter(filter).order_by('-start_date')
# Preselect related for efficiency
qs.select_related('person', 'organisation', 'venue', 'mic')
if len(qs) == 0:
if not qs.exists():
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
return qs
@@ -283,7 +239,7 @@ class EventAuthorise(generic.UpdateView):
self.template_name = self.success_template
messages.add_message(self.request, messages.SUCCESS,
'Success! Your event has been authorised. ' +
'You will also receive email confirmation to %s.' % self.object.email)
f'You will also receive email confirmation to {self.object.email}.')
return self.render_to_response(self.get_context_data())
@property
@@ -297,10 +253,10 @@ class EventAuthorise(generic.UpdateView):
return forms.InternalClientEventAuthorisationForm
def get_context_data(self, **kwargs):
context = super(EventAuthorise, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['event'] = self.event
context['tos_url'] = settings.TERMS_OF_HIRE_URL
context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name)
context['page_title'] = f"{self.event.display_id}: {self.event.name}"
if self.event.dry_hire:
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
context['preview'] = self.preview
@@ -316,10 +272,10 @@ class EventAuthorise(generic.UpdateView):
messages.add_message(self.request, messages.WARNING,
"This event has already been authorised, but the amount has changed. " +
"Please check the amount and reauthorise.")
return super(EventAuthorise, self).get(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
def get_form(self, **kwargs):
form = super(EventAuthorise, self).get_form(**kwargs)
form = super().get_form(**kwargs)
form.instance.event = self.event
form.instance.email = self.request.email
form.instance.sent_by = self.request.sent_by
@@ -335,7 +291,7 @@ class EventAuthorise(generic.UpdateView):
except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist):
raise SuspiciousOperation(
"This URL is invalid. Please ask your TEC contact for a new URL")
return super(EventAuthorise, self).dispatch(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
@@ -345,7 +301,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
@method_decorator(decorators.nottinghamtec_address_required)
def dispatch(self, *args, **kwargs):
return super(EventAuthorisationRequest, self).dispatch(*args, **kwargs)
return super().dispatch(*args, **kwargs)
@property
def object(self):
@@ -385,13 +341,13 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
context['to_name'] = event.organisation.name
msg = EmailMultiAlternatives(
"N%05d | %s - Event Authorisation Request" % (self.object.pk, self.object.name),
get_template("eventauthorisation_client_request.txt").render(context),
f"{self.object.display_id} | {self.object.name} - Event Authorisation Request",
get_template("email/eventauthorisation_client_request.txt").render(context),
to=[email],
reply_to=[self.request.user.email],
)
css = finders.find('css/email.css')
html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
html = premailer.Premailer(get_template("email/eventauthorisation_client_request.html").render(context),
external_styles=css).transform()
msg.attach_alternative(html, 'text/html')
@@ -401,18 +357,18 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
class EventAuthoriseRequestEmailPreview(generic.DetailView):
template_name = "eventauthorisation_client_request.html"
template_name = "email/eventauthorisation_client_request.html"
model = models.Event
def render_to_response(self, context, **response_kwargs):
css = finders.find('css/email.css')
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
response = super().render_to_response(context, **response_kwargs)
assert isinstance(response, HttpResponse)
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
return response
def get_context_data(self, **kwargs):
context = super(EventAuthoriseRequestEmailPreview, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['hmac'] = signing.dumps({
'pk': self.object.pk,
'email': self.request.GET.get('email', 'hello@world.test'),

View File

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

View File

@@ -1,6 +1,6 @@
from django.contrib import admin
from reversion.admin import VersionAdmin
from RIGS.admin import AssociateAdmin
from assets import models as assets
@@ -17,9 +17,13 @@ class AssetStatusAdmin(admin.ModelAdmin):
@admin.register(assets.Supplier)
class SupplierAdmin(VersionAdmin):
class SupplierAdmin(AssociateAdmin):
list_display = ['id', 'name']
ordering = ['id']
merge_fields = ['name', 'phone', 'email', 'address', 'notes']
def get_queryset(self, request):
return super(VersionAdmin, self).get_queryset(request)
@admin.register(assets.Asset)

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import random
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.db.utils import IntegrityError
from django.utils import timezone
from reversion import revisions as reversion
@@ -20,6 +21,7 @@ class Command(BaseCommand):
assets = []
def handle(self, *args, **kwargs):
print("Generating sample assets data")
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
@@ -34,6 +36,7 @@ class Command(BaseCommand):
self.create_assets()
self.create_connectors()
self.create_cables()
print("Done generating sample assets data")
def create_categories(self):
choices = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging']
@@ -102,8 +105,7 @@ class Command(BaseCommand):
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_id = get_available_asset_id(wanted_prefix=prefix)
asset = models.Asset(
asset_id=asset_id,
description=random.choice(asset_description),
@@ -123,5 +125,9 @@ class Command(BaseCommand):
if i % 3 == 0:
asset.purchased_from = random.choice(self.suppliers)
asset.clean()
asset.save()
with transaction.atomic():
try:
asset.clean()
asset.save()
except IntegrityError:
pass

View File

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

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.12 on 2022-02-14 15:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0022_alter_cabletype_unique_together'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='purchased_from',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.supplier'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-02-14 23:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0023_alter_asset_purchased_from'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='salvage_value',
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-05-26 09:22
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0024_alter_asset_salvage_value'),
]
operations = [
migrations.RenameField(
model_name='asset',
old_name='salvage_value',
new_name='replacement_cost',
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.2.12 on 2022-05-26 15:23
import assets.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0025_rename_salvage_value_asset_replacement_cost'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='purchase_price',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, validators=[assets.models.validate_positive]),
),
migrations.AlterField(
model_name='asset',
name='replacement_cost',
field=models.DecimalField(decimal_places=2, max_digits=10, null=True, validators=[assets.models.validate_positive]),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.16 on 2022-12-11 00:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0026_auto_20220526_1623'),
]
operations = [
migrations.AddField(
model_name='asset',
name='nickname',
field=models.CharField(blank=True, max_length=120),
),
]

View File

@@ -2,11 +2,13 @@ import re
from django.core.exceptions import ValidationError
from django.db import models, connection
from django.db.models import Q
from django.urls import reverse
from reversion import revisions as reversion
from reversion.models import Version
from RIGS.models import RevisionMixin, Profile
from RIGS.models import Profile, ContactableManager
from versioning.versioning import RevisionMixin
class AssetCategory(models.Model):
@@ -45,6 +47,8 @@ class Supplier(models.Model, RevisionMixin):
notes = models.TextField(blank=True, default="")
objects = ContactableManager()
class Meta:
ordering = ['name']
@@ -75,10 +79,11 @@ class CableType(models.Model):
class Meta:
ordering = ['plug', 'socket', '-circuits']
unique_together = ['plug', 'socket', 'circuits', 'cores']
def __str__(self):
if self.plug and self.socket:
return "%s%s" % (self.plug.description, self.socket.description)
return f"{self.plug.description}{self.socket.description}"
else:
return "Unknown"
@@ -86,23 +91,24 @@ class CableType(models.Model):
return reverse('cable_type_detail', kwargs={'pk': self.pk})
class AssetManager(models.Manager):
def search(self, query=None):
qs = self.get_queryset()
if query is not None:
or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query) | Q(nickname__icontains=query))
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
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()
last_asset = Asset.objects.filter(asset_id_prefix=wanted_prefix).last()
last_asset_id = last_asset.asset_id_number if last_asset else 0
return wanted_prefix + str(last_asset_id + 1)
def validate_positive(value):
if value < 0:
raise ValidationError("A price cannot be negative")
@reversion.register
@@ -114,12 +120,13 @@ class Asset(models.Model, RevisionMixin):
category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE)
status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE)
serial_number = models.CharField(max_length=150, blank=True)
purchased_from = models.ForeignKey(to=Supplier, on_delete=models.CASCADE, blank=True, null=True, related_name="assets")
purchased_from = models.ForeignKey(to=Supplier, on_delete=models.SET_NULL, blank=True, null=True, related_name="assets")
date_acquired = models.DateField()
date_sold = models.DateField(blank=True, null=True)
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
salvage_value = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
replacement_cost = models.DecimalField(null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
comments = models.TextField(blank=True)
nickname = models.CharField(max_length=120, blank=True)
# Audit
last_audited_at = models.DateTimeField(blank=True, null=True)
@@ -140,6 +147,8 @@ class Asset(models.Model, RevisionMixin):
reversion_perm = 'assets.asset_finance'
objects = AssetManager()
class Meta:
ordering = ['asset_id_prefix', 'asset_id_number']
permissions = [
@@ -147,7 +156,7 @@ class Asset(models.Model, RevisionMixin):
]
def __str__(self):
return "{} | {}".format(self.asset_id, self.description)
return f"{self.asset_id} | {self.description}"
def get_absolute_url(self):
return reverse('asset_detail', kwargs={'pk': self.asset_id})
@@ -163,12 +172,6 @@ class Asset(models.Model, RevisionMixin):
errdict["asset_id"] = [
"An Asset ID can only consist of letters and numbers, with a final number"]
if self.purchase_price and self.purchase_price < 0:
errdict["purchase_price"] = ["A price cannot be negative"]
if self.salvage_value and self.salvage_value < 0:
errdict["salvage_value"] = ["A price cannot be negative"]
if self.is_cable:
if not self.length or self.length <= 0:
errdict["length"] = ["The length of a cable must be more than 0"]
@@ -187,3 +190,7 @@ class Asset(models.Model, RevisionMixin):
@property
def display_id(self):
return str(self.asset_id)
@property
def name(self):
return f"{self.display_id} | {self.description}"

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -9,9 +9,11 @@
date = new Date();
}
$('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'));
return false;
}
function setFieldValue(ID, CSA) {
$('#' + String(ID)).val(CSA);
return false;
}
function checkIfCableHidden() {
document.getElementById("cable-table").hidden = !document.getElementById("id_is_cable").checked;
@@ -39,16 +41,16 @@
</div>
<div class="form-group form-row">
{% include 'partials/form_field.html' with field=form.date_acquired col="col-6" %}
<div class="col-sm-4">
<button class="btn btn-info" onclick="setAcquired(true);" tabindex="-1">Today</button>
<button class="btn btn-warning" onclick="setAcquired(false);" tabindex="-1">Unknown</button>
<div class="col-sm-2">
<button class="btn btn-info" onclick="return setAcquired(true);" tabindex="-1">Today</button>
<button class="btn btn-warning" onclick="return setAcquired(false);" tabindex="-1">Unknown</button>
</div>
</div>
<div class="form-group form-row">
{% include 'partials/form_field.html' with field=form.date_sold col="col-6" %}
</div>
<div class="form-group form-row">
{% include 'partials/form_field.html' with field=form.salvage_value col="col-6" prepend="£" %}
{% include 'partials/form_field.html' with field=form.replacement_cost col="col-6" prepend="£" %}
</div>
<hr>
<div class="form-group form-row">
@@ -64,16 +66,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="return setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1" type="button">5{{ form.length.help_text }}</button>
<button class="btn btn-success" onclick="return setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ form.length.help_text }}</button>
<button class="btn btn-info" onclick="return 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="return 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="return setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button>
</div>
</div>
</div>

View File

@@ -12,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(){

View File

@@ -5,11 +5,14 @@
{% block css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/easymde.min.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
{% endblock %}
{% block js %}
@@ -28,48 +31,13 @@
checkIfCableHidden();
</script>
<script>
$('#parent_id')
.selectpicker({
liveSearch: true
})
.ajaxSelectPicker({
ajax: {
url: "{% url 'asset_search_json' %}",
type: "GET",
data: function () {
let params = {
{% verbatim %}query: '{{{q}}}'{% endverbatim %}
};
return params;
}
},
locale: {
emptyTitle: 'Search for item...'
},
preprocessData: function(data){
var assets = [];
if(data.length){
var len = data.length;
for(var i = 0; i < len; i++){
var curr = data[i];
assets.push(
{
'value': curr.id,
'text': curr.label,
'disabled': false
}
);
}
assets.push(
{
'value': null,
'text': "No parent"
});
}
return assets;
},
preserveSelected: false
$('document').ready(function(){
$(document).find(".selectpicker").selectpicker().each(function(){initPicker($(this))});
});
</script>
<script>
$(document).ready(function () {
setupMDE('#id_comments');
});
</script>
{% endblock %}

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