Compare commits

...

173 Commits

Author SHA1 Message Date
David Taylor
19030fdf2f Use django-widget-tweaks from GitHub until latest version is on PyPI
See https://github.com/jazzband/django-widget-tweaks/issues/62
2018-03-25 00:48:17 +00:00
David Taylor
42450b5a22 User.is_authenticated is no longer callable 2018-03-25 00:28:37 +00:00
David Taylor
ce11df9bbc Rename MIDDLEWARE_CLASSES to MIDDLEWARE 2018-03-25 00:21:30 +00:00
David Taylor
82e664c5e0 SessionAuthenticationMiddleware is no longer required (as of Django 1.10) 2018-03-25 00:21:15 +00:00
David Taylor
8098b33698 Migrate profile to have longer last_name field (Django 2.0 updated AbstractUser model) 2018-03-25 00:20:51 +00:00
David Taylor
f4209f21dc Remove include( from admin.site.urls 2018-03-24 23:58:54 +00:00
David Taylor
1e3c021a76 Add on_delete=models.CASCADE to old migrations 2018-03-24 23:58:54 +00:00
David Taylor
d8f9256252 Add on_delete=models.CASCADE to OneToOneFields 2018-03-24 23:58:54 +00:00
David Taylor
014bab6c1f Add on_delete=models.CASCADE to all foreign keys. This replicates the previous default behaviour 2018-03-24 23:58:54 +00:00
David Taylor
8872084cab Import URL functions from django.urls 2018-03-24 23:58:39 +00:00
David Taylor
76ceb15000 Bump versions 2018-03-24 23:58:07 +00:00
David Taylor
05feb7df1c Merge pull request #325 from nottinghamtec/travis-fix
Bump chrome and chromedriver, update recaptcha keys, add --no-sandbox to chrome
2018-03-24 23:52:43 +00:00
David Taylor
ad3b38d222 Use new RECAPTCHA testing keys 2018-03-24 23:45:43 +00:00
David Taylor
a26e65073c Add no sandbox flag
See https://github.com/travis-ci/docs-travis-ci-com/pull/1671 for more info
2018-03-24 22:19:10 +00:00
David Taylor
7f3d628d01 Bump selenium 2018-03-12 18:06:49 +00:00
David Taylor
abdf785723 Bump chrome and chromedriver 2018-03-12 18:06:13 +00:00
David Taylor
85edba03a2 Merge pull request #315 from nottinghamtec/requires-io-master
[requires.io] dependency update on master branch
2017-11-29 15:03:28 +00:00
David Taylor
d61b21df0a Merge branch 'master' into requires-io-master 2017-11-29 14:56:00 +00:00
Johnathan Graydon
7e68dfb851 Fix hover text on invoice page 2017-11-29 14:40:11 +00:00
requires.io
69cbac31e7 [requires.io] dependency update 2017-11-27 09:21:59 +00:00
requires.io
ce4e0f5630 [requires.io] dependency update 2017-11-24 19:19:49 +00:00
requires.io
c99c5d573f [requires.io] dependency update 2017-11-24 02:55:03 +00:00
requires.io
221ef739b9 [requires.io] dependency update 2017-11-17 02:49:10 +00:00
requires.io
aa98039c35 [requires.io] dependency update 2017-11-15 18:16:25 +00:00
requires.io
7ef923b89f [requires.io] dependency update 2017-11-13 19:53:55 +00:00
requires.io
9ac86a6ad0 [requires.io] dependency update 2017-11-08 13:25:37 +00:00
requires.io
c0c143a166 [requires.io] dependency update 2017-11-08 13:09:23 +00:00
requires.io
0abfa2fd0c [requires.io] dependency update 2017-11-07 09:19:06 +00:00
requires.io
3333b29f24 [requires.io] dependency update 2017-11-05 19:41:14 +00:00
requires.io
880509d611 [requires.io] dependency update 2017-11-04 08:27:51 +00:00
requires.io
387f5b0d8e [requires.io] dependency update 2017-11-03 01:15:17 +00:00
requires.io
27b12d6bf4 [requires.io] dependency update 2017-11-02 02:27:15 +00:00
requires.io
c511f2f528 [requires.io] dependency update 2017-10-30 14:24:44 +00:00
requires.io
a61165f301 [requires.io] dependency update 2017-10-29 19:18:00 +00:00
requires.io
98245939fe [requires.io] dependency update 2017-10-24 21:49:12 +01:00
requires.io
b203832f79 [requires.io] dependency update 2017-10-13 18:30:28 +01:00
requires.io
c6846b85c7 [requires.io] dependency update 2017-10-05 19:35:42 +01:00
requires.io
0c15718d31 [requires.io] dependency update 2017-10-04 16:39:53 +01:00
requires.io
0f1db22452 [requires.io] dependency update 2017-10-02 16:39:04 +01:00
requires.io
e3970929e4 [requires.io] dependency update 2017-09-27 08:30:01 +01:00
requires.io
a64bf8e16f [requires.io] dependency update 2017-09-25 23:43:31 +01:00
David Taylor
cf8edc8a1c Merge pull request #314 from nottinghamtec/hotfix/ical
Fix ical end dates being wrong by 24 hours
2017-09-25 23:16:54 +01:00
David Taylor
ca9e309b1b Fix datetime/date check, and add https to calendar URLs 2017-09-25 23:11:00 +01:00
David Taylor
0ccb669019 Add failing test for ICAL end time issue 2017-09-25 22:58:36 +01:00
David Taylor
92acd17e64 Merge pull request #312 from nottinghamtec/js-dependencies
Update JS dependencies
2017-09-22 21:45:33 +01:00
David Taylor
ff83bcc14a Merge branch 'master' into js-dependencies 2017-09-22 21:36:05 +01:00
David Taylor
f17e0e199c Update bootstrap-select CSS 2017-09-22 21:30:55 +01:00
David Taylor
7b3b05677c Merge pull request #313 from nottinghamtec/requires-io-master
[requires.io] dependency update on master branch
2017-09-22 17:46:06 +01:00
requires.io
403549f449 [requires.io] dependency update 2017-09-22 17:41:17 +01:00
David Taylor
aee4ccb5a2 Update JS dependencies 2017-09-22 16:33:04 +01:00
David Taylor
f8c2551eeb Merge pull request #311 from nottinghamtec/pep8
Pep8
2017-09-22 15:57:12 +01:00
David Taylor
1bab3464b7 Merge branch 'master' into pep8 2017-09-22 15:10:40 +01:00
David Taylor
2b863b91b3 [requires.io] dependency update on master branch (#308)
* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update
2017-09-22 15:04:39 +01:00
David Taylor
5c4181a5cc Add PEP8 linting to Travis 2017-09-22 14:57:32 +01:00
David Taylor
f9389e3996 PEP8 all the things 2017-09-22 14:57:14 +01:00
David Taylor
131ff3e612 Fix chromedriver issues, and run headless on travis (#310)
* Fix chromedriver issues, and run headless on travis

* Explicitly set chrome window size

* Use travis chrome

* Try chrome beta

* Update chromedriver

* Remove virtual display from travis config
2017-09-22 14:24:20 +01:00
David Taylor
7dd7378e6c Merge pull request #307 from nottinghamtec/hotfix/migrations
Hotfix/migrations
2017-09-05 00:05:53 +01:00
David Taylor
4d0da2fdc1 Add missing migration 2017-09-04 23:59:53 +01:00
David Taylor
ddbc0699ee Add manage.py checks to travis build
This should prevent any migration fails reaching production
2017-09-04 23:59:11 +01:00
David Taylor
5f14001d01 Merge pull request #296 from nottinghamtec/python-3
Python 3
2017-09-04 23:42:26 +01:00
David Taylor
91f1e6d290 Change heroku runtime to 3.x 2017-09-04 23:19:22 +01:00
David Taylor
00d46274f1 Update travis python version 2017-09-04 23:19:22 +01:00
David Taylor
0aec836b1a Fix iCal tests 2017-09-04 23:19:22 +01:00
David Taylor
ee930aa86a Replace assertItemsEqual with assertCountEqual in model tests.
While the name is misleading, it does appear that this test does exactly the same as assertItemsEqual: https://docs.python.org/3/library/unittest.html?highlight=assertcountequal#unittest.TestCase.assertCountEqual
2017-09-04 23:19:22 +01:00
David Taylor
a88f4d0cb2 Fix paperwork printing 2017-09-04 23:19:22 +01:00
David Taylor
50c997b568 Fixed profile picture encoding 2017-09-04 23:19:22 +01:00
David Taylor
3035320e82 Run through 2-to-3 converter. This is not in a working state 2017-09-04 23:19:22 +01:00
David Taylor
98182143ec [requires.io] dependency update on master branch (#304)
* [requires.io] dependency update

* [requires.io] dependency update
2017-08-18 08:33:56 +01:00
David Taylor
bffbaeb4c6 [requires.io] dependency update (#303) 2017-08-04 19:53:29 +01:00
David Taylor
8f1c640bea [requires.io] dependency update on master branch (#301)
* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update
2017-08-01 13:52:09 +01:00
David Taylor
827a2cdd25 [requires.io] dependency update on master branch (#300)
* [requires.io] dependency update

* [requires.io] dependency update
2017-07-18 15:32:36 +01:00
David Taylor
3202981fbe [requires.io] dependency update (#299) 2017-07-10 20:01:06 +01:00
David Taylor
f0edd5020e [requires.io] dependency update on master branch (#297)
* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update

* [requires.io] dependency update
2017-07-10 19:01:11 +01:00
David Taylor
cf1c7ae725 Merge pull request #298 from nottinghamtec/telegram-build-notification
Add telegram travis notification
2017-07-02 18:24:39 +01:00
David Taylor
1ac637bfd7 Add telegram travis notification 2017-07-02 18:15:54 +01:00
David Taylor
37933650d3 Merge pull request #295 from nottinghamtec/reversion-upgrade
Reversion upgrade and versioning.py refactor
2017-07-02 18:14:51 +01:00
David Taylor
1266dd419c Use KVM based travis 2017-07-02 18:09:29 +01:00
Tom Price
74747e8700 Explicitly specify python runtime.
This follows a change to the heroku buildpack which now defaults to 3.6.1
https://devcenter.heroku.com/changelog-items/1178
2017-07-02 17:05:33 +01:00
David Taylor
34ffd62436 Add many tests for the versioning backend and interfaces 2017-06-21 03:12:26 +01:00
David Taylor
22c520e841 Refactored versioning.py (and associated templates) to improve readability and testability.
Takes inspiration from, but does not use, django-reversion-compare. We do a lot of RIGS-specific stuff that requires a lot of hacking to get working nicely with django-reversion-compare. The main example of this is event-item “many-to-one” fields. The performance difference of my code compared to django-reversion-compare was found to be negligible.
2017-06-21 03:09:46 +01:00
David Taylor
3fa9191150 Update requirements.txt 2017-06-19 23:59:19 +01:00
David Taylor
ae4f05a661 Update versioning.py to work with latest django-reversion 2017-06-19 23:53:00 +01:00
David Taylor
c178a293a1 Fix model registration and RevisionMixin lookups. Also much more efficient and DRY now 2017-06-19 23:51:56 +01:00
David Taylor
aebaa16311 Use EventAuthorisation post_save signal for EventAuthorisation email instead of the (now removed) reversion hook 2017-06-19 23:03:13 +01:00
David Taylor
175807f664 Merge pull request #294 from nottinghamtec/hotfix/spelling
Fix spelling in rig authorisation
2017-06-03 20:30:41 +01:00
Johnathan Graydon
abb56af222 Fix spelling in rig authorisation 2017-06-02 12:52:01 +01:00
David Taylor
48139b29cf Merge pull request #293 from nottinghamtec/feature/use_native_chrome
Use chrome natively on Travis, instead of sauce labs

Reduces test times to 2:30, from around 10:00
2017-05-25 14:28:08 +01:00
David Taylor
7ec09fb774 Use chrome natively on Travis, hopefully faster 2017-05-25 14:23:34 +01:00
David Taylor
e37d1c663b Merge pull request #292 from nottinghamtec/hotfix/event_authorisation_activity_feed
Add EventAuthorisation to the activity feed
2017-05-25 13:32:32 +01:00
David Taylor
bdd7f02fe2 Add EventAuthorisation to the activity feed 2017-05-25 13:09:52 +01:00
Tom Price
b4ea818992 Merge pull request #283 from nottinghamtec/feature/online-auth
Add online RIGS authorisation
2017-05-24 17:03:21 +01:00
Tom Price
7cb9e97ecb Fix text on quote paperwork for external clients
Actually finish fixing PDF footer formatting.
2017-05-24 16:34:32 +01:00
Tom Price
7fdafd854e Add payments to invoice PDF 2017-05-23 19:18:19 +01:00
Tom Price
d85ebb63a1 Minor PDF styling fixes 2017-05-23 19:04:57 +01:00
Tom Price
c6b7bbc219 Change to just using online auth for internal clients.
This effectively reverts 067e03b.
2017-05-23 18:19:02 +01:00
David Taylor
4d316c7a4a Stop authorisation information being duplicated with an event 2017-05-18 18:02:44 +01:00
David Taylor
75a3059c88 Add failing duplicate test 2017-05-18 18:02:29 +01:00
David Taylor
b6e4c0ed14 Merge branch 'master' into feature/online-auth
# Conflicts:
#	RIGS/test_functional.py
2017-05-18 17:49:54 +01:00
David Taylor
0a45b047a2 Add warnings when editing an event that has already been sent to a client 2017-05-18 17:34:49 +01:00
David Taylor
4e79f00551 Add pound signs to confirmation emails 2017-05-18 17:22:59 +01:00
David Taylor
b4ab29393e Allow confirmation emails to fail without blocking the interface 2017-05-18 16:42:13 +01:00
David Taylor
703fb8561a Move font definition into div, doesn’t seem to be picked up in body 2017-05-18 15:32:54 +01:00
David Taylor
f3c020b613 Update email templates to avoid using bootstrap. Should improve speed, and also cross-email-client compatability 2017-05-18 15:27:50 +01:00
Tom Price
4b87b0a196 Add some visual indicators that authorisations have been submitted.
This will show teccies and clients that RIGS is processing emails which can take a short while.
Should prevent duplicate sending.
2017-05-17 19:08:51 +01:00
David Taylor
7cc715cedc Merge pull request #290 from nottinghamtec/hotfix/event-times-dissapearing
Hotfix/event times dissapearing
2017-05-16 15:15:50 +01:00
David Taylor
4b032944ac Fix the time formatting 2017-05-16 14:50:33 +01:00
David Taylor
cb23fd183e Add failing test 2017-05-16 14:50:18 +01:00
David Taylor
fbc039c274 Fix tests so they can actually run locally (I failed) 2017-05-16 13:58:05 +01:00
David Taylor
b3156dbb0d Merge pull request #289 from nottinghamtec/feature/chrome-testing
Use Chrome for tests, and sauce-labs for CI integration tests
2017-05-15 21:56:09 +01:00
David Taylor
fdce2fa53d Update selenium, use chrome for tests, and use sauce-labs for CI integration tests 2017-05-15 21:46:00 +01:00
David Taylor
55d24e96cb Adds basic tests to check that versioning views load successfully
More comprehensive tests should be added when versioning.py is updated for the new version of django-reversion
2017-05-15 20:40:03 +01:00
David Taylor
36d258253f Fix issues caused by dependency upgrades 2017-05-12 21:32:17 +01:00
David Taylor
865bb131a5 Add merge migration 2017-05-12 21:02:48 +01:00
David Taylor
eb1e8935f4 Fix reversion in signals.py 2017-05-12 20:56:01 +01:00
David Taylor
f8aaf9f36e Merge branch 'master' into feature/online-auth
# Conflicts:
#	RIGS/rigboard.py
#	RIGS/test_functional.py
#	RIGS/urls.py
#	requirements.txt
2017-05-12 20:53:00 +01:00
David Taylor
7a4f5ba8bf Merge pull request #217 from nottinghamtec/python-deps
[WIP] Update Python dependencies
2017-05-12 16:54:55 +01:00
David Taylor
16a993123b Merge branch 'master' into python-deps 2017-05-12 16:49:14 +01:00
David Taylor
e547e3e858 Delete unused wercker config 2017-05-10 23:54:23 +01:00
David Taylor
ea26823fec Add dependency badge to README
Add dependency badge to README
2017-05-10 23:45:54 +01:00
David Taylor
1f4e53ad27 Add dependency badge to README 2017-05-10 23:40:48 +01:00
David Taylor
374c31e8b4 Fix date/time/datetime field types 2017-05-10 23:03:35 +01:00
David Taylor
0d726b2b60 Fix paperwork printing 2017-05-10 20:25:41 +01:00
David Taylor
38a8ac1eb4 Add failing tests for event paperwork printing 2017-05-10 20:25:14 +01:00
David Taylor
872e5e72f3 Update the requirements.txt file, always a useful thing to do 2017-05-10 20:11:12 +01:00
David Taylor
d916c1ca19 Update all the things, and fix some upgrade issues 2017-05-10 20:05:36 +01:00
David Taylor
9b1cc965c7 Update to Django 1.10 2017-05-10 18:41:17 +01:00
David Taylor
83028418fe Fix deprecation warnings for django 1.10 2017-05-10 18:30:17 +01:00
David Taylor
7f680dcffb Fix activity feed 2017-05-10 18:06:17 +01:00
David Taylor
e573088c5e Fix some issues caused by changes made over the last year 2017-05-10 17:51:55 +01:00
David Taylor
7ac9eef7a2 Merge master into python-deps
# Conflicts:
#	PyRIGS/settings.py
#	RIGS/admin.py
#	RIGS/models.py
#	RIGS/test_functional.py
#	RIGS/urls.py
#	requirements.txt
#	wercker.yml
2017-05-10 17:32:21 +01:00
David Taylor
286e4314f5 Require users to have nottinghamtec.co.uk address before allowing them to send messages to clients 2017-05-10 15:39:13 +01:00
Tom Price
6b05938953 Set authorisation button text to be more verbose 2017-05-09 19:21:35 +01:00
Tom Price
602ba1d051 Fix order of importing bootstrap variables. 2017-05-09 18:48:03 +01:00
Tom Price
1710c3f01f Send HTML confirmation emails.
Also tidy up the PDF and some of the source.
2017-05-09 18:43:27 +01:00
Tom Price
f57ac3acb1 Add sending of html email for the request 2017-05-09 18:35:29 +01:00
Tom Price
331dab20f7 Add basic tracking of when an event authorisation request was sent.
Designed and requested by Ross because he can't remember if he's push a button...
2017-04-19 18:14:36 +01:00
Tom Price
d9076a4f5f Quantize event totals to prevent issues with mixed precision on client authorisation form. 2017-04-19 15:27:12 +01:00
Tom Price
56d4e438b6 Fix test missing EventAuthorisation.sent_by 2017-04-11 16:50:49 +01:00
Tom Price
430862b24d Add tracking of who sent the link 2017-04-11 15:52:38 +01:00
Tom Price
e12367bde7 Few tweaks to printed documents following a conversation with Marilyn from treasury 2017-04-11 14:32:06 +01:00
Tom Price
c0f4884242 Add missing PO field.
Noticed in testing, that could have gone badly.
2017-04-11 14:10:00 +01:00
Tom Price
36638e4df6 Add some more disclaimers explaining things better to internal clients. 2017-04-11 14:02:58 +01:00
Tom Price
a0440e1587 Add useful email addresses for reference. 2017-04-11 13:48:51 +01:00
Tom Price
6e78f16c33 Add changes suggested by DT 2017-04-11 11:45:08 +01:00
Tom Price
82b6f1cbf8 Fix string formatting issue.
I used python 3 syntax, we aren't yet using python 3...
2017-04-10 23:43:42 +01:00
Tom Price
5be3842aea Fix test client login because we are still using Django 1.8 2017-04-10 23:28:00 +01:00
Tom Price
067e03b757 Remove Event.purchase_order in favour of a simple EventAuthorisation object. 2017-04-10 23:16:50 +01:00
Tom Price
391d9ef28f Update PDF templates and enable sending of PDF via email.
PDFs now state QUOTE, INVOICE or RECEIPT.
Single copy and all but INVOICE includes terms of hire.
2017-04-10 22:45:27 +01:00
Tom Price
5d17d642ec Update templates to include the new authorisation fields 2017-04-10 21:43:18 +01:00
Tom Price
22119a3d08 Add test for sending authorisation email to client 2017-04-10 20:50:28 +01:00
Tom Price
306c11bb2f Add tooltip JavaScript 2017-04-10 20:47:19 +01:00
Tom Price
3fa9795cde Change mic name to use the full name rather than the display name on RIGS 2017-04-10 20:40:51 +01:00
Tom Price
7fd0c50146 Add sending of emails to clients.
Add email sending methods.

Add TEC side sending of emails.
2017-04-10 20:39:19 +01:00
Tom Price
97b11eabbd Add test for sending emails.
Add backup email if there isn't an MIC
2017-04-10 19:28:35 +01:00
Tom Price
3b2aa02ae5 Add success notification emails.
Enable RevisionMixin for EventAuthorisation.

Add signal receivers for RIGS.

Expand RIGS into an explicitly defined app to support signals.
2017-04-10 19:16:45 +01:00
Tom Price
cf11e8235f Add ID tagging to auth form to autoscroll on error 2017-04-10 18:15:47 +01:00
Tom Price
1670b190c2 Add tests for the client side authorisation 2017-04-10 18:11:49 +01:00
Tom Price
e65e97b1a3 Client facing authorisation procedures.
Add forms, views, templates and URLs.

Remove created at in favour of the built in versioning as that's much more accurate.
Switch to a OneToOneField with EventAuthorisation -> event as a result of this.

Move validation from models to forms where it probably belongs.
Provide more descriptive errors.

Add success page for authorisation.
2017-04-07 02:14:33 +01:00
Tom Price
c2787d54b0 Add authorisation models.
Add EventAuthorisation model + migrations

Add authorised property to Event.

Add appropriate tests
2017-03-29 20:46:40 +01:00
Tom Price
9b7c84cf08 Change invoice filename format
Change the invoice filename format to include the event number.

Closes #279
2017-03-28 16:58:06 +01:00
David Taylor
3269e92ef2 Merge pull request #277 from nottinghamtec/davidtaylorhq-patch-1
Explicitly define height in oembed JSON
2017-03-10 15:29:29 +00:00
David Taylor
0ae7bcaf7c Explicitly define height in oembed JSON 2017-03-10 15:26:00 +00:00
Tom Price
9694d407ae Merge pull request #275 from nottinghamtec/hotfix/home-links
Make forum go to actual forum
2017-02-06 15:47:12 +00:00
Sam Osborne
82aa2785ea Make forum go to actual forum
Currently goes to old forum :(
2017-02-06 15:35:29 +00:00
Tom Price
8cfda69717 Set wercker up to use postgres for tests again.
Modify the tests to be postgres compatible by using more pointers to things rather than getting them everytime.
2016-03-31 16:03:07 +01:00
Tom Price
463c4d147c Update settings for django10 support
Allow env.EMAIL_PORT to be None without error

Change template context preprocessor in favour of the new syntax.
2016-03-31 15:14:54 +01:00
Tom Price
6da688cc9e Migrate VAT rate to use a single date field rather than datetime.
There is never any need to track the time as VAT rate hardly ever changes and will always do so at midnight. We were already assuming this anyway but it was generating loads of warnings/errors.

This will break your local VAT rate database if using sqlite, but it is tested with postgres and works fine.
2016-03-31 13:15:53 +01:00
Tom Price
c1d164bd73 Remove the db.sqlite3 file
This was loooong overdue
2016-03-31 13:12:35 +01:00
Tom Price
d43e4b2465 Update settings and urls to fix new deprecations 2016-03-31 12:33:46 +01:00
Tom Price
98ee9bb0db Fix imports in tests 2016-03-31 12:13:11 +01:00
Tom Price
cd2aed00d7 Update login URL in settings so redirects work correctly. 2016-03-31 12:09:04 +01:00
Tom Price
0ee37b1cd3 Fix issues with python2 imports 2016-03-31 12:07:07 +01:00
Tom Price
486c66b198 Update requirements.txt before starting to test for issues 2016-03-31 11:59:21 +01:00
112 changed files with 4864 additions and 1976 deletions

1
.gitignore vendored
View File

@@ -53,6 +53,7 @@ coverage.xml
# Django stuff:
*.log
db.sqlite3
# Sphinx documentation
docs/_build/

View File

@@ -1,21 +1,32 @@
language: python
python:
"2.7"
"3.6"
cache: pip
before_install:
- "export DISPLAY=:99.0"
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
addons:
chrome: stable
install:
- wget https://chromedriver.storage.googleapis.com/2.36/chromedriver_linux64.zip
- unzip chromedriver_linux64.zip
- export PATH=$PATH:$(pwd)
- chmod +x chromedriver
- pip install -r requirements.txt
- pip install coveralls codeclimate-test-reporter
- pip install coveralls codeclimate-test-reporter pep8
before_script:
- export PATH=$PATH:/usr/lib/chromium-browser/
- python manage.py collectstatic --noinput
script:
- coverage run manage.py test RIGS
- pep8 . --exclude=migrations,importer*
- python manage.py check
- python manage.py makemigrations --check --dry-run
- coverage run manage.py test --verbosity=2
after_success:
- coveralls
- codeclimate-test-reporter
notifications:
webhooks: https://fathomless-fjord-24024.herokuapp.com/notify

View File

@@ -1,8 +1,7 @@
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.urls import reverse
from RIGS import models
@@ -24,17 +23,17 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
def _checklogin(request, *args, **kwargs):
if test_func(request.user):
return view_func(request, *args, **kwargs)
elif not request.user.is_authenticated():
elif not request.user.is_authenticated:
if oembed_view is not None:
extra_context = {}
extra_context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
extra_context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
resp = render_to_response('login_redirect.html', extra_context, context_instance=RequestContext(request))
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())
resp = render(request, 'login_redirect.html', context=context)
return resp
else:
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
else:
resp = render_to_response('403.html', context_instance=RequestContext(request))
resp = render(request, '403.html')
resp.status_code = 403
return resp
_checklogin.__doc__ = view_func.__doc__
@@ -62,7 +61,7 @@ def api_key_required(function):
userid = kwargs.get('api_pk')
key = kwargs.get('api_key')
error_resp = render_to_response('403.html', context_instance=RequestContext(request))
error_resp = render(request, '403.html')
error_resp.status_code = 403
if key is None:
@@ -79,3 +78,17 @@ def api_key_required(function):
return error_resp
return function(request, *args, **kwargs)
return wrap
def nottinghamtec_address_required(function):
"""
Checks that the current user has an email address ending @nottinghamtec.co.uk
"""
def wrap(request, *args, **kwargs):
# Fail if current user's email address isn't @nottinghamtec.co.uk
if not request.user.email.endswith('@nottinghamtec.co.uk'):
error_resp = render(request, 'RIGS/eventauthorisation_request_error.html')
return error_resp
return function(request, *args, **kwargs)
return wrap

View File

@@ -1,5 +1,5 @@
from __future__ import unicode_literals
DATETIME_FORMAT = ('d/m/Y H:i')
DATE_FORMAT = ('d/m/Y')
TIME_FORMAT = ('H:i')
TIME_FORMAT = ('H:i')

View File

@@ -10,29 +10,35 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import raven
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get('SECRET_KEY') else 'gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e'
SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get(
'SECRET_KEY') else 'gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(int(os.environ.get('DEBUG'))) if os.environ.get('DEBUG') else True
STAGING = bool(int(os.environ.get('STAGING'))) if os.environ.get('STAGING') else False
TEMPLATE_DEBUG = True
STAGING = bool(int(os.environ.get('STAGING'))) if os.environ.get('STAGING') else False
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
if STAGING:
ALLOWED_HOSTS.append('.herokuapp.com')
if DEBUG:
ALLOWED_HOSTS.append('localhost')
ALLOWED_HOSTS.append('example.com')
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
if not DEBUG:
SECURE_SSL_REDIRECT = True # Redirect all http requests to https
SECURE_SSL_REDIRECT = True # Redirect all http requests to https
INTERNAL_IPS = ['127.0.0.1']
@@ -40,7 +46,6 @@ ADMINS = (
('Tom Price', 'tomtom5152@gmail.com')
)
# Application definition
INSTALLED_APPS = (
@@ -60,15 +65,15 @@ INSTALLED_APPS = (
'raven.contrib.django.raven_compat',
)
MIDDLEWARE_CLASSES = (
MIDDLEWARE = (
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
'django.middleware.security.SecurityMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
@@ -77,7 +82,6 @@ ROOT_URLCONF = 'PyRIGS.urls'
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
@@ -89,9 +93,10 @@ DATABASES = {
if not DEBUG:
import dj_database_url
DATABASES['default'] = dj_database_url.config()
# Logging
# Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
@@ -119,12 +124,12 @@ LOGGING = {
'mail_admins': {
'class': 'django.utils.log.AdminEmailHandler',
'level': 'ERROR',
# But the emails are plain text by default - HTML is nicer
# But the emails are plain text by default - HTML is nicer
'include_html': True,
},
},
'loggers': {
# Again, default Django configuration to email unhandled exceptions
# Again, default Django configuration to email unhandled exceptions
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
@@ -139,8 +144,6 @@ LOGGING = {
}
}
import raven
RAVEN_CONFIG = {
'dsn': os.environ.get('RAVEN_DSN'),
# If you are using git, you can also automatically configure the
@@ -152,14 +155,14 @@ RAVEN_CONFIG = {
AUTH_USER_MODEL = 'RIGS.Profile'
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/user/login'
LOGOUT_URL = '/user/logout'
LOGIN_URL = '/user/login/'
LOGOUT_URL = '/user/logout/'
ACCOUNT_ACTIVATION_DAYS = 7
# reCAPTCHA settings
RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', None)
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', None)
RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
NOCAPTCHA = True
# Email
@@ -167,7 +170,7 @@ EMAILER_TEST = False
if not DEBUG or EMAILER_TEST:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.environ.get('EMAIL_HOST')
EMAIL_PORT = int(os.environ.get('EMAIL_PORT'))
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 25))
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
EMAIL_USE_TLS = bool(int(os.environ.get('EMAIL_USE_TLS', 0)))
@@ -191,19 +194,7 @@ USE_L10N = True
USE_TZ = True
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M','%Y-%m-%dT%H:%M:%S')
TEMPLATE_CONTEXT_PROCESSORS = (
"django.contrib.auth.context_processors.auth",
"django.core.context_processors.debug",
"django.core.context_processors.i18n",
"django.core.context_processors.media",
"django.core.context_processors.static",
"django.core.context_processors.tz",
"django.core.context_processors.request",
"django.contrib.messages.context_processors.messages",
)
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
@@ -214,10 +205,30 @@ STATIC_DIRS = (
os.path.join(BASE_DIR, 'static/')
)
TEMPLATE_DIRS = (
os.path.join(BASE_DIR, 'templates'),
)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.debug",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.template.context_processors.request",
"django.contrib.messages.context_processors.messages",
],
'debug': DEBUG
},
},
]
USE_GRAVATAR=True
USE_GRAVATAR = True
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'

View File

@@ -1,4 +1,4 @@
from django.conf.urls import patterns, include, url
from django.conf.urls import include, url
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.conf import settings
@@ -6,19 +6,24 @@ from registration.backends.default.views import RegistrationView
import RIGS
from RIGS import regbackend
urlpatterns = patterns('',
urlpatterns = [
# Examples:
# url(r'^$', 'PyRIGS.views.home', name='home'),
# url(r'^blog/', include('blog.urls')),
url(r'^', include('RIGS.urls')),
url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail),
url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail),
name="registration_register"),
url('^user/', include('django.contrib.auth.urls')),
url('^user/', include('registration.backends.default.urls')),
url(r'^admin/', include(admin.site.urls)),
)
url(r'^admin/', admin.site.urls),
]
if settings.DEBUG:
urlpatterns += staticfiles_urlpatterns()
urlpatterns += staticfiles_urlpatterns()
import debug_toolbar
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)),
] + urlpatterns

View File

@@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "PyRIGS.settings")
from django.core.wsgi import get_wsgi_application
from dj_static import Cling
from django.core.wsgi import get_wsgi_application # noqa
from dj_static import Cling # noqa
application = Cling(get_wsgi_application())

View File

@@ -1,6 +1,8 @@
# TEC PA & Lighting - PyRIGS #
[![Build Status](https://travis-ci.org/nottinghamtec/PyRIGS.svg?branch=develop)](https://travis-ci.org/nottinghamtec/PyRIGS)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg?branch=develop)](https://coveralls.io/github/nottinghamtec/PyRIGS?branch=develop)
[![Dependency Status](https://gemnasium.com/badges/github.com/nottinghamtec/PyRIGS.svg)](https://gemnasium.com/github.com/nottinghamtec/PyRIGS)
Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails.
@@ -93,5 +95,18 @@ python manage.py generateSampleData
|keyholder|keyholder|
|basic |basic |
### Testing ###
Tests are contained in 3 files. `RIGS/test_models.py` contains tests for logic within the data models. `RIGS/test_unit.py` contains "Live server" tests, using raw web requests. `RIGS/test_integration.py` contains user interface tests which take control of a web browser. For automated Travis tests, we use [Sauce Labs](https://saucelabs.com). When debugging locally, ensure that you have the latest version of Google Chrome installed, then install [chromedriver](https://sites.google.com/a/chromium.org/chromedriver/) and ensure it is on the `PATH`.
You can run the entire test suite, or you can run specific sections individually. For example, in order of specificity:
```
python manage.py test
python manage.py test RIGS.test_models
python manage.py test RIGS.test_models.EventTestCase
python manage.py test RIGS.test_models.EventTestCase.test_current_events
```
### Committing, pushing and testing ###
Feel free to commit as you wish, on your own branch. On my branch (master for development) do not commit code that you either know doesn't work or don't know works. If you must commit this code, please make sure you say in the commit message that it isn't working, and if you can why it isn't working. If and only if you absolutely must push, then please don't leave it as the HEAD for too long, it's not much to ask but when you are done just make sure you haven't broken the HEAD for the next person.

View File

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

View File

@@ -2,7 +2,7 @@ from django.contrib import admin
from RIGS import models, forms
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import ugettext_lazy as _
import reversion
from reversion.admin import VersionAdmin
from django.contrib.admin import helpers
from django.template.response import TemplateResponse
@@ -12,10 +12,12 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count
from django.forms import ModelForm
from reversion import revisions as reversion
# Register your models here.
admin.site.register(models.VatRate, reversion.VersionAdmin)
admin.site.register(models.Event, reversion.VersionAdmin)
admin.site.register(models.EventItem, reversion.VersionAdmin)
admin.site.register(models.VatRate, VersionAdmin)
admin.site.register(models.Event, VersionAdmin)
admin.site.register(models.EventItem, VersionAdmin)
admin.site.register(models.Invoice)
admin.site.register(models.Payment)
@@ -41,7 +43,7 @@ class ProfileAdmin(UserAdmin):
add_form = forms.ProfileCreationForm
class AssociateAdmin(reversion.VersionAdmin):
class AssociateAdmin(VersionAdmin):
list_display = ('id', 'name', 'number_of_events')
search_fields = ['id', 'name']
list_display_links = ['id', 'name']
@@ -93,8 +95,7 @@ class AssociateAdmin(reversion.VersionAdmin):
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
'forms': forms
}
return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context,
current_app=self.admin_site.name)
return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context)
@admin.register(models.Person)

8
RIGS/apps.py Normal file
View File

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

View File

@@ -1,9 +1,8 @@
import cStringIO as StringIO
import datetime
import re
from django.contrib import messages
from django.core.urlresolvers import reverse_lazy
from django.urls import reverse_lazy
from django.http import Http404, HttpResponseRedirect
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
@@ -15,6 +14,9 @@ from z3c.rml import rml2pdf
from RIGS import models
from django import forms
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
class InvoiceIndex(generic.ListView):
model = models.Invoice
@@ -55,8 +57,8 @@ class InvoicePrint(generic.View):
invoice = get_object_or_404(models.Invoice, pk=pk)
object = invoice.event
template = get_template('RIGS/event_print.xml')
copies = ('TEC', 'Client')
context = RequestContext(request, {
context = {
'object': object,
'fonts': {
'opensans': {
@@ -66,10 +68,9 @@ class InvoicePrint(generic.View):
},
'invoice': invoice,
'current_user': request.user,
})
}
rml = template.render(context)
buffer = StringIO.StringIO()
buffer = rml2pdf.parseString(rml)
@@ -78,7 +79,7 @@ class InvoicePrint(generic.View):
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = "filename=Invoice %05d | %s.pdf" % (invoice.pk, escapedEventName)
response['Content-Disposition'] = "filename=Invoice %05d - N%05d | %s.pdf" % (invoice.pk, invoice.event.pk, escapedEventName)
response.write(pdfData)
return response
@@ -94,6 +95,7 @@ class InvoiceVoid(generic.View):
return HttpResponseRedirect(reverse_lazy('invoice_list'))
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
class InvoiceDelete(generic.DeleteView):
model = models.Invoice
@@ -114,6 +116,7 @@ class InvoiceDelete(generic.DeleteView):
def get_success_url(self):
return self.request.POST.get('next')
class InvoiceArchive(generic.ListView):
model = models.Invoice
template_name = 'RIGS/invoice_list_archive.html'
@@ -142,11 +145,11 @@ class InvoiceWaiting(generic.ListView):
events = self.model.objects.filter(
(
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
) & Q(invoice__isnull=True) # Has not already been invoiced
& Q(is_rig=True) # Is a rig (not non-rig)
).order_by('start_date') \
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
) & Q(invoice__isnull=True) & # Has not already been invoiced
Q(is_rig=True) # Is a rig (not non-rig)
).order_by('start_date') \
.select_related('person',
'organisation',
'venue', 'mic') \
@@ -175,7 +178,7 @@ class PaymentCreate(generic.CreateView):
def get_initial(self):
initial = super(generic.CreateView, self).get_initial()
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
if invoicepk == None:
if invoicepk is None:
raise Http404()
invoice = get_object_or_404(models.Invoice, pk=invoicepk)
initial.update({'invoice': invoice})

View File

@@ -1,4 +1,3 @@
__author__ = 'Ghost'
from django import forms
from django.utils import formats
from django.conf import settings
@@ -10,8 +9,14 @@ import simplejson
from RIGS import models
# Override the django form defaults to use the HTML date/time/datetime UI elements
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
forms.TimeField.widget = forms.TextInput(attrs={'type': 'time'})
forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'})
# Registration
class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
captcha = ReCaptchaField()
@@ -45,7 +50,7 @@ class ProfileChangeForm(UserChangeForm):
# Events Shit
class EventForm(forms.ModelForm):
datetime_input_formats = formats.get_format_lazy("DATETIME_INPUT_FORMATS") + settings.DATETIME_INPUT_FORMATS
datetime_input_formats = formats.get_format_lazy("DATETIME_INPUT_FORMATS") + list(settings.DATETIME_INPUT_FORMATS)
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
access_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
@@ -140,4 +145,32 @@ class EventForm(forms.ModelForm):
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
'collector', 'purchase_order']
'purchase_order', 'collector']
class BaseClientEventAuthorisationForm(forms.ModelForm):
tos = forms.BooleanField(required=True, label="Terms of hire")
name = forms.CharField(label="Your Name")
def clean(self):
if self.cleaned_data.get('amount') != self.instance.event.total:
self.add_error('amount', 'The amount authorised must equal the total for the event (inc VAT).')
return super(BaseClientEventAuthorisationForm, self).clean()
class Meta:
abstract = True
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
def __init__(self, **kwargs):
super(InternalClientEventAuthorisationForm, self).__init__(**kwargs)
self.fields['uni_id'].required = True
self.fields['account_code'].required = True
class Meta:
model = models.EventAuthorisation
fields = ('tos', 'name', 'amount', 'uni_id', 'account_code')
class EventAuthorisationRequestForm(forms.Form):
email = forms.EmailField(required=True, label='Authoriser Email')

View File

@@ -1,17 +1,19 @@
from RIGS import models, forms
from django_ical.views import ICalFeed
from django.db.models import Q
from django.core.urlresolvers import reverse_lazy, reverse, NoReverseMatch
from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.utils import timezone
from django.conf import settings
import datetime, pytz
import datetime
import pytz
class CalendarICS(ICalFeed):
"""
A simple event calender
"""
#Metadata which is passed on to clients
# Metadata which is passed on to clients
product_id = 'RIGS'
title = 'RIGS Calendar'
timezone = settings.TIME_ZONE
@@ -27,39 +29,39 @@ class CalendarICS(ICalFeed):
def get_object(self, request, *args, **kwargs):
params = {}
params['dry-hire'] = request.GET.get('dry-hire','true') == 'true'
params['non-rig'] = request.GET.get('non-rig','true') == 'true'
params['rig'] = request.GET.get('rig','true') == 'true'
params['dry-hire'] = request.GET.get('dry-hire', 'true') == 'true'
params['non-rig'] = request.GET.get('non-rig', 'true') == 'true'
params['rig'] = request.GET.get('rig', 'true') == 'true'
params['cancelled'] = request.GET.get('cancelled','false') == 'true'
params['provisional'] = request.GET.get('provisional','true') == 'true'
params['confirmed'] = request.GET.get('confirmed','true') == 'true'
params['cancelled'] = request.GET.get('cancelled', 'false') == 'true'
params['provisional'] = request.GET.get('provisional', 'true') == 'true'
params['confirmed'] = request.GET.get('confirmed', 'true') == 'true'
return params
def description(self,params):
def description(self, params):
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + ('Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + ('Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
return desc
def items(self, params):
#include events from up to 1 year ago
# include events from up to 1 year ago
start = datetime.datetime.now() - datetime.timedelta(days=365)
filter = Q(start_date__gte=start)
typeFilters = Q(pk=None) #Need something that is false for every entry
typeFilters = Q(pk=None) # Need something that is false for every entry
if params['dry-hire']:
typeFilters = typeFilters | Q(dry_hire=True, is_rig=True)
if params['non-rig']:
typeFilters = typeFilters | Q(is_rig=False)
if params['rig']:
typeFilters = typeFilters | Q(is_rig=True, dry_hire=False)
statusFilters = Q(pk=None) #Need something that is false for every entry
statusFilters = Q(pk=None) # Need something that is false for every entry
if params['cancelled']:
statusFilters = statusFilters | Q(status=models.Event.CANCELLED)
@@ -87,9 +89,9 @@ class CalendarICS(ICalFeed):
# Add the rig name
title += item.name
# Add the status
title += ' ('+str(item.get_status_display())+')'
title += ' (' + str(item.get_status_display()) + ')'
return title
@@ -97,12 +99,12 @@ class CalendarICS(ICalFeed):
return item.earliest_time
def item_end_datetime(self, item):
if type(item.latest_time) is datetime.date: # Ical end_datetime is non-inclusive, so add a day
if type(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):
def item_location(self, item):
return item.venue
def item_description(self, item):
@@ -111,34 +113,33 @@ class CalendarICS(ICalFeed):
tz = pytz.timezone(self.timezone)
desc = 'Rig ID = '+str(item.pk)+'\n'
desc = 'Rig ID = ' + str(item.pk) + '\n'
desc += '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 += 'Client = ' + item.person.name + ((' for ' + item.organisation.name) if item.organisation else '') + '\n'
desc += 'Status = ' + str(item.get_status_display()) + '\n'
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
desc += '\n'
if item.meet_at:
desc += 'Crew Meet = ' + (item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
if item.access_at:
desc += 'Access At = ' + (item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
if item.start_date:
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + ((' '+item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + ((' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
if item.end_date:
desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + ((' '+item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n'
desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + ((' ' + item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n'
desc += '\n'
if item.description:
desc += 'Event Description:\n'+item.description+'\n\n'
desc += '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'
# desc += 'Notes:\n'+item.notes+'\n\n'
base_url = "https://rigs.nottinghamtec.co.uk"
desc += 'URL = ' + base_url + str(item.get_absolute_url())
base_url = "http://rigs.nottinghamtec.co.uk"
desc += 'URL = '+base_url+str(item.get_absolute_url())
return desc
def item_link(self, item):
@@ -149,8 +150,8 @@ class CalendarICS(ICalFeed):
# 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
def item_updated(self, item): # some ical clients will display this
return item.last_edited_at
def item_guid(self, item): # use the rig-id as the ical unique event identifier
return item.pk
def item_guid(self, item): # use the rig-id as the ical unique event identifier
return item.pk

View File

@@ -1,12 +1,14 @@
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group, Permission
from django.db import transaction
import reversion
from reversion import revisions as reversion
import datetime
import random
from RIGS import models
class Command(BaseCommand):
help = 'Adds sample data to use for testing'
can_import_settings = True
@@ -19,32 +21,32 @@ class Command(BaseCommand):
keyholder_group = None
finance_group = None
def handle(self, *args, **options):
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
raise CommandError('You cannot run this command in production')
random.seed('Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests
random.seed('Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests
with transaction.atomic():
models.VatRate.objects.create(start_at='2014-03-05',rate=0.20,comment='test1')
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
self.setupGenericProfiles()
self.setupPeople()
self.setupOrganisations()
self.setupVenues()
self.setupGroups()
self.setupEvents()
self.setupUsefulProfiles()
def setupPeople(self):
names = ["Regulus Black","Sirius Black","Lavender Brown","Cho Chang","Vincent Crabbe","Vincent Crabbe","Bartemius Crouch","Fleur Delacour","Cedric Diggory","Alberforth Dumbledore","Albus Dumbledore","Dudley Dursley","Petunia Dursley","Vernon Dursley","Argus Filch","Seamus Finnigan","Nicolas Flamel","Cornelius Fudge","Goyle","Gregory Goyle","Hermione Granger","Rubeus Hagrid","Igor Karkaroff","Viktor Krum","Bellatrix Lestrange","Alice Longbottom","Frank Longbottom","Neville Longbottom","Luna Lovegood","Xenophilius Lovegood","Remus Lupin","Draco Malfoy","Lucius Malfoy","Narcissa Malfoy","Olympe Maxime","Minerva McGonagall","Mad-Eye Moody","Peter Pettigrew","Harry Potter","James Potter","Lily Potter","Quirinus Quirrell","Tom Riddle","Mary Riddle","Lord Voldemort","Rita Skeeter","Severus Snape","Nymphadora Tonks","Dolores Janes Umbridge","Arthur Weasley","Bill Weasley","Charlie Weasley","Fred Weasley","George Weasley","Ginny Weasley","Molly Weasley","Percy Weasley","Ron Weasley","Dobby","Fluffy","Hedwig","Moaning Myrtle","Aragog","Grawp"]
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe", "Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore", "Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan", "Nicolas Flamel", "Cornelius Fudge", "Goyle", "Gregory Goyle", "Hermione Granger", "Rubeus Hagrid", "Igor Karkaroff", "Viktor Krum", "Bellatrix Lestrange", "Alice Longbottom", "Frank Longbottom", "Neville Longbottom", "Luna Lovegood", "Xenophilius Lovegood", # noqa
"Remus Lupin", "Draco Malfoy", "Lucius Malfoy", "Narcissa Malfoy", "Olympe Maxime", "Minerva McGonagall", "Mad-Eye Moody", "Peter Pettigrew", "Harry Potter", "James Potter", "Lily Potter", "Quirinus Quirrell", "Tom Riddle", "Mary Riddle", "Lord Voldemort", "Rita Skeeter", "Severus Snape", "Nymphadora Tonks", "Dolores Janes Umbridge", "Arthur Weasley", "Bill Weasley", "Charlie Weasley", "Fred Weasley", "George Weasley", "Ginny Weasley", "Molly Weasley", "Percy Weasley", "Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
@@ -66,7 +68,8 @@ class Command(BaseCommand):
self.people.append(newPerson)
def setupOrganisations(self):
names = ["Acme, inc.","Widget Corp","123 Warehousing","Demo Company","Smith and Co.","Foo Bars","ABC Telecom","Fake Brothers","QWERTY Logistics","Demo, inc.","Sample Company","Sample, inc","Acme Corp","Allied Biscuit","Ankh-Sto Associates","Extensive Enterprise","Galaxy Corp","Globo-Chem","Mr. Sparkle","Globex Corporation","LexCorp","LuthorCorp","North Central Positronics","Omni Consimer Products","Praxis Corporation","Sombra Corporation","Sto Plains Holdings","Tessier-Ashpool","Wayne Enterprises","Wentworth Industries","ZiffCorp","Bluth Company","Strickland Propane","Thatherton Fuels","Three Waters","Water and Power","Western Gas & Electric","Mammoth Pictures","Mooby Corp","Gringotts","Thrift Bank","Flowers By Irene","The Legitimate Businessmens Club","Osato Chemicals","Transworld Consortium","Universal Export","United Fried Chicken","Virtucon","Kumatsu Motors","Keedsler Motors","Powell Motors","Industrial Automation","Sirius Cybernetics Corporation","U.S. Robotics and Mechanical Men","Colonial Movers","Corellian Engineering Corporation","Incom Corporation","General Products","Leeding Engines Ltd.","Blammo","Input, Inc.","Mainway Toys","Videlectrix","Zevo Toys","Ajax","Axis Chemical Co.","Barrytron","Carrys Candles","Cogswell Cogs","Spacely Sprockets","General Forge and Foundry","Duff Brewing Company","Dunder Mifflin","General Services Corporation","Monarch Playing Card Co.","Krustyco","Initech","Roboto Industries","Primatech","Sonky Rubber Goods","St. Anky Beer","Stay Puft Corporation","Vandelay Industries","Wernham Hogg","Gadgetron","Burleigh and Stronginthearm","BLAND Corporation","Nordyne Defense Dynamics","Petrox Oil Company","Roxxon","McMahon and Tate","Sixty Second Avenue","Charles Townsend Agency","Spade and Archer","Megadodo Publications","Rouster and Sideways","C.H. Lavatory and Sons","Globo Gym American Corp","The New Firm","SpringShield","Compuglobalhypermeganet","Data Systems","Gizmonic Institute","Initrode","Taggart Transcontinental","Atlantic Northern","Niagular","Plow King","Big Kahuna Burger","Big T Burgers and Fries","Chez Quis","Chotchkies","The Frying Dutchman","Klimpys","The Krusty Krab","Monks Diner","Milliways","Minuteman Cafe","Taco Grande","Tip Top Cafe","Moes Tavern","Central Perk","Chasers"]
names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
"Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
@@ -90,7 +93,8 @@ class Command(BaseCommand):
self.organisations.append(newOrganisation)
def setupVenues(self):
names = ["Bear Island","Crossroads Inn","Deepwood Motte","The Dreadfort","The Eyrie","Greywater Watch","The Iron Islands","Karhold","Moat Cailin","Oldstones","Raventree Hall","Riverlands","The Ruby Ford","Saltpans","Seagard","Torrhen's Square","The Trident","The Twins","The Vale of Arryn","The Whispering Wood","White Harbor","Winterfell","The Arbor","Ashemark","Brightwater Keep","Casterly Rock","Clegane's Keep","Dragonstone","Dorne","God's Eye","The Golden Tooth","Harrenhal","Highgarden","Horn Hill","Fingers","King's Landing","Lannisport","Oldtown","Rainswood","Storm's End","Summerhall","Sunspear","Tarth","Castle Black","Craster's Keep","Fist of the First Men","The Frostfangs","The Gift","The Skirling Pass","The Wall","Asshai","Astapor","Braavos","The Dothraki Sea","Lys","Meereen","Myr","Norvos","Pentos","Qarth","Qohor","The Red Waste","Tyrosh","Vaes Dothrak","Valyria","Village of the Lhazareen","Volantis","Yunkai"]
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch", "The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands", "The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins", "The Vale of Arryn", "The Whispering Wood", "White Harbor", "Winterfell", "The Arbor", "Ashemark", "Brightwater Keep", "Casterly Rock", "Clegane's Keep", "Dragonstone", "Dorne", "God's Eye", "The Golden Tooth", # noqa
"Harrenhal", "Highgarden", "Horn Hill", "Fingers", "King's Landing", "Lannisport", "Oldtown", "Rainswood", "Storm's End", "Summerhall", "Sunspear", "Tarth", "Castle Black", "Craster's Keep", "Fist of the First Men", "The Frostfangs", "The Gift", "The Skirling Pass", "The Wall", "Asshai", "Astapor", "Braavos", "The Dothraki Sea", "Lys", "Meereen", "Myr", "Norvos", "Pentos", "Qarth", "Qohor", "The Red Waste", "Tyrosh", "Vaes Dothrak", "Valyria", "Village of the Lhazareen", "Volantis", "Yunkai"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
@@ -117,8 +121,8 @@ class Command(BaseCommand):
self.keyholder_group = Group.objects.create(name='Keyholders')
self.finance_group = Group.objects.create(name='Finance')
keyholderPerms = ["add_event","change_event","view_event","add_eventitem","change_eventitem","delete_eventitem","add_organisation","change_organisation","view_organisation","add_person","change_person","view_person","view_profile","add_venue","change_venue","view_venue"]
financePerms = ["change_event","view_event","add_eventitem","change_eventitem","add_invoice","change_invoice","view_invoice","add_organisation","change_organisation","view_organisation","add_payment","change_payment","delete_payment","add_person","change_person","view_person"]
keyholderPerms = ["add_event", "change_event", "view_event", "add_eventitem", "change_eventitem", "delete_eventitem", "add_organisation", "change_organisation", "view_organisation", "add_person", "change_person", "view_person", "view_profile", "add_venue", "change_venue", "view_venue"]
financePerms = ["change_event", "view_event", "add_eventitem", "change_eventitem", "add_invoice", "change_invoice", "view_invoice", "add_organisation", "change_organisation", "view_organisation", "add_payment", "change_payment", "delete_payment", "add_person", "change_person", "view_person"]
for permId in keyholderPerms:
self.keyholder_group.permissions.add(Permission.objects.get(codename=permId))
@@ -127,11 +131,11 @@ class Command(BaseCommand):
self.finance_group.permissions.add(Permission.objects.get(codename=permId))
def setupGenericProfiles(self):
names = ["Clara Oswin Oswald","Rory Williams","Amy Pond","River Song","Martha Jones","Donna Noble","Jack Harkness","Mickey Smith","Rose Tyler"]
names = ["Clara Oswin Oswald", "Rory Williams", "Amy Pond", "River Song", "Martha Jones", "Donna Noble", "Jack Harkness", "Mickey Smith", "Rose Tyler"]
for i, name in enumerate(names):
newProfile = models.Profile.objects.create(username=name.replace(" ",""), first_name=name.split(" ")[0], last_name=name.split(" ")[-1],
email=name.replace(" ","")+"@example.com",
initials="".join([ j[0].upper() for j in name.split() ]))
newProfile = models.Profile.objects.create(username=name.replace(" ", ""), first_name=name.split(" ")[0], last_name=name.split(" ")[-1],
email=name.replace(" ", "") + "@example.com",
initials="".join([j[0].upper() for j in name.split()]))
if i % 2 == 0:
newProfile.phone = "01234 567894"
@@ -139,110 +143,110 @@ class Command(BaseCommand):
self.profiles.append(newProfile)
def setupUsefulProfiles(self):
superUser = models.Profile.objects.create(username="superuser", first_name="Super", last_name="User", initials="SU",
email="superuser@example.com", is_superuser=True, is_active=True, is_staff=True)
superUser = models.Profile.objects.create(username="superuser", first_name="Super", last_name="User", initials="SU",
email="superuser@example.com", is_superuser=True, is_active=True, is_staff=True)
superUser.set_password('superuser')
superUser.save()
financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User", initials="FU",
email="financeuser@example.com", is_active=True)
financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User", initials="FU",
email="financeuser@example.com", is_active=True)
financeUser.groups.add(self.finance_group)
financeUser.groups.add(self.keyholder_group)
financeUser.set_password('finance')
financeUser.save()
keyholderUser = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User", initials="KU",
email="keyholderuser@example.com", is_active=True)
keyholderUser = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User", initials="KU",
email="keyholderuser@example.com", is_active=True)
keyholderUser.groups.add(self.keyholder_group)
keyholderUser.set_password('keyholder')
keyholderUser.save()
basicUser = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User", initials="BU",
email="basicuser@example.com", is_active=True)
basicUser = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User", initials="BU",
email="basicuser@example.com", is_active=True)
basicUser.set_password('basic')
basicUser.save()
def setupEvents(self):
names = ["Outdoor Concert","Hall Open Mic Night","Festival","Weekend Event","Magic Show","Society Ball","Evening Show","Talent Show","Acoustic Evening","Hire of Things","SU Event","End of Term Show","Theatre Show","Outdoor Fun Day","Summer Carnival","Open Days","Magic Show","Awards Ceremony","Debating Event","Club Night","DJ Evening","Building Projection","Choir Concert"]
descriptions = ["A brief desciption of the event","This event is boring","Probably wont happen","Warning: this has lots of kit"]
notes = ["The client came into the office at some point","Who knows if this will happen", "Probably should check this event", "Maybe not happening", "Run away!"]
names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball", "Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show", "Awards Ceremony", "Debating Event", "Club Night", "DJ Evening", "Building Projection", "Choir Concert"]
descriptions = ["A brief desciption of the event", "This event is boring", "Probably wont happen", "Warning: this has lots of kit"]
notes = ["The client came into the office at some point", "Who knows if this will happen", "Probably should check this event", "Maybe not happening", "Run away!"]
itemOptions = [{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2, 'cost': 200.00},
{'name': 'Projector', 'description': 'Some kind of video thinamejig, probably with unnecessary processing for free', 'quantity': 1, 'cost': 500.00},
{'name': 'Lighting Desk', 'description': 'Cannot provide guarentee that it will work', 'quantity': 1, 'cost': 200.52},
{'name': 'Moving lights', 'description': 'Flashy lights, with the copper', 'quantity': 8, 'cost': 50.00},
{'name': 'Microphones', 'description': 'Make loud noise \n you will want speakers with this', 'quantity': 5, 'cost': 0.50},
{'name': 'Sound Mixer Thing', 'description': 'Might be analogue, might be digital', 'quantity': 1, 'cost': 100.00},
{'name': 'Electricity', 'description': 'You need this', 'quantity': 1, 'cost': 200.00},
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
{'name': 'Projector', 'description': 'Some kind of video thinamejig, probably with unnecessary processing for free', 'quantity': 1, 'cost': 500.00},
{'name': 'Lighting Desk', 'description': 'Cannot provide guarentee that it will work', 'quantity': 1, 'cost': 200.52},
{'name': 'Moving lights', 'description': 'Flashy lights, with the copper', 'quantity': 8, 'cost': 50.00},
{'name': 'Microphones', 'description': 'Make loud noise \n you will want speakers with this', 'quantity': 5, 'cost': 0.50},
{'name': 'Sound Mixer Thing', 'description': 'Might be analogue, might be digital', 'quantity': 1, 'cost': 100.00},
{'name': 'Electricity', 'description': 'You need this', 'quantity': 1, 'cost': 200.00},
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
dayDelta = -120 # start adding events from 4 months ago
dayDelta = -120 # start adding events from 4 months ago
for i in range(150): # Let's add 100 events
for i in range(150): # Let's add 100 events
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
name = names[i%len(names)]
name = names[i % len(names)]
startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
dayDelta = dayDelta + random.randint(0,3)
dayDelta = dayDelta + random.randint(0, 3)
newEvent = models.Event.objects.create(name=name, start_date=startDate)
if random.randint(0,2) > 1: # 1 in 3 have a start time
newEvent.start_time = datetime.time(random.randint(15,20))
if random.randint(0,2) > 1: # of those, 1 in 3 have an end time on the same day
newEvent.end_time = datetime.time(random.randint(21,23))
elif random.randint(0,1)>0: # half of the others finish early the next day
if random.randint(0, 2) > 1: # 1 in 3 have a start time
newEvent.start_time = datetime.time(random.randint(15, 20))
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
newEvent.end_time = datetime.time(random.randint(21, 23))
elif random.randint(0, 1) > 0: # half of the others finish early the next day
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=1)
newEvent.end_time = datetime.time(random.randint(0,5))
elif random.randint(0,2)>1: # 1 in 3 of the others finish a few days ahead
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1,4))
newEvent.end_time = datetime.time(random.randint(0, 5))
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1, 4))
if random.randint(0,6) > 0: # 5 in 6 have MIC
if random.randint(0, 6) > 0: # 5 in 6 have MIC
newEvent.mic = random.choice(self.profiles)
if random.randint(0,6) > 0: # 5 in 6 have organisation
if random.randint(0, 6) > 0: # 5 in 6 have organisation
newEvent.organisation = random.choice(self.organisations)
if random.randint(0,6) > 0: # 5 in 6 have person
if random.randint(0, 6) > 0: # 5 in 6 have person
newEvent.person = random.choice(self.people)
if random.randint(0,6) > 0: # 5 in 6 have venue
if random.randint(0, 6) > 0: # 5 in 6 have venue
newEvent.venue = random.choice(self.venues)
# Could have any status, equally weighted
newEvent.status = random.choice([models.Event.BOOKED,models.Event.CONFIRMED,models.Event.PROVISIONAL, models.Event.CANCELLED])
newEvent.status = random.choice([models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
newEvent.dry_hire = (random.randint(0,7)==0) # 1 in 7 are dry hire
newEvent.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
if random.randint(0,1) > 0: # 1 in 2 have description
if random.randint(0, 1) > 0: # 1 in 2 have description
newEvent.description = random.choice(descriptions)
if random.randint(0,1) > 0: # 1 in 2 have notes
if random.randint(0, 1) > 0: # 1 in 2 have notes
newEvent.notes = random.choice(notes)
newEvent.save()
# Now add some items
for j in range(random.randint(1,5)):
itemData = itemOptions[random.randint(0,len(itemOptions)-1)]
for j in range(random.randint(1, 5)):
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
newItem.save()
while newEvent.sum_total < 0:
itemData = itemOptions[random.randint(0,len(itemOptions)-1)]
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
newItem.save()
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
if newEvent.start_date < datetime.date.today(): # think about adding an invoice
if random.randint(0,2) > 0: # 2 in 3 have had paperwork sent to treasury
if newEvent.start_date < datetime.date.today(): # think about adding an invoice
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
newInvoice = models.Invoice.objects.create(event=newEvent)
if newEvent.status is models.Event.CANCELLED: # void cancelled events
if newEvent.status is models.Event.CANCELLED: # void cancelled events
newInvoice.void = True
elif random.randint(0,2)>1: # 1 in 3 have been paid
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance, date=datetime.date.today())

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.core.validators

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('postedAt', models.DateTimeField(auto_now=True)),
('message', models.TextField()),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
],
options={
},

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import RIGS.models

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import RIGS.models

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
@@ -33,11 +33,11 @@ class Migration(migrations.Migration):
('payment_method', models.CharField(blank=True, null=True, max_length=255)),
('payment_received', models.CharField(blank=True, null=True, max_length=255)),
('purchase_order', models.CharField(blank=True, null=True, max_length=255)),
('based_on', models.ForeignKey(to='RIGS.Event', related_name='future_events')),
('checked_in_by', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in')),
('mic', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic')),
('organisation', models.ForeignKey(to='RIGS.Organisation')),
('person', models.ForeignKey(to='RIGS.Person')),
('based_on', models.ForeignKey(to='RIGS.Event', related_name='future_events', on_delete=models.CASCADE)),
('checked_in_by', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in', on_delete=models.CASCADE)),
('mic', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', on_delete=models.CASCADE)),
('organisation', models.ForeignKey(to='RIGS.Organisation', on_delete=models.CASCADE)),
('person', models.ForeignKey(to='RIGS.Person', on_delete=models.CASCADE)),
],
options={
},
@@ -52,7 +52,7 @@ class Migration(migrations.Migration):
('quantity', models.IntegerField()),
('cost', models.DecimalField(max_digits=10, decimal_places=2)),
('order', models.IntegerField()),
('event', models.ForeignKey(to='RIGS.Event', related_name='item')),
('event', models.ForeignKey(to='RIGS.Event', related_name='item', on_delete=models.CASCADE)),
],
options={
},
@@ -75,7 +75,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='event',
name='venue',
field=models.ForeignKey(to='RIGS.Venue'),
field=models.ForeignKey(to='RIGS.Venue', on_delete=models.CASCADE),
preserve_default=True,
),
]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
@@ -14,26 +14,26 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='based_on',
field=models.ForeignKey(to='RIGS.Event', related_name='future_events', blank=True, null=True),
field=models.ForeignKey(to='RIGS.Event', related_name='future_events', blank=True, null=True, on_delete=models.CASCADE),
preserve_default=True,
),
migrations.AlterField(
model_name='event',
name='checked_in_by',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True,
null=True),
null=True, on_delete=models.CASCADE),
preserve_default=True,
),
migrations.AlterField(
model_name='event',
name='mic',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True),
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True, on_delete=models.CASCADE),
preserve_default=True,
),
migrations.AlterField(
model_name='event',
name='organisation',
field=models.ForeignKey(to='RIGS.Organisation', blank=True, null=True),
field=models.ForeignKey(to='RIGS.Organisation', blank=True, null=True, on_delete=models.CASCADE),
preserve_default=True,
),
migrations.AlterField(

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
@@ -19,8 +19,8 @@ class Migration(migrations.Migration):
('run', models.BooleanField(default=False)),
('derig', models.BooleanField(default=False)),
('notes', models.TextField(blank=True, null=True)),
('event', models.ForeignKey(related_name='crew', to='RIGS.Event')),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
('event', models.ForeignKey(related_name='crew', to='RIGS.Event', on_delete=models.CASCADE)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
],
options={
},
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='eventitem',
name='event',
field=models.ForeignKey(related_name='items', to='RIGS.Event'),
field=models.ForeignKey(related_name='items', to='RIGS.Event', on_delete=models.CASCADE),
preserve_default=True,
),
]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
@@ -14,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='person',
field=models.ForeignKey(blank=True, null=True, to='RIGS.Person'),
field=models.ForeignKey(blank=True, null=True, to='RIGS.Person', on_delete=models.CASCADE),
preserve_default=True,
),
]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
@@ -14,13 +14,13 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='venue',
field=models.ForeignKey(blank=True, to='RIGS.Venue', null=True),
field=models.ForeignKey(blank=True, to='RIGS.Venue', null=True, on_delete=models.CASCADE),
preserve_default=True,
),
migrations.AlterField(
model_name='eventitem',
name='event',
field=models.ForeignKey(related_name='items', blank=True, to='RIGS.Event'),
field=models.ForeignKey(related_name='items', blank=True, to='RIGS.Event', on_delete=models.CASCADE),
preserve_default=True,
),
]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('invoice_date', models.DateField(auto_now_add=True)),
('void', models.BooleanField()),
('event', models.OneToOneField(to='RIGS.Event')),
('event', models.OneToOneField(to='RIGS.Event', on_delete=models.CASCADE)),
],
options={
},
@@ -31,7 +31,7 @@ class Migration(migrations.Migration):
('date', models.DateField()),
('amount', models.DecimalField(help_text=b'Please use ex. VAT', max_digits=10, decimal_places=2)),
('method', models.CharField(max_length=2, choices=[(b'C', b'Cash'), (b'I', b'Internal'), (b'E', b'External'), (b'SU', b'SU Core'), (b'M', b'Members')])),
('invoice', models.ForeignKey(to='RIGS.Invoice')),
('invoice', models.ForeignKey(to='RIGS.Invoice', on_delete=models.CASCADE)),
],
options={
},
@@ -40,7 +40,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='mic',
field=models.ForeignKey(related_name='event_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True),
field=models.ForeignKey(related_name='event_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE),
preserve_default=True,
),
]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.core.validators

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.db.models.deletion

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2016-03-31 12:02
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0024_auto_20160229_2042'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='username',
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username'),
),
migrations.AlterField(
model_name='vatrate',
name='start_at',
field=models.DateField(),
),
]

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0024_auto_20160229_2042'),
]
operations = [
migrations.CreateModel(
name='EventAuthorisation',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('email', models.EmailField(max_length=254)),
('name', models.CharField(max_length=255)),
('uni_id', models.CharField(max_length=10, null=True, verbose_name=b'University ID', blank=True)),
('account_code', models.CharField(max_length=50, null=True, blank=True)),
('amount', models.DecimalField(verbose_name=b'authorisation amount', max_digits=10, decimal_places=2)),
('created_at', models.DateTimeField(auto_now_add=True)),
('event', models.ForeignKey(related_name='authroisations', to='RIGS.Event', on_delete=models.CASCADE)),
],
),
]

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-10 17:46
import django.contrib.auth.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0025_auto_20160331_1302'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='username',
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator()], verbose_name='username'),
),
]

View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0025_eventauthorisation'),
]
operations = [
migrations.RemoveField(
model_name='eventauthorisation',
name='created_at',
),
]

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0026_remove_eventauthorisation_created_at'),
]
operations = [
migrations.AlterField(
model_name='eventauthorisation',
name='event',
field=models.OneToOneField(related_name='authorisation', to='RIGS.Event', on_delete=models.CASCADE),
),
]

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0027_eventauthorisation_event_singular'),
]
operations = [
migrations.AddField(
model_name='eventauthorisation',
name='sent_by',
field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
preserve_default=False,
),
]

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0029_eventauthorisation_sent_by'),
]
operations = [
migrations.AddField(
model_name='event',
name='auth_request_at',
field=models.DateTimeField(null=True, blank=True),
),
migrations.AddField(
model_name='event',
name='auth_request_by',
field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE),
),
migrations.AddField(
model_name='event',
name='auth_request_to',
field=models.EmailField(max_length=254, null=True, blank=True),
),
]

View File

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-12 20:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0030_auth_request_sending'),
('RIGS', '0026_auto_20170510_1846'),
]
operations = [
]

View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-04 22:55
from __future__ import unicode_literals
from django.conf import settings
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('reversion', '0001_squashed_0004_auto_20160611_1202'),
('RIGS', '0031_merge_20170512_2102'),
]
operations = [
migrations.CreateModel(
name='RIGSVersion',
fields=[
],
options={
'indexes': [],
'proxy': True,
},
bases=('reversion.version',),
),
migrations.AlterField(
model_name='event',
name='collector',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='collected by'),
),
migrations.AlterField(
model_name='event',
name='mic',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='event_mic', to=settings.AUTH_USER_MODEL, verbose_name='MIC'),
),
migrations.AlterField(
model_name='event',
name='purchase_order',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='PO'),
),
migrations.AlterField(
model_name='eventauthorisation',
name='amount',
field=models.DecimalField(decimal_places=2, max_digits=10, verbose_name='authorisation amount'),
),
migrations.AlterField(
model_name='eventauthorisation',
name='uni_id',
field=models.CharField(blank=True, max_length=10, null=True, verbose_name='University ID'),
),
migrations.AlterField(
model_name='payment',
name='amount',
field=models.DecimalField(decimal_places=2, help_text='Please use ex. VAT', max_digits=10),
),
migrations.AlterField(
model_name='payment',
name='method',
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('SU', 'SU Core'), ('T', 'TEC Adjustment')], max_length=2, null=True),
),
migrations.AlterField(
model_name='profile',
name='username',
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.0.3 on 2018-03-25 00:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0032_auto_20170904_2355'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='last_name',
field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
),
]

View File

@@ -1,19 +1,24 @@
import datetime
import hashlib
import datetime
import pytz
import random
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.conf import settings
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.encoding import python_2_unicode_compatible
from reversion import revisions as reversion
from reversion.models import Version
import string
import random
from collections import Counter
from decimal import Decimal
import reversion
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse_lazy
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
from django.urls import reverse_lazy
# Create your models here.
@@ -28,13 +33,13 @@ class Profile(AbstractUser):
size = 20
chars = string.ascii_letters + string.digits
new_api_key = ''.join(random.choice(chars) for x in range(size))
return new_api_key;
return new_api_key
@property
def profile_picture(self):
url = ""
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email).hexdigest() + "?d=wavatar&s=500"
url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email.encode('utf-8')).hexdigest() + "?d=wavatar&s=500"
return url
@property
@@ -58,32 +63,31 @@ class Profile(AbstractUser):
class RevisionMixin(object):
@property
def current_version(self):
version = Version.objects.get_for_object(self).select_related('revision').first()
return version
@property
def last_edited_at(self):
versions = reversion.get_for_object(self)
if versions:
version = reversion.get_for_object(self)[0]
return version.revision.date_created
else:
version = self.current_version
if version is None:
return None
return version.revision.date_created
@property
def last_edited_by(self):
versions = reversion.get_for_object(self)
if versions:
version = reversion.get_for_object(self)[0]
return version.revision.user
else:
version = self.current_version
if version is None:
return None
return version.revision.user
@property
def current_version_id(self):
versions = reversion.get_for_object(self)
if versions:
version = reversion.get_for_object(self)[0]
return "V{0} | R{1}".format(version.pk, version.revision.pk)
else:
version = self.current_version
if version is None:
return None
return "V{0} | R{1}".format(version.pk, version.revision.pk)
@reversion.register
@@ -175,7 +179,7 @@ class Organisation(models.Model, RevisionMixin):
class VatManager(models.Manager):
def current_rate(self):
return self.find_rate(datetime.datetime.now())
return self.find_rate(timezone.now())
def find_rate(self, date):
# return self.filter(startAt__lte=date).latest()
@@ -190,7 +194,7 @@ class VatManager(models.Manager):
@reversion.register
@python_2_unicode_compatible
class VatRate(models.Model, RevisionMixin):
start_at = models.DateTimeField()
start_at = models.DateField()
rate = models.DecimalField(max_digits=6, decimal_places=6)
comment = models.CharField(max_length=255)
@@ -241,18 +245,12 @@ class Venue(models.Model, RevisionMixin):
class EventManager(models.Manager):
def current_events(self):
events = self.filter(
(models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(
status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q(
status=Event.CANCELLED)) | # Active dry hire
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
models.Q(status=Event.CANCELLED, start_date__gte=datetime.date.today()) # Canceled but not started
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
'organisation',
'venue', 'mic')
(models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=timezone.now().date()) & ~models.Q(status=Event.CANCELLED)) | # Active dry hire
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
models.Q(status=Event.CANCELLED, start_date__gte=timezone.now().date()) # Canceled but not started
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
return events
def events_in_bounds(self, start, end):
@@ -275,12 +273,12 @@ class EventManager(models.Manager):
def rig_count(self):
event_count = self.filter(
(models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False,
(models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False,
is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=datetime.date.today(), dry_hire=False, is_rig=True) & ~models.Q(
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False, is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=datetime.date.today(), is_rig=True) & ~models.Q(
(models.Q(dry_hire=True, start_date__gte=timezone.now().date(), is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Active dry hire
(models.Q(dry_hire=True, checked_in_by__isnull=True, is_rig=True) & (
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) # Active dry hire GT
@@ -304,9 +302,9 @@ class Event(models.Model, RevisionMixin):
)
name = models.CharField(max_length=255)
person = models.ForeignKey('Person', null=True, blank=True)
organisation = models.ForeignKey('Organisation', blank=True, null=True)
venue = models.ForeignKey('Venue', blank=True, null=True)
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
description = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, null=True)
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
@@ -325,9 +323,9 @@ class Event(models.Model, RevisionMixin):
meet_info = models.CharField(max_length=255, blank=True, null=True)
# Crew management
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True)
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True, on_delete=models.CASCADE)
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
verbose_name="MIC")
verbose_name="MIC", on_delete=models.CASCADE)
# Monies
payment_method = models.CharField(max_length=255, blank=True, null=True)
@@ -335,6 +333,11 @@ class Event(models.Model, RevisionMixin):
purchase_order = models.CharField(max_length=255, blank=True, null=True, verbose_name='PO')
collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='collected by')
# Authorisation request details
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
auth_request_at = models.DateTimeField(null=True, blank=True)
auth_request_to = models.EmailField(null=True, blank=True)
# Calculated values
"""
EX Vat
@@ -367,7 +370,7 @@ class Event(models.Model, RevisionMixin):
@property
def vat(self):
return self.sum_total * self.vat_rate.rate
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
"""
Inc VAT
@@ -375,7 +378,7 @@ class Event(models.Model, RevisionMixin):
@property
def total(self):
return self.sum_total + self.vat
return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
@property
def cancelled(self):
@@ -385,6 +388,10 @@ class Event(models.Model, RevisionMixin):
def confirmed(self):
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
@property
def authorised(self):
return not self.internal and self.purchase_order or self.authorisation.amount == self.total
@property
def has_start_time(self):
return self.start_time is not None
@@ -445,13 +452,17 @@ class Event(models.Model, RevisionMixin):
else:
return endDate
@property
def internal(self):
return self.organisation and self.organisation.union_account
objects = EventManager()
def get_absolute_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.pk})
def __str__(self):
return unicode(self.pk) + ": " + self.name
return str(self.pk) + ": " + self.name
def clean(self):
if self.end_date and self.start_date > self.end_date:
@@ -474,7 +485,7 @@ class Event(models.Model, RevisionMixin):
class EventItem(models.Model):
event = models.ForeignKey('Event', related_name='items', blank=True)
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
quantity = models.IntegerField()
@@ -493,17 +504,35 @@ class EventItem(models.Model):
class EventCrew(models.Model):
event = models.ForeignKey('Event', related_name='crew')
user = models.ForeignKey(settings.AUTH_USER_MODEL)
event = models.ForeignKey('Event', related_name='crew', on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
rig = models.BooleanField(default=False)
run = models.BooleanField(default=False)
derig = models.BooleanField(default=False)
notes = models.TextField(blank=True, null=True)
@reversion.register
class EventAuthorisation(models.Model, RevisionMixin):
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
email = models.EmailField()
name = models.CharField(max_length=255)
uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID")
account_code = models.CharField(max_length=50, blank=True, null=True)
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
sent_by = models.ForeignKey('RIGS.Profile', on_delete=models.CASCADE)
def get_absolute_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
@property
def activity_feed_string(self):
return str("N%05d" % self.event.pk + ' (requested by ' + self.sent_by.initials + ')')
@python_2_unicode_compatible
class Invoice(models.Model):
event = models.OneToOneField('Event')
event = models.OneToOneField('Event', on_delete=models.CASCADE)
invoice_date = models.DateField(auto_now_add=True)
void = models.BooleanField(default=False)
@@ -555,7 +584,7 @@ class Payment(models.Model):
(ADJUSTMENT, 'TEC Adjustment'),
)
invoice = models.ForeignKey('Invoice')
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
date = models.DateField()
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True)

View File

@@ -1,13 +1,15 @@
from RIGS.models import Profile
from RIGS.forms import ProfileRegistrationFormUniqueEmail
from registration.signals import user_registered
def user_created(sender, user, request, **kwargs):
form = ProfileRegistrationFormUniqueEmail(request.POST)
user.first_name = form.data['first_name']
user.last_name = form.data['last_name']
user.initials = form.data['initials']
user.phone = form.data['phone']
user.save()
form = ProfileRegistrationFormUniqueEmail(request.POST)
user.first_name = form.data['first_name']
user.last_name = form.data['last_name']
user.initials = form.data['initials']
user.phone = form.data['phone']
user.save()
from registration.signals import user_registered
user_registered.connect(user_created)
user_registered.connect(user_created)

View File

@@ -1,23 +1,30 @@
import os
import cStringIO as StringIO
from io import BytesIO
import urllib2
import urllib.request
import urllib.error
import urllib.parse
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.views import generic
from django.core.urlresolvers import reverse_lazy
from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.template.loader import get_template
from django.conf import settings
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core import signing
from django.http import HttpResponse
from django.core.exceptions import SuspiciousOperation
from django.db.models import Q
from django.contrib import messages
from django.utils.decorators import method_decorator
from z3c.rml import rml2pdf
from PyPDF2 import PdfFileMerger, PdfFileReader
import simplejson
import premailer
from RIGS import models, forms
from PyRIGS import decorators
import datetime
import re
import copy
@@ -36,15 +43,17 @@ class RigboardIndex(generic.TemplateView):
context['events'] = models.Event.objects.current_events()
return context
class WebCalendar(generic.TemplateView):
template_name = 'RIGS/calendar.html'
def get_context_data(self, **kwargs):
context = super(WebCalendar, self).get_context_data(**kwargs)
context['view'] = kwargs.get('view','')
context['date'] = kwargs.get('date','')
context['view'] = kwargs.get('view', '')
context['date'] = kwargs.get('date', '')
return context
class EventDetail(generic.DetailView):
model = models.Event
@@ -53,7 +62,6 @@ class EventOembed(generic.View):
model = models.Event
def get(self, request, pk=None):
embed_url = reverse('event_embed', args=[pk])
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
@@ -61,6 +69,7 @@ class EventOembed(generic.View):
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
'version': '1.0',
'type': 'rich',
'height': '250'
}
json = simplejson.JSONEncoderForHTML().encode(data)
@@ -84,9 +93,8 @@ class EventCreate(generic.CreateView):
if re.search('"-\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.iteritems():
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)
@@ -107,24 +115,39 @@ class EventUpdate(generic.UpdateView):
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.iteritems():
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)
# If this event has already been emailed to a client, show a warning
if self.object.auth_request_at is not None:
messages.info(self.request, 'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.')
if hasattr(self.object, 'authorised'):
messages.warning(self.request, 'This event has already been authorised by client, any changes to price will require reauthorisation.')
return context
def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
class EventDuplicate(EventUpdate):
def get_object(self, queryset=None):
old = super(EventDuplicate, self).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
old = super(EventDuplicate, self).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
if self.request.method in ('POST', 'PUT'): # This only happens on save (otherwise items won't display in editor)
new.pk = None # This means a new event will be created on save, and all items will be re-created
# Remove all the authorisation information from the new event
new.auth_request_to = None
new.auth_request_by = None
new.auth_request_at = None
if self.request.method in (
'POST', 'PUT'): # This only happens on save (otherwise items won't display in editor)
new.pk = None # This means a new event will be created on save, and all items will be re-created
else:
messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.')
@@ -135,41 +158,34 @@ class EventDuplicate(EventUpdate):
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('RIGS/event_print.xml')
copies = ('TEC', 'Client')
merger = PdfFileMerger()
for copy in copies:
context = {
'object': object,
'fonts': {
'opensans': {
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'quote': True,
'current_user': request.user,
}
context = RequestContext(request, { # this should be outside the loop, but bug in 1.8.2 prevents this
'object': object,
'fonts': {
'opensans': {
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'copy':copy,
'current_user':request.user,
})
rml = template.render(context)
# context['copy'] = copy # this is the way to do it once we upgrade to Django 1.8.3
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
rml = template.render(context)
buffer = StringIO.StringIO()
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
terms = urllib2.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(StringIO.StringIO(terms.read()))
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(BytesIO(terms.read()))
merged = BytesIO()
merger.write(merged)
@@ -182,6 +198,7 @@ class EventPrint(generic.View):
response.write(merged.getvalue())
return response
class EventArchive(generic.ArchiveIndexView):
model = models.Event
date_field = "start_date"
@@ -218,3 +235,148 @@ class EventArchive(generic.ArchiveIndexView):
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
return qs
class EventAuthorise(generic.UpdateView):
template_name = 'RIGS/eventauthorisation_form.html'
success_template = 'RIGS/eventauthorisation_success.html'
def form_valid(self, form):
self.object = form.save()
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))
return self.render_to_response(self.get_context_data())
@property
def event(self):
return models.Event.objects.select_related('organisation', 'person', 'venue').get(pk=self.kwargs['pk'])
def get_object(self, queryset=None):
return getattr(self.event, 'authorisation', None)
def get_form_class(self):
return forms.InternalClientEventAuthorisationForm
def get_context_data(self, **kwargs):
context = super(EventAuthorise, self).get_context_data(**kwargs)
context['event'] = self.event
context['tos_url'] = settings.TERMS_OF_HIRE_URL
return context
def get(self, request, *args, **kwargs):
if self.get_object() is not None and self.get_object().pk is not None:
if self.event.authorised:
messages.add_message(self.request, messages.WARNING,
"This event has already been authorised. "
"Reauthorising is not necessary at this time.")
else:
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)
def get_form(self, **kwargs):
form = super(EventAuthorise, self).get_form(**kwargs)
form.instance.event = self.event
form.instance.email = self.request.email
form.instance.sent_by = self.request.sent_by
return form
def dispatch(self, request, *args, **kwargs):
# Verify our signature matches up and all is well with the integrity of the URL
try:
data = signing.loads(kwargs.get('hmac'))
assert int(kwargs.get('pk')) == int(data.get('pk'))
request.email = data['email']
request.sent_by = models.Profile.objects.get(pk=data['sent_by'])
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)
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
model = models.Event
form_class = forms.EventAuthorisationRequestForm
template_name = 'RIGS/eventauthorisation_request.html'
@method_decorator(decorators.nottinghamtec_address_required)
def dispatch(self, *args, **kwargs):
return super(EventAuthorisationRequest, self).dispatch(*args, **kwargs)
@property
def object(self):
return self.get_object()
def get_success_url(self):
if self.request.is_ajax():
url = reverse_lazy('closemodal')
messages.info(self.request, "location.reload()")
else:
url = reverse_lazy('event_detail', kwargs={
'pk': self.object.pk,
})
messages.add_message(self.request, messages.SUCCESS, "Authorisation request successfully sent.")
return url
def form_valid(self, form):
email = form.cleaned_data['email']
event = self.object
event.auth_request_by = self.request.user
event.auth_request_at = datetime.datetime.now()
event.auth_request_to = email
event.save()
context = {
'object': self.object,
'request': self.request,
'hmac': signing.dumps({
'pk': self.object.pk,
'email': email,
'sent_by': self.request.user.pk,
}),
}
if email == event.person.email:
context['to_name'] = event.person.name
msg = EmailMultiAlternatives(
"N%05d | %s - Event Authorisation Request" % (self.object.pk, self.object.name),
get_template("RIGS/eventauthorisation_client_request.txt").render(context),
to=[email],
reply_to=[self.request.user.email],
)
css = staticfiles_storage.path('css/email.css')
html = premailer.Premailer(get_template("RIGS/eventauthorisation_client_request.html").render(context),
external_styles=css).transform()
msg.attach_alternative(html, 'text/html')
msg.send()
return super(EventAuthorisationRequest, self).form_valid(form)
class EventAuthoriseRequestEmailPreview(generic.DetailView):
template_name = "RIGS/eventauthorisation_client_request.html"
model = models.Event
def render_to_response(self, context, **response_kwargs):
from django.contrib.staticfiles.storage import staticfiles_storage
css = staticfiles_storage.path('css/email.css')
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
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['hmac'] = signing.dumps({
'pk': self.object.pk,
'email': self.request.GET.get('email', 'hello@world.test'),
'sent_by': self.request.user.pk,
})
context['to_name'] = self.request.GET.get('to_name', None)
return context

98
RIGS/signals.py Normal file
View File

@@ -0,0 +1,98 @@
import re
import urllib.request
import urllib.error
import urllib.parse
from io import BytesIO
from django.db.models.signals import post_save
from PyPDF2 import PdfFileReader, PdfFileMerger
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.template.loader import get_template
from premailer import Premailer
from z3c.rml import rml2pdf
from RIGS import models
def send_eventauthorisation_success_email(instance):
# Generate PDF first to prevent context conflicts
context = {
'object': instance.event,
'fonts': {
'opensans': {
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'receipt': True,
'current_user': False,
}
template = get_template('RIGS/event_print.xml')
merger = PdfFileMerger()
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)
# Produce email content
context = {
'object': instance,
}
if instance.email == instance.event.person.email:
context['to_name'] = instance.event.person.name
subject = "N%05d | %s - Event Authorised" % (instance.event.pk, instance.event.name)
client_email = EmailMultiAlternatives(
subject,
get_template("RIGS/eventauthorisation_client_success.txt").render(context),
to=[instance.email],
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
)
css = staticfiles_storage.path('css/email.css')
html = Premailer(get_template("RIGS/eventauthorisation_client_success.html").render(context),
external_styles=css).transform()
client_email.attach_alternative(html, 'text/html')
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', instance.event.name)
client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
merged.getvalue(),
'application/pdf'
)
if instance.event.mic:
mic_email_address = instance.event.mic.email
else:
mic_email_address = settings.AUTHORISATION_NOTIFICATION_ADDRESS
mic_email = EmailMessage(
subject,
get_template("RIGS/eventauthorisation_mic_success.txt").render(context),
to=[mic_email_address]
)
# Now we have both emails successfully generated, send them out
client_email.send(fail_silently=True)
mic_email.send(fail_silently=True)
def on_revision_commit(sender, instance, created, **kwargs):
if created:
send_eventauthorisation_success_email(instance)
post_save.connect(on_revision_commit, sender=models.EventAuthorisation)

12
RIGS/static/css/bootstrap-select.min.css vendored Normal file → Executable file

File diff suppressed because one or more lines are too long

View File

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

12
RIGS/static/js/affix.js Normal file → Executable file
View File

@@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: affix.js v3.3.2
* Bootstrap: affix.js v3.3.7
* http://getbootstrap.com/javascript/#affix
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@@ -21,14 +21,14 @@
.on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this))
this.$element = $(element)
this.affixed =
this.unpin =
this.affixed = null
this.unpin = null
this.pinnedOffset = null
this.checkPosition()
}
Affix.VERSION = '3.3.2'
Affix.VERSION = '3.3.7'
Affix.RESET = 'affix affix-top affix-bottom'
@@ -78,7 +78,7 @@
var offset = this.options.offset
var offsetTop = offset.top
var offsetBottom = offset.bottom
var scrollHeight = $('body').height()
var scrollHeight = Math.max($(document).height(), $(document.body).height())
if (typeof offset != 'object') offsetBottom = offsetTop = offset
if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element)

84
RIGS/static/js/ajax-bootstrap-select.js Executable file → Normal file
View File

@@ -3,16 +3,16 @@
*
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
*
* @version 1.3.1
* @version 1.4.1
* @author Adam Heim - https://github.com/truckingsim
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
* @copyright 2015 Adam Heim
* @copyright 2017 Adam Heim
* @license Released under the MIT license.
*
* Contributors:
* Mark Carver - https://github.com/markcarver
*
* Last build: 2015-01-06 8:43:11 PM EST
* Last build: 2017-07-21 1:08:54 PM GMT-0400
*/
!(function ($, window) {
@@ -186,10 +186,25 @@ var AjaxBootstrapSelect = function (element, options) {
if (dataKeys.length) {
// Object containing the data attribute options.
var dataOptions = {};
var flattenedOptions = ['locale'];
for (i = 0, l = dataKeys.length; i < l; i++) {
var name = dataKeys[i].replace(/^abs([A-Z])/, matchToLowerCase).replace(/([A-Z])/g, '-$1').toLowerCase();
var keys = name.split('-');
// Certain options should be flattened to a single object
// and not fully expanded (such as Locale).
if (keys[0] && keys.length > 1 && flattenedOptions.indexOf(keys[0]) !== -1) {
var newKeys = [keys.shift()];
var property = '';
// Combine the remaining keys as a single property.
for (var ii = 0; ii < keys.length; ii++) {
property += (ii === 0 ? keys[ii] : keys[ii].charAt(0).toUpperCase() + keys[ii].slice(1));
}
newKeys.push(property);
keys = newKeys;
}
this.log(this.LOG_DEBUG, 'Processing data attribute "data-abs-' + name + '":', data[dataKeys[i]]);
expandObject(name.split('-'), data[dataKeys[i]], dataOptions);
expandObject(keys, data[dataKeys[i]], dataOptions);
}
this.options = $.extend(true, {}, this.options, dataOptions);
this.log(this.LOG_DEBUG, 'Merged in the data attribute options: ', dataOptions, this.options);
@@ -315,6 +330,12 @@ AjaxBootstrapSelect.prototype.init = function () {
plugin.log(plugin.LOG_DEBUG, 'Key ignored.');
return;
}
// Don't process if below minimum query length
if (query.length < plugin.options.minLength) {
plugin.list.setStatus(plugin.t('statusTooShort'));
return;
}
// Clear out any existing timer.
clearTimeout(requestDelayTimer);
@@ -573,8 +594,25 @@ var AjaxBootstrapSelectList = function (plugin) {
this.title = null;
this.selectedTextFormat = plugin.selectpicker.options.selectedTextFormat;
// Save initial options
var initial_options = [];
plugin.$element.find('option').each(function() {
var $option = $(this);
var value = $option.attr('value');
initial_options.push({
value: value,
text: $option.text(),
'class': $option.attr('class') || '',
data: $option.data() || {},
preserved: plugin.options.preserveSelected,
selected: !!$option.attr('selected')
});
});
this.cacheSet(/*query=*/'', initial_options);
// Preserve selected options.
if (plugin.options.preserveSelected) {
that.selected = initial_options;
plugin.$element.on('change.abs.preserveSelected', function (e) {
var $selected = plugin.$element.find(':selected');
that.selected = [];
@@ -644,7 +682,7 @@ AjaxBootstrapSelectList.prototype.build = function (data) {
}
// Set various properties.
$option.val(item.value).text(item.text);
$option.val(item.value).text(item.text).attr('title', item.text);
if (item['class'].length) {
$option.attr('class', item['class']);
}
@@ -732,7 +770,13 @@ AjaxBootstrapSelectList.prototype.refresh = function (triggerChange) {
if (!this.plugin.$element.find('option').length && emptyTitle && emptyTitle.length) {
this.setTitle(emptyTitle);
}
else if (this.title) {
else if (
this.title ||
(
this.selectedTextFormat !== 'static' &&
this.selectedTextFormat !== this.plugin.selectpicker.options.selectedTextFormat
)
) {
this.restoreTitle();
}
this.plugin.selectpicker.refresh();
@@ -1224,6 +1268,14 @@ $.fn.ajaxSelectPicker.defaults = {
}
},
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Number} minLength = 0
* @markdown
* Invoke a request for empty search values.
*/
minLength: 0,
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {String} ajaxSearchUrl
@@ -1296,8 +1348,7 @@ $.fn.ajaxSelectPicker.defaults = {
* 39: "right",
* 38: "up",
* 40: "down",
* 91: "meta",
* 229: "unknown"
* 91: "meta"
* }
* ```
*/
@@ -1311,8 +1362,7 @@ $.fn.ajaxSelectPicker.defaults = {
39: "right",
38: "up",
40: "down",
91: "meta",
229: "unknown"
91: "meta"
},
/**
@@ -1406,7 +1456,7 @@ $.fn.ajaxSelectPicker.defaults = {
* }
* ```
*/
preprocessData: function(){},
preprocessData: function () { },
/**
* @member $.fn.ajaxSelectPicker.defaults
@@ -1434,7 +1484,7 @@ $.fn.ajaxSelectPicker.defaults = {
* @markdown
* Process the data returned after this plugin, but before the list is built.
*/
processData: function(){},
processData: function () { },
/**
* @member $.fn.ajaxSelectPicker.defaults
@@ -1547,7 +1597,15 @@ $.fn.ajaxSelectPicker.locale['en-US'] = {
* @markdown
* The text to use in the status container when a request is being initiated.
*/
statusSearching: 'Searching...'
statusSearching: 'Searching...',
/**
* @member $.fn.ajaxSelectPicker.locale
* @cfg {String} statusToShort = 'Please enter more characters'
* @markdown
* The text used in the status container when the request returns no results.
*/
statusTooShort: 'Please enter more characters'
};
$.fn.ajaxSelectPicker.locale.en = $.fn.ajaxSelectPicker.locale['en-US'];

8
RIGS/static/js/alert.js Normal file → Executable file
View File

@@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: alert.js v3.3.2
* Bootstrap: alert.js v3.3.7
* http://getbootstrap.com/javascript/#alerts
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@@ -18,7 +18,7 @@
$(el).on('click', dismiss, this.close)
}
Alert.VERSION = '3.3.2'
Alert.VERSION = '3.3.7'
Alert.TRANSITION_DURATION = 150
@@ -31,7 +31,7 @@
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
var $parent = $(selector)
var $parent = $(selector === '#' ? [] : selector)
if (e) e.preventDefault()

File diff suppressed because it is too large Load Diff

37
RIGS/static/js/button.js Normal file → Executable file
View File

@@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: button.js v3.3.2
* Bootstrap: button.js v3.3.7
* http://getbootstrap.com/javascript/#buttons
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@@ -19,7 +19,7 @@
this.isLoading = false
}
Button.VERSION = '3.3.2'
Button.VERSION = '3.3.7'
Button.DEFAULTS = {
loadingText: 'loading...'
@@ -31,7 +31,7 @@
var val = $el.is('input') ? 'val' : 'html'
var data = $el.data()
state = state + 'Text'
state += 'Text'
if (data.resetText == null) $el.data('resetText', $el[val]())
@@ -41,10 +41,10 @@
if (state == 'loadingText') {
this.isLoading = true
$el.addClass(d).attr(d, d)
$el.addClass(d).attr(d, d).prop(d, true)
} else if (this.isLoading) {
this.isLoading = false
$el.removeClass(d).removeAttr(d)
$el.removeClass(d).removeAttr(d).prop(d, false)
}
}, this), 0)
}
@@ -56,15 +56,19 @@
if ($parent.length) {
var $input = this.$element.find('input')
if ($input.prop('type') == 'radio') {
if ($input.prop('checked') && this.$element.hasClass('active')) changed = false
else $parent.find('.active').removeClass('active')
if ($input.prop('checked')) changed = false
$parent.find('.active').removeClass('active')
this.$element.addClass('active')
} else if ($input.prop('type') == 'checkbox') {
if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false
this.$element.toggleClass('active')
}
if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change')
$input.prop('checked', this.$element.hasClass('active'))
if (changed) $input.trigger('change')
} else {
this.$element.attr('aria-pressed', !this.$element.hasClass('active'))
this.$element.toggleClass('active')
}
if (changed) this.$element.toggleClass('active')
}
@@ -104,10 +108,15 @@
$(document)
.on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) {
var $btn = $(e.target)
if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
var $btn = $(e.target).closest('.btn')
Plugin.call($btn, 'toggle')
e.preventDefault()
if (!($(e.target).is('input[type="radio"], input[type="checkbox"]'))) {
// Prevent double click on radios, and the double selections (so cancellation) on checkboxes
e.preventDefault()
// The target component still receive the focus
if ($btn.is('input,button')) $btn.trigger('focus')
else $btn.find('input:visible,button:visible').first().trigger('focus')
}
})
.on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) {
$(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type))

14
RIGS/static/js/carousel.js Normal file → Executable file
View File

@@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: carousel.js v3.3.2
* Bootstrap: carousel.js v3.3.7
* http://getbootstrap.com/javascript/#carousel
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@@ -17,10 +17,10 @@
this.$element = $(element)
this.$indicators = this.$element.find('.carousel-indicators')
this.options = options
this.paused =
this.sliding =
this.interval =
this.$active =
this.paused = null
this.sliding = null
this.interval = null
this.$active = null
this.$items = null
this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this))
@@ -30,7 +30,7 @@
.on('mouseleave.bs.carousel', $.proxy(this.cycle, this))
}
Carousel.VERSION = '3.3.2'
Carousel.VERSION = '3.3.7'
Carousel.TRANSITION_DURATION = 600

17
RIGS/static/js/collapse.js Normal file → Executable file
View File

@@ -1,11 +1,12 @@
/* ========================================================================
* Bootstrap: collapse.js v3.3.2
* Bootstrap: collapse.js v3.3.7
* http://getbootstrap.com/javascript/#collapse
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
/* jshint latedef: false */
+function ($) {
'use strict';
@@ -16,7 +17,8 @@
var Collapse = function (element, options) {
this.$element = $(element)
this.options = $.extend({}, Collapse.DEFAULTS, options)
this.$trigger = $(this.options.trigger).filter('[href="#' + element.id + '"], [data-target="#' + element.id + '"]')
this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' +
'[data-toggle="collapse"][data-target="#' + element.id + '"]')
this.transitioning = null
if (this.options.parent) {
@@ -28,13 +30,12 @@
if (this.options.toggle) this.toggle()
}
Collapse.VERSION = '3.3.2'
Collapse.VERSION = '3.3.7'
Collapse.TRANSITION_DURATION = 350
Collapse.DEFAULTS = {
toggle: true,
trigger: '[data-toggle="collapse"]'
toggle: true
}
Collapse.prototype.dimension = function () {
@@ -172,7 +173,7 @@
var data = $this.data('bs.collapse')
var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data && options.toggle && option == 'show') options.toggle = false
if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false
if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
if (typeof option == 'string') data[option]()
})
@@ -203,7 +204,7 @@
var $target = getTargetFromTrigger($this)
var data = $target.data('bs.collapse')
var option = data ? 'toggle' : $.extend({}, $this.data(), { trigger: this })
var option = data ? 'toggle' : $this.data()
Plugin.call($target, option)
})

94
RIGS/static/js/dropdown.js Normal file → Executable file
View File

@@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: dropdown.js v3.3.2
* Bootstrap: dropdown.js v3.3.7
* http://getbootstrap.com/javascript/#dropdowns
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@@ -19,7 +19,41 @@
$(element).on('click.bs.dropdown', this.toggle)
}
Dropdown.VERSION = '3.3.2'
Dropdown.VERSION = '3.3.7'
function getParent($this) {
var selector = $this.attr('data-target')
if (!selector) {
selector = $this.attr('href')
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
var $parent = selector && $(selector)
return $parent && $parent.length ? $parent : $this.parent()
}
function clearMenus(e) {
if (e && e.which === 3) return
$(backdrop).remove()
$(toggle).each(function () {
var $this = $(this)
var $parent = getParent($this)
var relatedTarget = { relatedTarget: this }
if (!$parent.hasClass('open')) return
if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return
$parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
if (e.isDefaultPrevented()) return
$this.attr('aria-expanded', 'false')
$parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget))
})
}
Dropdown.prototype.toggle = function (e) {
var $this = $(this)
@@ -34,7 +68,10 @@
if (!isActive) {
if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
// if mobile we use a backdrop because click events don't delegate
$('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus)
$(document.createElement('div'))
.addClass('dropdown-backdrop')
.insertAfter($(this))
.on('click', clearMenus)
}
var relatedTarget = { relatedTarget: this }
@@ -48,7 +85,7 @@
$parent
.toggleClass('open')
.trigger('shown.bs.dropdown', relatedTarget)
.trigger($.Event('shown.bs.dropdown', relatedTarget))
}
return false
@@ -67,57 +104,25 @@
var $parent = getParent($this)
var isActive = $parent.hasClass('open')
if ((!isActive && e.which != 27) || (isActive && e.which == 27)) {
if (!isActive && e.which != 27 || isActive && e.which == 27) {
if (e.which == 27) $parent.find(toggle).trigger('focus')
return $this.trigger('click')
}
var desc = ' li:not(.divider):visible a'
var $items = $parent.find('[role="menu"]' + desc + ', [role="listbox"]' + desc)
var desc = ' li:not(.disabled):visible a'
var $items = $parent.find('.dropdown-menu' + desc)
if (!$items.length) return
var index = $items.index(e.target)
if (e.which == 38 && index > 0) index-- // up
if (e.which == 40 && index < $items.length - 1) index++ // down
if (!~index) index = 0
if (e.which == 38 && index > 0) index-- // up
if (e.which == 40 && index < $items.length - 1) index++ // down
if (!~index) index = 0
$items.eq(index).trigger('focus')
}
function clearMenus(e) {
if (e && e.which === 3) return
$(backdrop).remove()
$(toggle).each(function () {
var $this = $(this)
var $parent = getParent($this)
var relatedTarget = { relatedTarget: this }
if (!$parent.hasClass('open')) return
$parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
if (e.isDefaultPrevented()) return
$this.attr('aria-expanded', 'false')
$parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget)
})
}
function getParent($this) {
var selector = $this.attr('data-target')
if (!selector) {
selector = $this.attr('href')
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
var $parent = selector && $(selector)
return $parent && $parent.length ? $parent : $this.parent()
}
// DROPDOWN PLUGIN DEFINITION
// ==========================
@@ -155,7 +160,6 @@
.on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
.on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
.on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
.on('keydown.bs.dropdown.data-api', '[role="menu"]', Dropdown.prototype.keydown)
.on('keydown.bs.dropdown.data-api', '[role="listbox"]', Dropdown.prototype.keydown)
.on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown)
}(jQuery);

View File

@@ -136,4 +136,4 @@ $("#item-table tbody").sortable({
});
}
});
});

10
RIGS/static/js/modal.js Normal file → Executable file
View File

@@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: modal.js v3.3.5
* Bootstrap: modal.js v3.3.7
* http://getbootstrap.com/javascript/#modals
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@@ -33,7 +33,7 @@
}
}
Modal.VERSION = '3.3.5'
Modal.VERSION = '3.3.7'
Modal.TRANSITION_DURATION = 300
Modal.BACKDROP_TRANSITION_DURATION = 150
@@ -140,7 +140,9 @@
$(document)
.off('focusin.bs.modal') // guard against infinite focus loop
.on('focusin.bs.modal', $.proxy(function (e) {
if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
if (document !== e.target &&
this.$element[0] !== e.target &&
!this.$element.has(e.target).length) {
this.$element.trigger('focus')
}
}, this))

13
RIGS/static/js/popover.js Normal file → Executable file
View File

@@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: popover.js v3.3.2
* Bootstrap: popover.js v3.3.7
* http://getbootstrap.com/javascript/#popovers
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@@ -19,7 +19,7 @@
if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
Popover.VERSION = '3.3.2'
Popover.VERSION = '3.3.7'
Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
placement: 'right',
@@ -75,11 +75,6 @@
return (this.$arrow = this.$arrow || this.tip().find('.arrow'))
}
Popover.prototype.tip = function () {
if (!this.$tip) this.$tip = $(this.options.template)
return this.$tip
}
// POPOVER PLUGIN DEFINITION
// =========================
@@ -90,7 +85,7 @@
var data = $this.data('bs.popover')
var options = typeof option == 'object' && option
if (!data && option == 'destroy') return
if (!data && /destroy|hide/.test(option)) return
if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
if (typeof option == 'string') data[option]()
})

39
RIGS/static/js/scrollspy.js Normal file → Executable file
View File

@@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: scrollspy.js v3.3.2
* Bootstrap: scrollspy.js v3.3.7
* http://getbootstrap.com/javascript/#scrollspy
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@@ -14,10 +14,8 @@
// ==========================
function ScrollSpy(element, options) {
var process = $.proxy(this.process, this)
this.$body = $('body')
this.$scrollElement = $(element).is('body') ? $(window) : $(element)
this.$body = $(document.body)
this.$scrollElement = $(element).is(document.body) ? $(window) : $(element)
this.options = $.extend({}, ScrollSpy.DEFAULTS, options)
this.selector = (this.options.target || '') + ' .nav li > a'
this.offsets = []
@@ -25,12 +23,12 @@
this.activeTarget = null
this.scrollHeight = 0
this.$scrollElement.on('scroll.bs.scrollspy', process)
this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this))
this.refresh()
this.process()
}
ScrollSpy.VERSION = '3.3.2'
ScrollSpy.VERSION = '3.3.7'
ScrollSpy.DEFAULTS = {
offset: 10
@@ -41,20 +39,19 @@
}
ScrollSpy.prototype.refresh = function () {
var offsetMethod = 'offset'
var offsetBase = 0
var that = this
var offsetMethod = 'offset'
var offsetBase = 0
this.offsets = []
this.targets = []
this.scrollHeight = this.getScrollHeight()
if (!$.isWindow(this.$scrollElement[0])) {
offsetMethod = 'position'
offsetBase = this.$scrollElement.scrollTop()
}
this.offsets = []
this.targets = []
this.scrollHeight = this.getScrollHeight()
var self = this
this.$body
.find(this.selector)
.map(function () {
@@ -69,8 +66,8 @@
})
.sort(function (a, b) { return a[0] - b[0] })
.each(function () {
self.offsets.push(this[0])
self.targets.push(this[1])
that.offsets.push(this[0])
that.targets.push(this[1])
})
}
@@ -99,7 +96,7 @@
for (i = offsets.length; i--;) {
activeTarget != targets[i]
&& scrollTop >= offsets[i]
&& (!offsets[i + 1] || scrollTop <= offsets[i + 1])
&& (offsets[i + 1] === undefined || scrollTop < offsets[i + 1])
&& this.activate(targets[i])
}
}
@@ -110,8 +107,8 @@
this.clear()
var selector = this.selector +
'[data-target="' + target + '"],' +
this.selector + '[href="' + target + '"]'
'[data-target="' + target + '"],' +
this.selector + '[href="' + target + '"]'
var active = $(selector)
.parents('li')

12
RIGS/static/js/tab.js Normal file → Executable file
View File

@@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: tab.js v3.3.2
* Bootstrap: tab.js v3.3.7
* http://getbootstrap.com/javascript/#tabs
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@@ -14,10 +14,12 @@
// ====================
var Tab = function (element) {
// jscs:disable requireDollarBeforejQueryAssignment
this.element = $(element)
// jscs:enable requireDollarBeforejQueryAssignment
}
Tab.VERSION = '3.3.2'
Tab.VERSION = '3.3.7'
Tab.TRANSITION_DURATION = 150
@@ -65,7 +67,7 @@
var $active = container.find('> .active')
var transition = callback
&& $.support.transition
&& (($active.length && $active.hasClass('fade')) || !!container.find('> .fade').length)
&& ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length)
function next() {
$active
@@ -88,7 +90,7 @@
element.removeClass('fade')
}
if (element.parent('.dropdown-menu')) {
if (element.parent('.dropdown-menu').length) {
element
.closest('li.dropdown')
.addClass('active')

120
RIGS/static/js/tooltip.js Normal file → Executable file
View File

@@ -1,9 +1,9 @@
/* ========================================================================
* Bootstrap: tooltip.js v3.3.2
* Bootstrap: tooltip.js v3.3.7
* http://getbootstrap.com/javascript/#tooltip
* Inspired by the original jQuery.tipsy by Jason Frame
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
@@ -15,17 +15,18 @@
// ===============================
var Tooltip = function (element, options) {
this.type =
this.options =
this.enabled =
this.timeout =
this.hoverState =
this.type = null
this.options = null
this.enabled = null
this.timeout = null
this.hoverState = null
this.$element = null
this.inState = null
this.init('tooltip', element, options)
}
Tooltip.VERSION = '3.3.2'
Tooltip.VERSION = '3.3.7'
Tooltip.TRANSITION_DURATION = 150
@@ -50,7 +51,12 @@
this.type = type
this.$element = $(element)
this.options = this.getOptions(options)
this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport)
this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
this.inState = { click: false, hover: false, focus: false }
if (this.$element[0] instanceof document.constructor && !this.options.selector) {
throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!')
}
var triggers = this.options.trigger.split(' ')
@@ -105,16 +111,20 @@
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget).data('bs.' + this.type)
if (self && self.$tip && self.$tip.is(':visible')) {
self.hoverState = 'in'
return
}
if (!self) {
self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
$(obj.currentTarget).data('bs.' + this.type, self)
}
if (obj instanceof $.Event) {
self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true
}
if (self.tip().hasClass('in') || self.hoverState == 'in') {
self.hoverState = 'in'
return
}
clearTimeout(self.timeout)
self.hoverState = 'in'
@@ -126,6 +136,14 @@
}, self.options.delay.show)
}
Tooltip.prototype.isInStateTrue = function () {
for (var key in this.inState) {
if (this.inState[key]) return true
}
return false
}
Tooltip.prototype.leave = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget).data('bs.' + this.type)
@@ -135,6 +153,12 @@
$(obj.currentTarget).data('bs.' + this.type, self)
}
if (obj instanceof $.Event) {
self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false
}
if (self.isInStateTrue()) return
clearTimeout(self.timeout)
self.hoverState = 'out'
@@ -181,6 +205,7 @@
.data('bs.' + this.type, this)
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
this.$element.trigger('inserted.bs.' + this.type)
var pos = this.getPosition()
var actualWidth = $tip[0].offsetWidth
@@ -188,13 +213,12 @@
if (autoPlace) {
var orgPlacement = placement
var $container = this.options.container ? $(this.options.container) : this.$element.parent()
var containerDim = this.getPosition($container)
var viewportDim = this.getPosition(this.$viewport)
placement = placement == 'bottom' && pos.bottom + actualHeight > containerDim.bottom ? 'top' :
placement == 'top' && pos.top - actualHeight < containerDim.top ? 'bottom' :
placement == 'right' && pos.right + actualWidth > containerDim.width ? 'left' :
placement == 'left' && pos.left - actualWidth < containerDim.left ? 'right' :
placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' :
placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' :
placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' :
placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' :
placement
$tip
@@ -235,8 +259,8 @@
if (isNaN(marginTop)) marginTop = 0
if (isNaN(marginLeft)) marginLeft = 0
offset.top = offset.top + marginTop
offset.left = offset.left + marginLeft
offset.top += marginTop
offset.left += marginLeft
// $.fn.offset doesn't round pixel values
// so we use setOffset directly with our own function B-0
@@ -272,10 +296,10 @@
this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)
}
Tooltip.prototype.replaceArrow = function (delta, dimension, isHorizontal) {
Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) {
this.arrow()
.css(isHorizontal ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
.css(isHorizontal ? 'top' : 'left', '')
.css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
.css(isVertical ? 'top' : 'left', '')
}
Tooltip.prototype.setContent = function () {
@@ -288,14 +312,16 @@
Tooltip.prototype.hide = function (callback) {
var that = this
var $tip = this.tip()
var $tip = $(this.$tip)
var e = $.Event('hide.bs.' + this.type)
function complete() {
if (that.hoverState != 'in') $tip.detach()
that.$element
.removeAttr('aria-describedby')
.trigger('hidden.bs.' + that.type)
if (that.$element) { // TODO: Check whether guarding this code with this `if` is really necessary.
that.$element
.removeAttr('aria-describedby')
.trigger('hidden.bs.' + that.type)
}
callback && callback()
}
@@ -305,7 +331,7 @@
$tip.removeClass('in')
$.support.transition && this.$tip.hasClass('fade') ?
$.support.transition && $tip.hasClass('fade') ?
$tip
.one('bsTransitionEnd', complete)
.emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
@@ -318,7 +344,7 @@
Tooltip.prototype.fixTitle = function () {
var $e = this.$element
if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') {
if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') {
$e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
}
}
@@ -338,7 +364,10 @@
// width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093
elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })
}
var elOffset = isBody ? { top: 0, left: 0 } : $element.offset()
var isSvg = window.SVGElement && el instanceof window.SVGElement
// Avoid using $.offset() on SVGs since it gives incorrect results in jQuery 3.
// See https://github.com/twbs/bootstrap/issues/20280
var elOffset = isBody ? { top: 0, left: 0 } : (isSvg ? null : $element.offset())
var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
@@ -373,7 +402,7 @@
var rightEdgeOffset = pos.left + viewportPadding + actualWidth
if (leftEdgeOffset < viewportDimensions.left) { // left overflow
delta.left = viewportDimensions.left - leftEdgeOffset
} else if (rightEdgeOffset > viewportDimensions.width) { // right overflow
} else if (rightEdgeOffset > viewportDimensions.right) { // right overflow
delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
}
}
@@ -399,7 +428,13 @@
}
Tooltip.prototype.tip = function () {
return (this.$tip = this.$tip || $(this.options.template))
if (!this.$tip) {
this.$tip = $(this.options.template)
if (this.$tip.length != 1) {
throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!')
}
}
return this.$tip
}
Tooltip.prototype.arrow = function () {
@@ -428,7 +463,13 @@
}
}
self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
if (e) {
self.inState.click = !self.inState.click
if (self.isInStateTrue()) self.enter(self)
else self.leave(self)
} else {
self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
}
}
Tooltip.prototype.destroy = function () {
@@ -436,6 +477,13 @@
clearTimeout(this.timeout)
this.hide(function () {
that.$element.off('.' + that.type).removeData('bs.' + that.type)
if (that.$tip) {
that.$tip.detach()
}
that.$tip = null
that.$arrow = null
that.$viewport = null
that.$element = null
})
}
@@ -449,7 +497,7 @@
var data = $this.data('bs.tooltip')
var options = typeof option == 'object' && option
if (!data && option == 'destroy') return
if (!data && /destroy|hide/.test(option)) return
if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
if (typeof option == 'string') data[option]()
})

4
RIGS/static/js/transition.js Normal file → Executable file
View File

@@ -1,8 +1,8 @@
/* ========================================================================
* Bootstrap: transition.js v3.3.2
* Bootstrap: transition.js v3.3.7
* http://getbootstrap.com/javascript/#transitions
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */

View File

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

View File

@@ -33,13 +33,13 @@
{% endif %}
<p>
<small>
{% if version.old == None %}
{% if version.changes.old == None %}
Created
{% else %}
Changed {% include 'RIGS/version_changes.html' %} in
{% endif %}
{% include 'RIGS/object_button.html' with object=version.new %}
{% include 'RIGS/object_button.html' with object=version.changes.new %}
{% if version.revision.comment %}
({{ version.revision.comment }})
{% endif %}

View File

@@ -67,16 +67,16 @@
<tr>
<td>{{ version.revision.date_created }}</td>
<td><a href="{{ version.new.get_absolute_url }}">{{version.new|to_class_name}} {{ version.new.pk|stringformat:"05d" }}</a></td>
<td>{{ version.version.pk }}|{{ version.revision.pk }}</td>
<td><a href="{{ version.changes.new.get_absolute_url }}">{{version.changes.new|to_class_name}} {{ version.changes.new.pk|stringformat:"05d" }}</a></td>
<td>{{ version.pk }}|{{ version.revision.pk }}</td>
<td>{{ version.revision.user.name }}</td>
<td>
{% if version.old == None %}
{{version.new|to_class_name}} Created
{% if version.changes.old == None %}
{{version.changes.new|to_class_name}} Created
{% else %}
{% include 'RIGS/version_changes.html' %}
{% endif %} </td>
<td>{{ version.revision.comment }}</td>
<td>{{ version.changes.revision.comment }}</td>
</tr>
{% endfor %}

View File

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

View File

@@ -11,36 +11,9 @@
</h1>
</div>
<div class="col-sm-12 text-right">
<div class="btn-group btn-page">
<a href="{% url 'event_update' event.pk %}" class="btn btn-default"><span
class="glyphicon glyphicon-edit"></span> <span
class="hidden-xs">Edit</span></a>
{% if event.is_rig %}
<a href="{% url 'event_print' event.pk %}" target="_blank" class="btn btn-default"><span
class="glyphicon glyphicon-print"></span> <span
class="hidden-xs">Print</span></a>
{% endif %}
<a href="{% url 'event_duplicate' event.pk %}" class="btn btn-default" title="Duplicate Rig"><span
class="glyphicon glyphicon-duplicate"></span> <span
class="hidden-xs">Duplicate</span></a>
{% if event.is_rig %}
{% if perms.RIGS.add_invoice %}
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
{% if event.invoice and event.invoice.is_closed %}
btn-success
{% elif event.invoice %}
btn-warning
{% else %}
btn-danger
{% endif %}
" title="Invoice Rig"><span
class="glyphicon glyphicon-gbp"></span>
<span class="hidden-xs">Invoice</span></a>
{% endif %}
{% endif %}
</div>
{% include 'RIGS/event_detail_buttons.html' %}
</div>
{% endif %}
{% if object.is_rig %}
{# only need contact details for a rig #}
@@ -97,6 +70,39 @@
</div>
</div>
{% endif %}
{% if event.is_rig and event.internal %}
<div class="panel panel-default">
<div class="panel-heading">Client Authorisation</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>Authorised</dt>
<dd>{{ object.authorised|yesno:"Yes,No" }}</dd>
<dt>Authorised by</dt>
<dd>
{% if object.authorisation %}
{{ object.authorisation.name }}
(<a href="mailto:{{ object.authorisation.email }}">{{ object.authorisation.email }}</a>)
{% endif %}
</dd>
<dt>Authorised at</dt>
<dd>{{ object.authorisation.last_edited_at }}</dd>
<dt>Authorised amount</dt>
<dd>
{% if object.authorisation %}
£ {{ object.authorisation.amount|floatformat:"2" }}
{% endif %}
</dd>
<dt>Requested by</dt>
<dd>{{ object.authorisation.sent_by }}</dd>
</dl>
</div>
</div>
{% endif %}
</div>
{% endif %}
<div class="col-sm-12 {% if event.is_rig %}col-md-6 col-lg-7{% endif %}">
@@ -175,8 +181,25 @@
{% endif %}
{% if event.is_rig %}
<dt>PO</dt>
<dd>{{ object.purchase_order }}</dd>
<dd>&nbsp;</dd>
{% if object.internal %}
<dt>Authorisation Request</dt>
<dd>{{ object.auth_request_to|yesno:"Yes,No" }}</dd>
<dt>By</dt>
<dd>{{ object.auth_request_by }}</dd>
<dt>At</dt>
<dd>{{ object.auth_request_at|date:"D d M Y H:i"|default:"" }}</dd>
<dt>To</dt>
<dd>{{ object.auth_request_to }}</dd>
{% else %}
<dt>PO</dt>
<dd>{{ object.purchase_order }}</dd>
{% endif %}
{% endif %}
</dl>
</div>
@@ -184,34 +207,7 @@
</div>
{% if not request.is_ajax %}
<div class="col-sm-12 text-right">
<div class="btn-group btn-page">
<a href="{% url 'event_update' event.pk %}" class="btn btn-default"><span
class="glyphicon glyphicon-edit"></span> <span
class="hidden-xs">Edit</span></a>
{% if event.is_rig %}
<a href="{% url 'event_print' event.pk %}" target="_blank" class="btn btn-default"><span
class="glyphicon glyphicon-print"></span> <span
class="hidden-xs">Print</span></a>
{% endif %}
<a href="{% url 'event_duplicate' event.pk %}" class="btn btn-default" title="Duplicate Rig"><span
class="glyphicon glyphicon-duplicate"></span> <span
class="hidden-xs">Duplicate</span></a>
{% if event.is_rig %}
{% if perms.RIGS.add_invoice %}
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
{% if event.invoice and event.invoice.is_closed %}
btn-success
{% elif event.invoice %}
btn-warning
{% else %}
btn-danger
{% endif %}
" title="Invoice Rig"><span
class="glyphicon glyphicon-gbp"></span>
<span class="hidden-xs">Invoice</span></a>
{% endif %}
{% endif %}
</div>
{% include 'RIGS/event_detail_buttons.html' %}
</div>
{% endif %}
{% if event.is_rig %}
@@ -229,34 +225,7 @@
</div>
{% if not request.is_ajax %}
<div class="col-sm-12 text-right">
<div class="btn-group btn-page">
<a href="{% url 'event_update' event.pk %}" class="btn btn-default"><span
class="glyphicon glyphicon-edit"></span> <span
class="hidden-xs">Edit</span></a>
{% if event.is_rig %}
<a href="{% url 'event_print' event.pk %}" target="_blank" class="btn btn-default"><span
class="glyphicon glyphicon-print"></span> <span
class="hidden-xs">Print</span></a>
{% endif %}
<a href="{% url 'event_duplicate' event.pk %}" class="btn btn-default" title="Duplicate Rig"><span
class="glyphicon glyphicon-duplicate"></span> <span
class="hidden-xs">Duplicate</span></a>
{% if event.is_rig %}
{% if perms.RIGS.add_invoice %}
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
{% if event.invoice and event.invoice.is_closed %}
btn-success
{% elif event.invoice %}
btn-warning
{% else %}
btn-danger
{% endif %}
" title="Invoice Rig"><span
class="glyphicon glyphicon-gbp"></span>
<span class="hidden-xs">Invoice</span></a>
{% endif %}
{% endif %}
</div>
{% include 'RIGS/event_detail_buttons.html' %}
</div>
{% endif %}
{% endif %}

View File

@@ -0,0 +1,54 @@
<div class="btn-group btn-page">
<a href="{% url 'event_update' event.pk %}" class="btn btn-default"><span
class="glyphicon glyphicon-edit"></span> <span
class="hidden-xs">Edit</span></a>
{% if event.is_rig %}
<a href="{% url 'event_print' event.pk %}" target="_blank" class="btn btn-default"><span
class="glyphicon glyphicon-print"></span> <span
class="hidden-xs">Print</span></a>
{% endif %}
<a href="{% url 'event_duplicate' event.pk %}" class="btn btn-default" title="Duplicate Rig"><span
class="glyphicon glyphicon-duplicate"></span> <span
class="hidden-xs">Duplicate</span></a>
{% if event.is_rig %}
{% if event.internal %}
<a class="btn btn-default item-add modal-href event-authorise-request
{% if event.authorised %}
btn-success
{% 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 %}
btn-info
{% endif %}
"
href="{% url 'event_authorise_request' object.pk %}">
<span class="glyphicon glyphicon-send"></span>
<span class="hidden-xs">
{% if event.authorised %}
Authorised
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
Authorisation Issue
{% elif event.auth_request_to %}
Awaiting Authorisation
{% else %}
Request Authorisation
{% endif %}
</span>
</a>
{% endif %}
{% if perms.RIGS.add_invoice %}
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
{% if event.invoice and event.invoice.is_closed %}
btn-success
{% elif event.invoice %}
btn-warning
{% else %}
btn-danger
{% endif %}
" title="Invoice Rig"><span
class="glyphicon glyphicon-gbp"></span>
<span class="hidden-xs">Invoice</span></a>
{% endif %}
{% endif %}
</div>

View File

@@ -289,10 +289,10 @@
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
{% render_field form.start_date type="date" class+="form-control" %}
{% render_field form.start_date class+="form-control" %}
</div>
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="Start time of event, can be left blank">
{% render_field form.start_time type="time" class+="form-control" %}
{% render_field form.start_time class+="form-control" %}
</div>
</div>
</div>
@@ -304,10 +304,10 @@
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
{% render_field form.end_date type="date" class+="form-control" %}
{% render_field form.end_date class+="form-control" %}
</div>
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="End time of event, leave blank if unknown">
{% render_field form.end_time type="time" class+="form-control" %}
{% render_field form.end_time class+="form-control" %}
</div>
</div>
@@ -329,7 +329,7 @@
class="col-sm-4 control-label">{{ form.access_at.label }}</label>
<div class="col-sm-8">
{% render_field form.access_at type="datetime-local" class+="form-control" %}
{% render_field form.access_at class+="form-control" %}
</div>
</div>
<div class="form-group" data-toggle="tooltip" title="The date/time at which crew should meet for this event">
@@ -337,7 +337,7 @@
class="col-sm-4 control-label">{{ form.meet_at.label }}</label>
<div class="col-sm-8">
{% render_field form.meet_at type="datetime-local" class+="form-control" %}
{% render_field form.meet_at class+="form-control" %}
</div>
</div>
<div class="form-group">
@@ -398,6 +398,7 @@
{% render_field form.collector class+="form-control" %}
</div>
</div>
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
<label for="{{ form.purchase_order.id_for_label }}"
class="col-sm-4 control-label">{{ form.purchase_order.label }}</label>

View File

@@ -54,20 +54,22 @@
<td><a href="{% url 'event_detail' object.pk %}">N{{ object.pk|stringformat:"05d" }}</a><br>
<span class="text-muted">{{ object.get_status_display }}</span></td>
<td>{{ object.start_date }}</td>
<td>{{ object.name }}</td>
<td>
{% if object.organisation %}
{{ object.organisation.name }}
<br>
<span class="text-muted">{{ object.organisation.union_account|yesno:'Internal,External' }}</span>
{% else %}
{{ object.person.name }}
<br>
<span class="text-muted">External</span>
{{ object.name }}
{% if object.is_rig and perms.RIGS.view_event and object.authorised %}
<span class="glyphicon glyphicon-check"></span>
{% endif %}
</td>
<td>{{ object.sum_total|floatformat:2 }}</td>
<td>
{{ object.organisation.name }}
<br>
<span class="text-muted">{{ object.internal|yesno:'Internal,External' }}</span>
</td>
<td>
{{ object.sum_total|floatformat:2 }}
<br />
<span class="text-muted">{% if not object.internal %}{{ object.purchase_order }}{% endif %}</span>
</td>
<td class="text-center">
{% if object.mic %}
{{ object.mic.initials }}<br>
@@ -77,7 +79,10 @@
{% endif %}
</td>
<td class="text-right">
<a href="{% url 'invoice_event' object.pk %}" class="btn btn-default" data-toggle="tooltip" title="'Invoice' this event - click this when paperwork has been sent to treasury">
<a href="{% url 'invoice_event' object.pk %}"
class="btn btn-default"
data-toggle="tooltip"
title="'Invoice' this event - click this when paperwork has been sent to treasury">
<span class="glyphicon glyphicon-gbp"></span>
</a>
</td>
@@ -92,4 +97,4 @@
</div>
{% endif %}
</div>
{% endblock %}
{% endblock %}

View File

@@ -22,21 +22,21 @@
<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="invoice-head" alignment="center" fontName="OpenSans-Bold" fontSize="16" leading="18" spaceAfter="0"/>
<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.invoice_titles" fontName="OpenSans-Bold" fontSize="10" />
<paraStyle name="style.invoice_numbers" 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="invoiceLayout">
<blockTableStyle id="headLayout">
<blockValign value="top"/>
</blockTableStyle>
@@ -100,10 +100,11 @@
<setFont name="OpenSans" size="10" />
{% if not invoice %}<drawCenteredString x="302.5" y="50">[{{ copy }} Copy]</drawCenteredString>{% endif %}
<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 by {{current_user.name}} | {% now "d/m/Y H:i" %} | {{object.current_version_id}}]</drawCenteredString>
<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"/>
@@ -115,10 +116,11 @@
<image file="RIGS/static/imgs/paperwork/corner-bl.jpg" x="0" y="0" height="200" width="200"/>
<setFont name="OpenSans" size="10"/>
{% if not invoice %}<drawCenteredString x="302.5" y="50">[{{ copy }} Copy]</drawCenteredString>{% endif %}
<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 by {{current_user.name}} | {% now "d/m/Y H:i" %} | {{object.current_version_id}}]</drawCenteredString>
<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>
@@ -128,4 +130,4 @@
{% include "RIGS/event_print_page.xml" %}
</story>
</document>
</document>

View File

@@ -1,59 +1,70 @@
<setNextFrame name="main"/>
<nextFrame/>
{% if invoice %}
<blockTable style="invoiceLayout" colWidths="330,165">
<blockTable style="headLayout" colWidths="330,165">
<tr>
<td>
{% endif %}
<h1><b>N{{ object.pk|stringformat:"05d" }}:</b> '{{ object.name }}'<small></small></h1>
<h1><b>N{{ object.pk|stringformat:"05d" }}:</b> '{{ object.name }}'<small></small></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:""|linebreaksbr }}
<para style="style.event_description">
<b>{{object.start_date|date:"D jS N Y"}}</b>
</para>
</keepInFrame>
{% if invoice %}
<keepInFrame>
<para style="style.event_description">
{{ object.description|default_if_none:""|linebreaksbr }}
</para>
</keepInFrame>
</td>
<td>
<para style="invoice-head">INVOICE</para>
<spacer length="10"/>
<blockTable style="eventDetails" colWidths="100,175">
<tr>
<td><para style="invoice_titles">Invoice Number</para></td>
<td>
<para style="invoice_numbers">{{ invoice.pk|stringformat:"05d" }}</para>
</td>
</tr>
<tr>
<td><para style="invoice_titles">Invoice Date</para></td>
<td>
<para style="invoice_numbers">{{ invoice.invoice_date|date:"d/m/Y" }}</para>
</td>
</tr>
<tr>
<td><para style="invoice_titles">PO Number</para></td>
<td>
<para style="invoice_numbers">{{ object.purchase_order|default_if_none:"" }}</para>
</td>
</tr>
{% if invoice %}
<para style="page-head">INVOICE</para>
<spacer length="10"/>
<blockTable style="eventDetails" colWidths="100,175">
<tr>
<td><para style="head_titles">Invoice Number</para></td>
<td>
<para style="head_numbers">{{ invoice.pk|stringformat:"05d" }}</para>
</td>
</tr>
<tr>
<td><para style="head_titles">Invoice Date</para></td>
<td>
<para style="head_numbers">{{ invoice.invoice_date|date:"d/m/Y" }}</para>
</td>
</tr>
</blockTable>
{% if not object.internal %}
<tr>
<td><para style="head_titles">PO</para></td>
<td><para style="head_numbers">{{ object.purchase_order }}</para></td>
</tr>
{% endif %}
</blockTable>
{% elif quote %}
<para style="page-head">QUOTE</para>
<spacer length="10"/>
<blockTable style="eventDetails" colWidths="100,175">
<tr>
<td><para style="head_titles">Quote Date</para></td>
<td>
<para style="head_numbers">{% now "d/m/Y" %}</para>
</td>
</tr>
</blockTable>
{% elif receipt %}
<para style="page-head">CONFIRMATION</para>
{% endif %}
</td>
</tr>
</blockTable>
{% endif %}
<spacer length="15"/>
<blockTable style="eventSpecifics" colWidths="165,165,165">
<tr>
@@ -189,16 +200,16 @@
<keepTogether>
<blockTable style="totalTable" colWidths="300,115,80">
<tr>
<td>{% if not invoice %}VAT Registration Number: 170734807{% endif %}</td>
<td>{% if quote %}VAT Registration Number: 170734807{% endif %}</td>
<td>Total (ex. VAT)</td>
<td>£ {{ object.sum_total|floatformat:2 }}</td>
</tr>
<tr>
<td>
{% if not invoice %}
<para>
<b>The full hire fee is payable at least 10 days before the event.</b>
</para>
{% if quote %}
<para>
This quote is valid for 30 days unless otherwise arranged.
</para>
{% endif %}
</td>
<td>VAT @ {{ object.vat_rate.as_percent|floatformat:2 }}%</td>
@@ -206,113 +217,140 @@
</tr>
<tr>
<td>
<para>
{% if invoice %}
VAT Registration Number: 170734807
{% if quote %}
<para>
<b>The full hire fee is payable at least 10 days before the event.</b>
</para>
{% endif %}
</td>
{% if invoice %}
<td>Total</td>
<td>£ {{ object.total|floatformat:2 }}</td>
{% else %}
<td>
<para>
<b>Total</b>
</para>
</td>
<td>
<para>
<b>£ {{ object.total|floatformat:2 }}</b>
</para>
</td>
{% endif %}
</tr>
</blockTable>
</keepTogether>
{% if invoice %}
<spacer length="15"/>
<keepTogether>
<h2>Payments</h2>
<blockTable style="itemTable" colWidths="300,115,80">
<tr>
<td>
<para>
<b>Method</b>
</para>
</td>
<td>
<para>
<b>Date</b>
</para>
</td>
<td>
<para>
<b>Amount</b>
</para>
</td>
</tr>
{% for payment in object.invoice.payment_set.all %}
<tr>
<td>{{ payment.get_method_display }}</td>
<td>{{ payment.date }}</td>
<td>£ {{ payment.amount|floatformat:2 }}</td>
</tr>
{% endfor %}
</blockTable>
<blockTable style="totalTable" colWidths="300,115,80">
<tr>
<td></td>
<td>Payment Total</td>
<td>£ {{ object.invoice.payment_total|floatformat:2 }}</td>
</tr>
<tr>
<td></td>
<td>
<para>
<b>Balance</b> (ex. VAT)
</para>
</td>
<td>
<para>
<b>£ {{ object.invoice.balance|floatformat:2 }}</b>
</para>
</td>
</tr>
</blockTable>
</keepTogether>
{% endif %}
<keepTogether>
<blockTable style="infoTable">>
{% if quote %}
<tr><td><spacer length="15" /></td></tr>
<tr>
<td>
{% if object.internal %}
<para>Bookings will
<b>not</b>
be confirmed until the event is authorised online.
</para>
{% else %}
<b>This contract is not an invoice.</b>
<para>Bookings will
<b>not</b>
be confirmed until we have received written confirmation and a Purchase Order.
</para>
{% endif %}
</para>
</td>
</td>
</tr>
<tr>
<td>24 Hour Emergency Contacts: 07825 065681 and 07825 065678</td>
</tr>
{% else %}
<tr>
<td>
<para>
<b>Total</b>
</para>
<para>VAT Registration Number: 170734807</para>
</td>
</tr>
{% endif %}
<tr><td><spacer length="15" /></td></tr>
<tr>
<td>
<para>
<b>£ {{ object.total|floatformat:2 }}</b>
</para>
{% if object.internal and object.authorised %}
<para>
Event authorised online by {{ object.authorisation.name }} ({{ object.authorisation.email }}) at
{{ object.authorisation.last_edited_at }}.
</para>
<blockTable colWidths="165,165,165">
<tr>
<td><para><b>University ID</b></para></td>
<td><para><b>Account Code</b></para></td>
<td><para><b>Authorised Amount</b></para></td>
</tr>
<tr>
<td>{{ object.authorisation.uni_id }}</td>
<td>{{ object.authorisation.account_code }}</td>
<td>£ {{ object.authorisation.amount|floatformat:2 }}</td>
</tr>
</blockTable>
{% endif %}
</td>
</tr>
</blockTable>
</keepTogether>
{% if not invoice %}
<keepTogether>
<blockTable style="infoTable">
<tr>
<td>
<para>Bookings will
<b>not</b>
be confirmed until payment is received and the contract is signed.
</para>
</td>
</tr>
<tr>
<td>24 Hour Emergency Contacts: 07825 065681 and 07825 065678</td>
</tr>
</blockTable>
</keepTogether>
<spacer length="15"/>
<keepTogether>
<para style="blockPara">
<b>To be signed on booking:</b>
</para>
{% if object.organisation.union_account %}
<para style="blockPara">
<i>
I agree that am authorised to sign this invoice. I agree that I am the President/Treasurer of the hirer, or
that I have provided written permission from either the President or Treasurer of the hirer stating that I can
sign for this invoice.
</i>
</para>
<para style="blockPara">
<i>
I have read, understood and fully accepted the current conditions of hire. I agree to return any dry hire
items to TEC PA &amp; Lighting in the same condition at the end of the hire period.
</i>
</para>
<para style="blockPara">
<b>
Conditions of hire attached and available on the TEC PA &amp; Lighting website. E&amp;OE
</b>
</para>
<para style="blockPara">
Please return this form directly to TEC PA &amp; Lighting and not the Students' Union Finance Department.
</para>
<blockTable style="signatureTable" colWidths="70,100,325">
<tr>
<td>Account Code</td>
<td></td>
<td></td>
</tr>
</blockTable>
{% else %}
<para style="blockPara">
<i>
I, the hirer, have read, understand and fully accept the current conditions of hire. This document forms a
binding contract between TEC PA &amp; Lighting and the hirer, the aforementioned conditions of hire forming
an integral part of it.
</i>
</para>
<para style="blockPara">
<b>
Conditions of hire attached and available on the TEC PA &amp; Lighting website. E&amp;OE
</b>
</para>
{% include "RIGS/event_print_signature.xml" %}
<spacer length="10"/>
<para style="blockPara">
<b>To be signed on the day of the event/hire:</b>
</para>
<para style="blockPara">
<i>
I, the hirer, have received the goods/services as requested and in good order. I agree to return any dry hire
items to TEC PA &amp; Lighting in a similar condition at the end of the hire period.
</i>
</para>
{% endif %}
{% include "RIGS/event_print_signature.xml" %}
</keepTogether>
{% endif %}
<namedString id="lastPage"><pageNumber/></namedString>
<namedString id="lastPage"><pageNumber/></namedString>

View File

@@ -33,13 +33,18 @@
</td>
<td>
<h4>
<a {% if perms.RIGS.view_event %}href="{% url 'event_detail' event.pk %}" {% endif %}>{{ event.name }}</a>
<a {% if perms.RIGS.view_event %}href="{% url 'event_detail' event.pk %}" {% endif %}>
{{ event.name }}
</a>
{% if event.venue %}
<small>at {{ event.venue }}</small>
{% endif %}
{% if event.dry_hire %}
<span class="label label-default">Dry Hire</span>
{% endif %}
{% if event.is_rig and perms.RIGS.view_event and event.authorised %}
<span class="glyphicon glyphicon-check"></span>
{% endif %}
</h4>
{% if event.is_rig and not event.cancelled %}
<h5>
@@ -99,4 +104,4 @@
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

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

View File

@@ -0,0 +1,16 @@
Hi {{ to_name|default:"there" }},
{{ request.user.get_full_name }} has requested that you authorise N{{ object.pk|stringformat:"05d" }}| {{ object.name }}{% if not to_name %} on behalf of {{ object.person.name }}{% endif %}.
Please find the link below to complete the event booking process.
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
Remember that only Presidents or Treasurers are allowed to sign off payments. You may need to forward
this
email on.
{% endif %}
{{ request.scheme }}://{{ request.get_host }}{% url 'event_authorise' object.pk hmac %}
Please note you event will not be booked until you complete this form.
TEC PA & Lighting

View File

@@ -0,0 +1,21 @@
{% extends 'base_client_email.html' %}
{% block content %}
<p>Hi {{ to_name|default:"there" }},</p>
<p>
Your event <b>N{{ object.event.pk|stringformat:"05d" }}</b> has been successfully authorised
for <b>&pound;{{ object.amount }}</b>
by <b>{{ object.name }}</b> as of <b>{{ object.last_edited_at }}</b>.
</p>
<p>
{% if object.event.organisation and object.event.organisation.union_account %}{# internal #}
Your event is now fully booked and payment will be processed by the finance department automatically.
{% else %}{# external #}
Your event is now fully booked and our finance department will be contact to arrange payment.
{% endif %}
</p>
<p>TEC PA &amp; Lighting</p>
{% endblock %}

View File

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

View File

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

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.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.last_edited_at}}.
The TEC Rig Information Gathering System

View File

@@ -0,0 +1,59 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
{% load widget_tweaks %}
{% block title %}Request Authorisation{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<div class="alert alert-warning">
<h1>Send authorisation request email.</h1>
<p>Pressing send will email the address provided. Please triple check everything before continuing.</p>
</div>
<div class="alert alert-info">
<dl class="dl-horizontal">
<dt>Person Email</dt>
<dd>{{ object.person.email }}</dd>
<dt>Organisation Email</dt>
<dd>{{ object.organisation.email }}</dd>
</dl>
</div>
</div>
<div class="col-sm-12 col-md-10 col-md-offset-1">
<form action="{{ form.action|default:request.path }}" method="post" class="form-horizontal"
id="auth-request-form">
{% csrf_token %}
<div class="row">
{% include 'form_errors.html' %}
<div class="form-group">
<label class="col-sm-3 control-label"
for="{{ form.email.id_for_label }}">{{ form.email.label }}</label>
<div class="col-sm-9">
{% render_field form.email type="email" class+="form-control" %}
</div>
</div>
<div class="text-right col-sm-3 col-sm-offset-9">
<div class="form-group">
<button type="submit" class="form-control btn btn-primary">
<span class="glyphicon glyphicon-send"></span>
Send
</button>
</div>
</div>
</div>
</form>
</div>
</div>
<script>
$('#auth-request-form').on('submit', function () {
$('#auth-request-form button').attr('disabled', true);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends request.is_ajax|yesno:'base_ajax.html,base.html' %}
{% load widget_tweaks %}
{% block title %}NottinghamTEC Email Address Required{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<div class="alert alert-warning">
<h1>An error occured.</h1>
<p>Your RIGS account must have an @nottinghamtec.co.uk email address before you can send emails to clients.</p>
</div>
</div>
</div>
{% endblock %}

View File

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

View File

@@ -23,7 +23,7 @@
<div class="list-group-item default"></div>
<a class="list-group-item" href="//members.nottinghamtec.co.uk/forum" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Forum</a>
<a class="list-group-item" href="https://forum.nottinghamtec.co.uk" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Forum</a>
<a class="list-group-item" href="//members.nottinghamtec.co.uk/wiki" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Wiki</a>
<a class="list-group-item" href="http://members.nottinghamtec.co.uk/wiki/images/2/22/Event_Risk_Assesment.pdf" target="_blank"><span class="glyphicon glyphicon-link"></span> Pre-Event Risk Assessment</a>
<a class="list-group-item" href="//members.nottinghamtec.co.uk/price" target="_blank"><span class="glyphicon glyphicon-link"></span> Price List</a>

View File

@@ -11,7 +11,7 @@
<div class="col-sm-4 text-right">
<div class="btn-group btn-page">
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-default" title="Void Invoice">
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-default" title="Delete Invoice">
<span class="glyphicon glyphicon-remove"></span> <span
class="hidden-xs">Delete</span>
</a>
@@ -19,7 +19,7 @@
<span class="glyphicon glyphicon-ban-circle"></span> <span
class="hidden-xs">Void</span>
</a>
<a href="{% url 'invoice_print' object.pk %}" target="_blank" class="btn btn-default"><span
<a href="{% url 'invoice_print' object.pk %}" target="_blank" title="Print Invoice" class="btn btn-default"><span
class="glyphicon glyphicon-print"></span> <span
class="hidden-xs">Print</span></a>
</div>
@@ -76,8 +76,43 @@
<dd>{{ object.checked_in_by.name }}</dd>
{% endif %}
<dt>PO</dt>
<dd>{{ object.event.purchase_order }}</dd>
<dd>&nbsp;</dd>
<dt>Authorised</dt>
<dd>{{ object.event.authorised|yesno:"Yes,No" }}</dd>
<dt>Authorised by</dt>
<dd>
{% if object.event.authorised %}
{{ object.event.authorisation.name }}
(<a href="mailto:{{ object.event.authorisation.email }}">{{ object.event.authorisation.email }}</a>)
{% endif %}
</dd>
{% if object.event.internal %}
{# internal #}
<dt>Uni ID</dt>
<dd>{{ object.event.authorisation.uni_id }}</dd>
<dt>Account code</dt>
<dd>{{ object.event.authorisation.account_code }}</dd>
{% else %}
<dt>PO</dt>
<dd>{{ object.event.purchase_order }}</dd>
{% endif %}
<dt>Authorised at</dt>
<dd>{{ object.event.authorisation.last_edited_at }}</dd>
<dt>Authorised amount</dt>
<dd>
{% if object.event.authorised %}
£ {{ object.event.authorisation.amount|floatformat:"2" }}
{% endif %}
</dd>
<dt>Authorsation request sent by</dt>
<dd>{{ object.authorisation.sent_by }}</dd>
</dl>
</div>
</div>
@@ -139,4 +174,4 @@
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -50,7 +50,7 @@
<td>{% if object.event.organisation %}
{{ object.event.organisation.name }}
<br>
<span class="text-muted">{{ object.event.organisation.union_account|yesno:'Internal,External' }}</span>
<span class="text-muted">{{ object.event.internal|yesno:'Internal,External' }}</span>
{% else %}
{{ object.event.person.name }}
<br>
@@ -59,7 +59,11 @@
</td>
<td>{{ object.event.start_date }}</td>
<td>{{ object.invoice_date }}</td>
<td>{{ object.balance|floatformat:2 }}</td>
<td>
{{ object.balance|floatformat:2 }}
<br />
<span class="text-muted">{{ object.event.purchase_order }}</span>
</td>
<td class="text-right">
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default">
<span class="glyphicon glyphicon-pencil"></span>
@@ -76,4 +80,4 @@
</div>
{% endif %}
</div>
{% endblock %}
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% load to_class_name from filters %}
{# pass in variable "object" to this template #}
<a title="{% if object.is_rig == False %}Non-rig{% elif object.dry_hire %}Dry Hire{% elif object.is_rig %}Rig{%else%}{{object|to_class_name}}{% endif %} | '{{object.name}}'" href="{{ object.get_absolute_url }}">{% if object.is_rig == False %}Non-rig{% elif object.dry_hire %}Dry Hire{% elif object.is_rig %}Rig{%else%}{{object|to_class_name}}{% endif %} | '{{object.name}}'</a>
<a title="{% if object.is_rig == False %}Non-rig{% elif object.dry_hire %}Dry Hire{% elif object.is_rig %}Rig{%else%}{{object|to_class_name}}{% endif %} | '{{object.name}}'" href="{{ object.get_absolute_url }}">{% if object.is_rig == False %}Non-rig{% elif object.dry_hire %}Dry Hire{% elif object.is_rig %}Rig{%else%}{{object|to_class_name}}{% endif %} | '{{ object.activity_feed_string|default:object.name }}'</a>

View File

@@ -16,7 +16,7 @@
for="{{ form.date.id_for_label }}">{{ form.date.label }}</label>
<div class="col-sm-10">
{% render_field form.date type="date" class+="form-control" %}
{% render_field form.date class+="form-control" %}
</div>
</div>

View File

@@ -1,21 +1,26 @@
{% for change in version.field_changes %}
{% if version.changes.item_changes or version.changes.field_changes or version.changes.old == None %}
<button title="Changes to {{ change.field.verbose_name }}" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover' data-toggle="popover" data-content='{% spaceless %}
{% include "RIGS/version_changes_change.html" %}
{% endspaceless %}'>{{ change.field.verbose_name }}</button>
{% for change in version.changes.field_changes %}
{% endfor %}
<button title="Changes to {{ change.field.verbose_name }}" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover' data-toggle="popover" data-content='{% spaceless %}
{% include "RIGS/version_changes_change.html" %}
{% endspaceless %}'>{{ change.field.verbose_name }}</button>
{% for itemChange in version.item_changes %}
<button title="Changes to item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover' data-toggle="popover" data-content='{% spaceless %}
<ul class="list-group">
{% for change in itemChange.changes %}
<li class="list-group-item">
<h4 class="list-group-item-heading">{{ change.field.verbose_name }}</h4>
{% include "RIGS/version_changes_change.html" %}
</li>
{% endfor %}
</ul>
{% endspaceless %}'>item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'</button>
{% endfor %}
{% endfor %}
{% for itemChange in version.changes.item_changes %}
<button title="Changes to item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover' data-toggle="popover" data-content='{% spaceless %}
<ul class="list-group">
{% for change in itemChange.field_changes %}
<li class="list-group-item">
<h4 class="list-group-item-heading">{{ change.field.verbose_name }}</h4>
{% include "RIGS/version_changes_change.html" %}
</li>
{% endfor %}
</ul>
{% endspaceless %}'>item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'</button>
{% endfor %}
{% else %}
nothing useful
{% endif %}

View File

@@ -40,13 +40,13 @@
</thead>
<tbody>
{% for version in object_list %}
{% if version.item_changes or version.field_changes or version.old == None %}
<tr>
<td>{{ version.revision.date_created }}</td>
<td>{{ version.version.pk }}|{{ version.revision.pk }}</td>
<td>{{ version.pk }}|{{ version.revision.pk }}</td>
<td>{{ version.revision.user.name }}</td>
<td>
{% if version.old == None %}
{% if version.changes.old is None %}
{{object|to_class_name}} Created
{% else %}
{% include 'RIGS/version_changes.html' %}
@@ -56,7 +56,8 @@
{{ version.revision.comment }}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>

View File

@@ -5,19 +5,22 @@ from django.forms.utils import ErrorDict
register = template.Library()
@register.filter
def multiply(value, arg):
return value*arg
return value * arg
@register.filter
def to_class_name(value):
return value.__class__.__name__
@register.filter
def nice_errors(form, non_field_msg='General form errors'):
nice_errors = ErrorDict()
if isinstance(form, forms.BaseForm):
for field, errors in form.errors.items():
for field, errors in list(form.errors.items()):
if field == NON_FIELD_ERRORS:
key = non_field_msg
else:
@@ -25,6 +28,7 @@ def nice_errors(form, non_field_msg='General form errors'):
nice_errors[key] = errors
return nice_errors
def paginator(context, adjacent_pages=3):
"""
To be used in conjunction with the object_list generic view.
@@ -37,11 +41,13 @@ def paginator(context, adjacent_pages=3):
page = context['page_obj']
paginator = context['paginator']
startPage = max(page.number - adjacent_pages, 1)
if startPage <= 3: startPage = 1
if startPage <= 3:
startPage = 1
endPage = page.number + adjacent_pages + 1
if endPage >= paginator.num_pages - 1: endPage = paginator.num_pages + 1
page_numbers = [n for n in range(startPage, endPage) \
if n > 0 and n <= paginator.num_pages]
if endPage >= paginator.num_pages - 1:
endPage = paginator.num_pages + 1
page_numbers = [n for n in range(startPage, endPage)
if n > 0 and n <= paginator.num_pages]
dict = {
'request': context['request'],
@@ -57,15 +63,18 @@ def paginator(context, adjacent_pages=3):
'has_next': page.has_next(),
'has_previous': page.has_previous(),
}
if page.has_next():
dict['next'] = page.next_page_number()
if page.has_previous():
dict['previous'] = page.previous_page_number()
return dict
register.inclusion_tag('pagination.html', takes_context=True)(paginator)
@register.simple_tag
def url_replace(request, field, value):
@@ -75,6 +84,7 @@ def url_replace(request, field, value):
return dict_.urlencode()
@register.simple_tag
def orderby(request, field, attr):
@@ -88,4 +98,4 @@ def orderby(request, field, attr):
else:
dict_[field] = attr
return dict_.urlencode()
return dict_.urlencode()

View File

@@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
import os
import re
from datetime import date, timedelta
import pytz
from datetime import date, time, datetime, timedelta
import reversion
from django.core import mail
from django.db import transaction
from django.test import LiveServerTestCase
from django.http import HttpResponseBadRequest
from django.test import LiveServerTestCase, TestCase
from django.test.client import Client
from selenium import webdriver
from selenium.common.exceptions import StaleElementReferenceException, WebDriverException
@@ -15,12 +17,31 @@ from selenium.webdriver.support.ui import WebDriverWait
from RIGS import models
from reversion import revisions as reversion
from django.urls import reverse
from django.core import mail, signing
from django.conf import settings
import sys
def create_browser():
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
if os.environ.get('CI', False):
options.add_argument("--headless")
options.add_argument("--no-sandbox")
driver = webdriver.Chrome(chrome_options=options)
return driver
class UserRegistrationTest(LiveServerTestCase):
def setUp(self):
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3) # Set implicit wait session wide
self.browser = create_browser()
self.browser.implicitly_wait(3) # Set implicit wait session wide
os.environ['RECAPTCHA_TESTING'] = 'True'
def tearDown(self):
@@ -149,18 +170,18 @@ class UserRegistrationTest(LiveServerTestCase):
class EventTest(LiveServerTestCase):
def setUp(self):
self.profile = models.Profile(
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
self.profile.set_password("EventTestPassword")
self.profile.save()
self.vatrate = models.VatRate.objects.create(start_at='2014-03-05',rate=0.20,comment='test1')
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3) # Set implicit wait session wide
self.browser.maximize_window()
self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
self.browser = create_browser()
self.browser.implicitly_wait(10) # Set implicit wait session wide
# self.browser.maximize_window()
os.environ['RECAPTCHA_TESTING'] = 'True'
def tearDown(self):
@@ -203,7 +224,7 @@ class EventTest(LiveServerTestCase):
# Gets redirected to login and back
self.authenticate('/event/create/')
wait = WebDriverWait(self.browser, 10) #setup WebDriverWait to use later (to wait for animations)
wait = WebDriverWait(self.browser, 3) # setup WebDriverWait to use later (to wait for animations)
wait.until(animation_is_finished())
@@ -358,11 +379,11 @@ class EventTest(LiveServerTestCase):
self.assertEqual(obj.pk, int(option.get_attribute("value")))
# Set start date/time
form.find_element_by_id('id_start_date').send_keys('3015-05-25')
form.find_element_by_id('id_start_date').send_keys('25/05/3015')
form.find_element_by_id('id_start_time').send_keys('06:59')
# Set end date/time
form.find_element_by_id('id_end_date').send_keys('4000-06-27')
form.find_element_by_id('id_end_date').send_keys('27/06/4000')
form.find_element_by_id('id_end_time').send_keys('07:00')
# Add item
@@ -370,29 +391,31 @@ class EventTest(LiveServerTestCase):
wait.until(animation_is_finished())
modal = self.browser.find_element_by_id("itemModal")
modal.find_element_by_id("item_name").send_keys("Test Item 1")
modal.find_element_by_id("item_description").send_keys("This is an item description\nthat for reasons unkown spans two lines")
modal.find_element_by_id("item_description").send_keys(
"This is an item description\nthat for reasons unkown spans two lines")
e = modal.find_element_by_id("item_quantity")
e.click()
e.send_keys(Keys.UP)
e.send_keys(Keys.UP)
e = modal.find_element_by_id("item_cost")
e.send_keys("23.95")
e.send_keys(Keys.ENTER) # enter submit
e.send_keys(Keys.ENTER) # enter submit
# Confirm item has been saved to json field
objectitems = self.browser.execute_script("return objectitems;")
self.assertEqual(1, len(objectitems))
testitem = objectitems["-1"]['fields'] # as we are deliberately creating this we know the ID
testitem = objectitems["-1"]['fields'] # as we are deliberately creating this we know the ID
self.assertEqual("Test Item 1", testitem['name'])
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
# See new item appear in table
row = self.browser.find_element_by_id('item--1') # ID number is known, see above
row = self.browser.find_element_by_id('item--1') # ID number is known, see above
self.assertIn("Test Item 1", row.find_element_by_xpath('//span[@class="name"]').text)
self.assertIn("This is an item description", row.find_element_by_xpath('//div[@class="item-description"]').text)
self.assertEqual(u'£ 23.95', row.find_element_by_xpath('//tr[@id="item--1"]/td[2]').text)
self.assertIn("This is an item description",
row.find_element_by_xpath('//div[@class="item-description"]').text)
self.assertEqual('£ 23.95', row.find_element_by_xpath('//tr[@id="item--1"]/td[2]').text)
self.assertEqual("2", row.find_element_by_xpath('//td[@class="quantity"]').text)
self.assertEqual(u'£ 47.90', row.find_element_by_xpath('//tr[@id="item--1"]/td[4]').text)
self.assertEqual('£ 47.90', row.find_element_by_xpath('//tr[@id="item--1"]/td[4]').text)
# Check totals
self.assertEqual("47.90", self.browser.find_element_by_id('sumtotal').text)
@@ -415,7 +438,7 @@ class EventTest(LiveServerTestCase):
self.assertFalse(error.is_displayed())
except StaleElementReferenceException:
pass
except:
except BaseException:
self.assertFail("Element does not appear to have been deleted")
# Check at least some data is preserved. Some = all will be there
@@ -431,92 +454,111 @@ class EventTest(LiveServerTestCase):
# See redirected to success page
successTitle = self.browser.find_element_by_xpath('//h1').text
event = models.Event.objects.get(name='Test Event Name')
self.assertIn("N0000%d | Test Event Name"%event.pk, successTitle)
self.assertIn("N%05d | Test Event Name" % event.pk, successTitle)
except WebDriverException:
# This is a dirty workaround for wercker being a bit funny and not running it correctly.
# Waiting for wercker to get back to me about this
pass
def testEventDuplicate(self):
testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, start_date=date.today() + timedelta(days=6), description="start future no end", purchase_order="TESTPO")
testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL,
start_date=date.today() + timedelta(days=6),
description="start future no end",
purchase_order='TESTPO',
auth_request_by=self.profile,
auth_request_at=self.create_datetime(2015, 0o6, 0o4, 10, 00),
auth_request_to="some@email.address")
item1 = models.EventItem(
event=testEvent,
name="Test Item 1",
cost="10.00",
quantity="1",
order=1
).save()
event=testEvent,
name="Test Item 1",
cost="10.00",
quantity="1",
order=1
).save()
item2 = models.EventItem(
event=testEvent,
name="Test Item 2",
description="Foo",
cost="9.72",
quantity="3",
order=2,
).save()
event=testEvent,
name="Test Item 2",
description="Foo",
cost="9.72",
quantity="3",
order=2,
).save()
self.browser.get(self.live_server_url + '/event/' + str(testEvent.pk) + '/duplicate/')
self.authenticate('/event/' + str(testEvent.pk) + '/duplicate/')
wait = WebDriverWait(self.browser, 10) #setup WebDriverWait to use later (to wait for animations)
wait = WebDriverWait(self.browser, 3) # setup WebDriverWait to use later (to wait for animations)
save = self.browser.find_element_by_xpath(
'(//button[@type="submit"])[3]')
form = self.browser.find_element_by_tag_name('form')
# Check the items are visible
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
self.assertIn("Test Item 1", table.text)
self.assertIn("Test Item 2", table.text)
# Check the info message is visible
self.assertIn("Event data duplicated but not yet saved",self.browser.find_element_by_id('content').text)
self.assertIn("Event data duplicated but not yet saved", self.browser.find_element_by_id('content').text)
# Add item
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
wait.until(animation_is_finished())
modal = self.browser.find_element_by_id("itemModal")
modal.find_element_by_id("item_name").send_keys("Test Item 3")
modal.find_element_by_id("item_description").send_keys("This is an item description\nthat for reasons unkown spans two lines")
modal.find_element_by_id("item_description").send_keys(
"This is an item description\nthat for reasons unkown spans two lines")
e = modal.find_element_by_id("item_quantity")
e.click()
e.send_keys(Keys.UP)
e.send_keys(Keys.UP)
e = modal.find_element_by_id("item_cost")
e.send_keys("23.95")
e.send_keys(Keys.ENTER) # enter submit
e.send_keys(Keys.ENTER) # enter submit
# Attempt to save
save.click()
self.assertNotIn("N0000%d"%testEvent.pk, self.browser.find_element_by_xpath('//h1').text)
self.assertNotIn("Event data duplicated but not yet saved", self.browser.find_element_by_id('content').text) # Check info message not visible
newEvent = models.Event.objects.latest('pk')
self.assertEqual(newEvent.auth_request_to, None)
self.assertEqual(newEvent.auth_request_by, None)
self.assertEqual(newEvent.auth_request_at, None)
self.assertFalse(hasattr(newEvent, 'authorised'))
self.assertNotIn("N%05d" % testEvent.pk, self.browser.find_element_by_xpath('//h1').text)
self.assertNotIn("Event data duplicated but not yet saved", self.browser.find_element_by_id('content').text) # Check info message not visible
# Check the new items are visible
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
self.assertIn("Test Item 1", table.text)
self.assertIn("Test Item 2", table.text)
self.assertIn("Test Item 3", table.text)
infoPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
self.assertIn("N0000%d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
self.assertIn("N0000%d" % testEvent.pk,
infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
# Check the PO hasn't carried through
self.assertNotIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
self.assertIn("N%05d" % testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
self.browser.get(self.live_server_url + '/event/' + str(testEvent.pk)) # Go back to the old event
self.browser.get(self.live_server_url + '/event/' + str(testEvent.pk)) #Go back to the old event
#Check that based-on hasn't crept into the old event
# Check that based-on hasn't crept into the old event
infoPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
self.assertNotIn("N0000%d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
self.assertNotIn("N0000%d" % testEvent.pk,
infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
# Check the PO remains on the old event
self.assertIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
self.assertNotIn("N%05d" % testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
# Check the items are as they were
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
self.assertIn("Test Item 1", table.text)
self.assertIn("Test Item 2", table.text)
self.assertNotIn("Test Item 3", table.text)
@@ -526,7 +568,7 @@ class EventTest(LiveServerTestCase):
# Gets redirected to login and back
self.authenticate('/event/create/')
wait = WebDriverWait(self.browser, 10) #setup WebDriverWait to use later (to wait for animations)
wait = WebDriverWait(self.browser, 3) # setup WebDriverWait to use later (to wait for animations)
wait.until(animation_is_finished())
@@ -541,32 +583,29 @@ class EventTest(LiveServerTestCase):
e.send_keys('Test Event Name')
# Both dates, no times, end before start
form.find_element_by_id('id_start_date').clear()
form.find_element_by_id('id_start_date').send_keys('3015-04-24')
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
form.find_element_by_id('id_end_date').clear()
form.find_element_by_id('id_end_date').send_keys('3015-04-23')
self.browser.execute_script("document.getElementById('id_end_date').value='3015-04-23'")
# Attempt to save - should fail
wait.until(animation_is_finished())
save.click()
error = self.browser.find_element_by_xpath('//div[contains(@class, "alert-danger")]')
self.assertTrue(error.is_displayed())
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
# Same date, end time before start time
form = self.browser.find_element_by_tag_name('form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
form.find_element_by_id('id_start_date').clear()
form.find_element_by_id('id_start_date').send_keys('3015-04-24')
form.find_element_by_id('id_end_date').clear()
form.find_element_by_id('id_end_date').send_keys('3015-04-23')
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
self.browser.execute_script("document.getElementById('id_end_date').value='3015-04-23'")
form.find_element_by_id('id_start_time').clear()
form.find_element_by_id('id_start_time').send_keys(Keys.DELETE)
form.find_element_by_id('id_start_time').send_keys('06:59')
form.find_element_by_id('id_end_time').clear()
form.find_element_by_id('id_end_time').send_keys(Keys.DELETE)
form.find_element_by_id('id_end_time').send_keys('06:00')
# Attempt to save - should fail
@@ -575,35 +614,30 @@ class EventTest(LiveServerTestCase):
self.assertTrue(error.is_displayed())
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
# Same date, end time before start time
form = self.browser.find_element_by_tag_name('form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
form.find_element_by_id('id_start_date').clear()
form.find_element_by_id('id_start_date').send_keys('3015-04-24')
form.find_element_by_id('id_end_date').clear()
form.find_element_by_id('id_end_date').send_keys('3015-04-23')
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
self.browser.execute_script("document.getElementById('id_end_date').value='3015-04-24'")
form.find_element_by_id('id_start_time').clear()
form.find_element_by_id('id_start_time').send_keys(Keys.DELETE)
form.find_element_by_id('id_start_time').send_keys('06:59')
form.find_element_by_id('id_end_time').clear()
form.find_element_by_id('id_end_time').send_keys(Keys.DELETE)
form.find_element_by_id('id_end_time').send_keys('06:00')
# No end date, end time before start time
form = self.browser.find_element_by_tag_name('form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
form.find_element_by_id('id_start_date').clear()
form.find_element_by_id('id_start_date').send_keys('3015-04-24')
form.find_element_by_id('id_end_date').clear()
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
self.browser.execute_script("document.getElementById('id_end_date').value=''")
form.find_element_by_id('id_start_time').clear()
form.find_element_by_id('id_start_time').send_keys(Keys.DELETE)
form.find_element_by_id('id_start_time').send_keys('06:59')
form.find_element_by_id('id_end_time').clear()
form.find_element_by_id('id_end_time').send_keys(Keys.DELETE)
form.find_element_by_id('id_end_time').send_keys('06:00')
# Attempt to save - should fail
@@ -612,35 +646,31 @@ class EventTest(LiveServerTestCase):
self.assertTrue(error.is_displayed())
self.assertIn("can't finish before it has started", error.find_element_by_xpath('//dd[1]/ul/li').text)
# 2 dates, end after start
form = self.browser.find_element_by_tag_name('form')
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[3]')
form.find_element_by_id('id_start_date').clear()
form.find_element_by_id('id_start_date').send_keys('3015-04-24')
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
self.browser.execute_script("document.getElementById('id_end_date').value='3015-04-26'")
form.find_element_by_id('id_end_date').clear()
form.find_element_by_id('id_end_date').send_keys('3015-04-26')
self.browser.execute_script("document.getElementById('id_start_time').value=''")
self.browser.execute_script("document.getElementById('id_end_time').value=''")
form.find_element_by_id('id_start_time').clear()
form.find_element_by_id('id_end_time').clear()
# Attempt to save - should succeed
save.click()
# See redirected to success page
successTitle = self.browser.find_element_by_xpath('//h1').text
event = models.Event.objects.get(name='Test Event Name')
self.assertIn("N0000%d | Test Event Name"%event.pk, successTitle)
self.assertIn("N%05d | Test Event Name" % event.pk, successTitle)
def testRigNonRig(self):
self.browser.get(self.live_server_url + '/event/create/')
# Gets redirected to login and back
self.authenticate('/event/create/')
wait = WebDriverWait(self.browser, 10) #setup WebDriverWait to use later (to wait for animations)
self.browser.implicitly_wait(3) #Set session-long wait (only works for non-existant DOM objects)
wait = WebDriverWait(self.browser, 3) # setup WebDriverWait to use later (to wait for animations)
self.browser.implicitly_wait(3) # Set session-long wait (only works for non-existant DOM objects)
wait.until(animation_is_finished())
@@ -658,10 +688,10 @@ class EventTest(LiveServerTestCase):
e.send_keys('Test Event Name')
# Set an arbitrary date
form.find_element_by_id('id_start_date').clear()
form.find_element_by_id('id_start_date').send_keys('3015-04-24')
self.browser.execute_script("document.getElementById('id_start_date').value='3015-04-24'")
# Save the rig
wait.until(animation_is_finished())
save.click()
detail_panel = self.browser.find_element_by_xpath("//div[@id='content']/div/div[6]/div/div")
self.assertTrue(detail_panel.is_displayed())
@@ -672,7 +702,8 @@ class EventTest(LiveServerTestCase):
person = models.Person(name="Event Detail Person", email="eventdetail@person.tests.rigs", phone="123 123")
person.save()
with transaction.atomic(), reversion.create_revision():
organisation = models.Organisation(name="Event Detail Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456").save()
organisation = models.Organisation(name="Event Detail Organisation",
email="eventdetail@organisation.tests.rigs", phone="123 456").save()
with transaction.atomic(), reversion.create_revision():
venue = models.Venue(name="Event Detail Venue").save()
with transaction.atomic(), reversion.create_revision():
@@ -702,59 +733,145 @@ class EventTest(LiveServerTestCase):
order=2,
).save()
self.browser.get(self.live_server_url + '/event/%d'%event.pk)
self.authenticate('/event/%d/'%event.pk)
self.assertIn("N%05d | %s"%(event.pk, event.name), self.browser.find_element_by_xpath('//h1').text)
self.browser.get(self.live_server_url + '/event/%d' % event.pk)
self.authenticate('/event/%d/' % event.pk)
self.assertIn("N%05d | %s" % (event.pk, event.name), self.browser.find_element_by_xpath('//h1').text)
personPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..')
self.assertEqual(person.name, personPanel.find_element_by_xpath('//dt[text()="Person"]/following-sibling::dd[1]').text)
self.assertEqual(person.email, personPanel.find_element_by_xpath('//dt[text()="Email"]/following-sibling::dd[1]').text)
self.assertEqual(person.phone, personPanel.find_element_by_xpath('//dt[text()="Phone Number"]/following-sibling::dd[1]').text)
self.assertEqual(person.name,
personPanel.find_element_by_xpath('//dt[text()="Person"]/following-sibling::dd[1]').text)
self.assertEqual(person.email,
personPanel.find_element_by_xpath('//dt[text()="Email"]/following-sibling::dd[1]').text)
self.assertEqual(person.phone,
personPanel.find_element_by_xpath('//dt[text()="Phone Number"]/following-sibling::dd[1]').text)
organisationPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Contact Details")]/..')
class IcalTest(LiveServerTestCase):
def testEventEdit(self):
person = models.Person(name="Event Edit Person", email="eventdetail@person.tests.rigs", phone="123 123").save()
organisation = models.Organisation(name="Event Edit Organisation", email="eventdetail@organisation.tests.rigs", phone="123 456").save()
venue = models.Venue(name="Event Detail Venue").save()
eventData = {
'name': "Detail Test",
'description': "This is an event to test the detail view",
'notes': "It is going to be awful",
'person': person,
'organisation': organisation,
'venue': venue,
'mic': self.profile,
'start_date': date(2015, 0o6, 0o4),
'end_date': date(2015, 0o6, 0o5),
'start_time': time(10, 00),
'end_time': time(15, 00),
'meet_at': self.create_datetime(2015, 0o6, 0o4, 10, 00),
'access_at': self.create_datetime(2015, 0o6, 0o4, 10, 00),
'collector': 'A Person'
}
event = models.Event(**eventData)
event.save()
item1Data = {
'event': event,
'name': "Detail Item 1",
'cost': "10.00",
'quantity': "1",
'order': 1
}
models.EventItem(**item1Data).save()
self.browser.get(self.live_server_url + '/event/%d/edit/' % event.pk)
self.authenticate('/event/%d/edit/' % event.pk)
save = self.browser.find_element_by_xpath('(//button[@type="submit"])[1]')
save.click()
successTitle = self.browser.find_element_by_xpath('//h1').text
self.assertIn("N%05d | Detail Test" % event.pk, successTitle)
reloadedEvent = models.Event.objects.get(name='Detail Test')
reloadedItem = models.EventItem.objects.get(name='Detail Item 1')
# Check the event
for key, value in eventData.items():
self.assertEqual(str(getattr(reloadedEvent, key)), str(value))
# Check the item
for key, value in item1Data.items():
self.assertEqual(str(getattr(reloadedItem, key)), str(value))
def create_datetime(self, year, month, day, hour, min):
tz = pytz.timezone(settings.TIME_ZONE)
return tz.localize(datetime(year, month, day, hour, min)).astimezone(pytz.utc)
class IcalTest(LiveServerTestCase):
def setUp(self):
self.all_events = set(range(1, 18))
self.current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18)
self.not_current_events = set(self.all_events) - set(self.current_events)
self.vatrate = models.VatRate.objects.create(start_at='2014-03-05',rate=0.20,comment='test1')
self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
self.profile = models.Profile(
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
self.profile.set_password("EventTestPassword")
self.profile.save()
# produce 7 normal events - 5 current - 1 last week - 1 two years ago - 2 provisional - 2 confirmed - 3 booked
models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, start_date=date.today() + timedelta(days=6), description="start future no end")
models.Event.objects.create(name="TE E2", status=models.Event.PROVISIONAL, start_date=date.today(), description="start today no end")
models.Event.objects.create(name="TE E3", status=models.Event.CONFIRMED, start_date=date.today(), end_date=date.today(), description="start today with end today")
models.Event.objects.create(name="TE E4", status=models.Event.CONFIRMED, start_date=date.today()-timedelta(weeks=104), description="start past 2 years no end")
models.Event.objects.create(name="TE E5", status=models.Event.BOOKED, start_date=date.today()-timedelta(days=7), end_date=date.today()-timedelta(days=1), description="start past 1 week with end past")
models.Event.objects.create(name="TE E6", status=models.Event.BOOKED, start_date=date.today()-timedelta(days=2), end_date=date.today()+timedelta(days=2), description="start past, end future")
models.Event.objects.create(name="TE E7", status=models.Event.BOOKED, start_date=date.today()+timedelta(days=2), end_date=date.today()+timedelta(days=2), description="start + end in future")
models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL,
start_date=date.today() + timedelta(days=6), description="start future no end")
models.Event.objects.create(name="TE E2", status=models.Event.PROVISIONAL, start_date=date.today(),
description="start today no end")
models.Event.objects.create(name="TE E3", status=models.Event.CONFIRMED, start_date=date.today(),
end_date=date.today(), description="start today with end today")
models.Event.objects.create(name="TE E4", status=models.Event.CONFIRMED,
start_date=date.today() - timedelta(weeks=104),
description="start past 2 years no end")
models.Event.objects.create(name="TE E5", status=models.Event.BOOKED,
start_date=date.today() - timedelta(days=7),
end_date=date.today() - timedelta(days=1),
description="start past 1 week with end past")
models.Event.objects.create(name="TE E6", status=models.Event.BOOKED,
start_date=date.today() - timedelta(days=2),
start_time=time(8, 00),
end_date=date.today() + timedelta(days=2),
end_time=time(23, 00), description="start past, end future")
models.Event.objects.create(name="TE E7", status=models.Event.BOOKED,
start_date=date.today() + timedelta(days=2),
end_date=date.today() + timedelta(days=2), description="start + end in future")
# 2 cancelled - 1 current
models.Event.objects.create(name="TE E8", start_date=date.today()+timedelta(days=2), end_date=date.today()+timedelta(days=2), status=models.Event.CANCELLED, description="cancelled in future")
models.Event.objects.create(name="TE E9", start_date=date.today()-timedelta(days=1), end_date=date.today()+timedelta(days=2), status=models.Event.CANCELLED, description="cancelled and started")
models.Event.objects.create(name="TE E8", start_date=date.today() + timedelta(days=2),
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
description="cancelled in future")
models.Event.objects.create(name="TE E9", start_date=date.today() - timedelta(days=1),
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
description="cancelled and started")
# 5 dry hire - 3 current - 1 cancelled
models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True, description="dryhire today")
models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True, checked_in_by=self.profile, description="dryhire today, checked in")
models.Event.objects.create(name="TE E12", start_date=date.today()-timedelta(days=1), dry_hire=True, status=models.Event.BOOKED, description="dryhire past")
models.Event.objects.create(name="TE E13", start_date=date.today()-timedelta(days=2), dry_hire=True, checked_in_by=self.profile, description="dryhire past checked in")
models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True, status=models.Event.CANCELLED, description="dryhire today cancelled")
models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True, checked_in_by=self.profile,
description="dryhire today, checked in")
models.Event.objects.create(name="TE E12", start_date=date.today() - timedelta(days=1), dry_hire=True,
status=models.Event.BOOKED, description="dryhire past")
models.Event.objects.create(name="TE E13", start_date=date.today() - timedelta(days=2), dry_hire=True,
checked_in_by=self.profile, description="dryhire past checked in")
models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True,
status=models.Event.CANCELLED, description="dryhire today cancelled")
# 4 non rig - 3 current
models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False, description="non rig today")
models.Event.objects.create(name="TE E16", start_date=date.today()+timedelta(days=1), is_rig=False, description="non rig tomorrow")
models.Event.objects.create(name="TE E17", start_date=date.today()-timedelta(days=1), is_rig=False, description="non rig yesterday")
models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False, status=models.Event.CANCELLED, description="non rig today cancelled")
models.Event.objects.create(name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False,
description="non rig tomorrow")
models.Event.objects.create(name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False,
description="non rig yesterday")
models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False, status=models.Event.CANCELLED,
description="non rig today cancelled")
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3) # Set implicit wait session wide
self.browser = create_browser()
self.browser.implicitly_wait(3) # Set implicit wait session wide
os.environ['RECAPTCHA_TESTING'] = 'True'
def tearDown(self):
@@ -785,14 +902,15 @@ class IcalTest(LiveServerTestCase):
# Completes and comes back to /user/
# Checks that no api key is displayed
self.assertEqual("No API Key Generated", self.browser.find_element_by_xpath("//div[@id='content']/div/div/div[3]/dl[2]/dd").text)
self.assertEqual("No API Key Generated",
self.browser.find_element_by_xpath("//div[@id='content']/div/div/div[3]/dl[2]/dd").text)
self.assertEqual("No API Key Generated", self.browser.find_element_by_css_selector("pre").text)
# Now creates an API key, and check a URL is displayed one
self.browser.find_element_by_link_text("Generate API Key").click()
self.assertIn("rigs.ics", self.browser.find_element_by_id("cal-url").text)
self.assertNotIn("?", self.browser.find_element_by_id("cal-url").text)
# Lets change everything so it's not the default value
self.browser.find_element_by_xpath("//input[@value='rig']").click()
self.browser.find_element_by_xpath("//input[@value='non-rig']").click()
@@ -802,11 +920,15 @@ class IcalTest(LiveServerTestCase):
self.browser.find_element_by_xpath("//input[@value='confirmed']").click()
# and then check the url is correct
self.assertIn("rigs.ics?rig=false&non-rig=false&dry-hire=false&cancelled=true&provisional=false&confirmed=false", self.browser.find_element_by_id("cal-url").text)
self.assertIn(
"rigs.ics?rig=false&non-rig=false&dry-hire=false&cancelled=true&provisional=false&confirmed=false",
self.browser.find_element_by_id("cal-url").text)
# Awesome - all seems to work
def testICSFiles(self):
specialEvent = models.Event.objects.get(name="TE E6")
# Requests address
self.browser.get(self.live_server_url + '/user/')
# Gets redirected to login
@@ -815,27 +937,30 @@ class IcalTest(LiveServerTestCase):
# Now creates an API key, and check a URL is displayed one
self.browser.find_element_by_link_text("Generate API Key").click()
c = Client()
# Default settings - should have all non-cancelled events
# Get the ical file (can't do this in selanium because reasons)
icalUrl = self.browser.find_element_by_id("cal-url").text
response = c.get(icalUrl)
self.assertEqual(200, response.status_code)
#Check has entire file
self.assertIn("BEGIN:VCALENDAR", response.content)
self.assertIn("END:VCALENDAR", response.content)
# content = response.content.decode('utf-8')
expectedIn= [1,2,3,5,6,7,10,11,12,13,15,16,17]
for test in range(1,18):
# Check has entire file
self.assertContains(response, "BEGIN:VCALENDAR")
self.assertContains(response, "END:VCALENDAR")
expectedIn = [1, 2, 3, 5, 6, 7, 10, 11, 12, 13, 15, 16, 17]
for test in range(1, 18):
if test in expectedIn:
self.assertIn("TE E"+str(test)+" ", response.content)
self.assertContains(response, "TE E" + str(test) + " ")
else:
self.assertNotIn("TE E"+str(test)+" ", response.content)
self.assertNotContains(response, "TE E" + str(test) + " ")
# Check that times have been included correctly
self.assertContains(response, specialEvent.start_date.strftime('%Y%m%d') + 'T' + specialEvent.start_time.strftime('%H%M%S'))
self.assertContains(response, specialEvent.end_date.strftime('%Y%m%d') + 'T' + specialEvent.end_time.strftime('%H%M%S'))
# Only dry hires
self.browser.find_element_by_xpath("//input[@value='rig']").click()
@@ -845,13 +970,12 @@ class IcalTest(LiveServerTestCase):
response = c.get(icalUrl)
self.assertEqual(200, response.status_code)
expectedIn= [10,11,12,13]
for test in range(1,18):
expectedIn = [10, 11, 12, 13]
for test in range(1, 18):
if test in expectedIn:
self.assertIn("TE E"+str(test)+" ", response.content)
self.assertContains(response, "TE E" + str(test) + " ")
else:
self.assertNotIn("TE E"+str(test)+" ", response.content)
self.assertNotContains(response, "TE E" + str(test) + " ")
# Only provisional rigs
self.browser.find_element_by_xpath("//input[@value='rig']").click()
@@ -862,12 +986,12 @@ class IcalTest(LiveServerTestCase):
response = c.get(icalUrl)
self.assertEqual(200, response.status_code)
expectedIn= [1,2]
for test in range(1,18):
expectedIn = [1, 2]
for test in range(1, 18):
if test in expectedIn:
self.assertIn("TE E"+str(test)+" ", response.content)
self.assertContains(response, "TE E" + str(test) + " ")
else:
self.assertNotIn("TE E"+str(test)+" ", response.content)
self.assertNotContains(response, "TE E" + str(test) + " ")
# Only cancelled non-rigs
self.browser.find_element_by_xpath("//input[@value='rig']").click()
@@ -879,12 +1003,12 @@ class IcalTest(LiveServerTestCase):
response = c.get(icalUrl)
self.assertEqual(200, response.status_code)
expectedIn= [18]
for test in range(1,18):
expectedIn = [18]
for test in range(1, 18):
if test in expectedIn:
self.assertIn("TE E"+str(test)+" ", response.content)
self.assertContains(response, "TE E" + str(test) + " ")
else:
self.assertNotIn("TE E"+str(test)+" ", response.content)
self.assertNotContains(response, "TE E" + str(test) + " ")
# Nothing selected
self.browser.find_element_by_xpath("//input[@value='non-rig']").click()
@@ -894,17 +1018,19 @@ class IcalTest(LiveServerTestCase):
response = c.get(icalUrl)
self.assertEqual(200, response.status_code)
expectedIn= []
for test in range(1,18):
expectedIn = []
for test in range(1, 18):
if test in expectedIn:
self.assertIn("TE E"+str(test)+" ", response.content)
self.assertContains(response, "TE E" + str(test) + " ")
else:
self.assertNotIn("TE E"+str(test)+" ", response.content)
# Wow - that was a lot of tests
self.assertNotContains(response, "TE E" + str(test) + " ")
# Wow - that was a lot of tests
class animation_is_finished(object):
""" Checks if animation is done """
def __init__(self):
pass
@@ -915,3 +1041,167 @@ class animation_is_finished(object):
import time
time.sleep(0.1)
return finished
class ClientEventAuthorisationTest(TestCase):
auth_data = {
'name': 'Test ABC',
'po': '1234ABCZXY',
'account_code': 'ABC TEST 12345',
'uni_id': 1234567890,
'tos': True
}
def setUp(self):
self.profile = models.Profile.objects.get_or_create(
first_name='Test',
last_name='TEC User',
username='eventauthtest',
email='teccie@functional.test',
is_superuser=True # lazily grant all permissions
)[0]
self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
venue = models.Venue.objects.create(name='Authorisation Test Venue')
client = models.Person.objects.create(name='Authorisation Test Person', email='authorisation@functional.test')
organisation = models.Organisation.objects.create(name='Authorisation Test Organisation', union_account=False)
self.event = models.Event.objects.create(
name='Authorisation Test',
start_date=date.today(),
venue=venue,
person=client,
organisation=organisation,
)
self.hmac = signing.dumps({'pk': self.event.pk, 'email': 'authemail@function.test',
'sent_by': self.profile.pk})
self.url = reverse('event_authorise', kwargs={'pk': self.event.pk, 'hmac': self.hmac})
def test_requires_valid_hmac(self):
bad_hmac = self.hmac[:-1]
url = reverse('event_authorise', kwargs={'pk': self.event.pk, 'hmac': bad_hmac})
response = self.client.get(url)
self.assertIsInstance(response, HttpResponseBadRequest)
# TODO: Add some form of sensbile user facing error
# self.assertIn(response.content, "new URL") # check there is some level of sane instruction
response = self.client.get(self.url)
self.assertContains(response, self.event.organisation.name)
def test_generic_validation(self):
response = self.client.get(self.url)
self.assertContains(response, "Terms of Hire")
response = self.client.post(self.url)
self.assertContains(response, "This field is required.", 5)
data = self.auth_data
data['amount'] = self.event.total + 1
response = self.client.post(self.url, data)
self.assertContains(response, "The amount authorised must equal the total for the event")
self.assertNotContains(response, "This field is required.")
data['amount'] = self.event.total
response = self.client.post(self.url, data)
self.assertContains(response, "Your event has been authorised")
self.event.refresh_from_db()
self.assertTrue(self.event.authorised)
self.assertEqual(self.event.authorisation.email, "authemail@function.test")
def test_internal_validation(self):
self.event.organisation.union_account = True
self.event.organisation.save()
response = self.client.get(self.url)
self.assertContains(response, "Account code")
self.assertContains(response, "University ID")
response = self.client.post(self.url)
self.assertContains(response, "This field is required.", 5)
data = self.auth_data
response = self.client.post(self.url, data)
self.assertContains(response, "Your event has been authorised.")
def test_duplicate_warning(self):
auth = models.EventAuthorisation.objects.create(event=self.event, name='Test ABC', email='dupe@functional.test',
amount=self.event.total, sent_by=self.profile)
response = self.client.get(self.url)
self.assertContains(response, 'This event has already been authorised.')
auth.amount += 1
auth.save()
response = self.client.get(self.url)
self.assertContains(response, 'amount has changed')
def test_email_sent(self):
mail.outbox = []
data = self.auth_data
data['amount'] = self.event.total
response = self.client.post(self.url, data)
self.assertContains(response, "Your event has been authorised.")
self.assertEqual(len(mail.outbox), 2)
self.assertEqual(mail.outbox[0].to, ['authemail@function.test'])
self.assertEqual(mail.outbox[1].to, [settings.AUTHORISATION_NOTIFICATION_ADDRESS])
class TECEventAuthorisationTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.profile = models.Profile.objects.get_or_create(
first_name='Test',
last_name='TEC User',
username='eventauthtest',
email='teccie@nottinghamtec.co.uk',
is_superuser=True # lazily grant all permissions
)[0]
cls.profile.set_password('eventauthtest123')
cls.profile.save()
def setUp(self):
self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
venue = models.Venue.objects.create(name='Authorisation Test Venue')
client = models.Person.objects.create(name='Authorisation Test Person', email='authorisation@functional.test')
organisation = models.Organisation.objects.create(name='Authorisation Test Organisation', union_account=False)
self.event = models.Event.objects.create(
name='Authorisation Test',
start_date=date.today(),
venue=venue,
person=client,
organisation=organisation,
)
self.url = reverse('event_authorise_request', kwargs={'pk': self.event.pk})
def test_email_check(self):
self.profile.email = 'teccie@someotherdomain.com'
self.profile.save()
self.assertTrue(self.client.login(username=self.profile.username, password='eventauthtest123'))
response = self.client.post(self.url)
self.assertContains(response, 'must have an @nottinghamtec.co.uk email address')
def test_request_send(self):
self.assertTrue(self.client.login(username=self.profile.username, password='eventauthtest123'))
response = self.client.post(self.url)
self.assertContains(response, 'This field is required.')
mail.outbox = []
response = self.client.post(self.url, {'email': 'client@functional.test'})
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0]
self.assertIn('client@functional.test', email.to)
self.assertIn('/event/%d/' % (self.event.pk), email.body)
# Check sent by details are populated
self.event.refresh_from_db()
self.assertEqual(self.event.auth_request_by, self.profile)
self.assertEqual(self.event.auth_request_to, 'client@functional.test')
self.assertIsNotNone(self.event.auth_request_at)

View File

@@ -1,7 +1,11 @@
import pytz
from reversion import revisions as reversion
from django.conf import settings
from django.core.exceptions import ValidationError
from django.test import TestCase
from RIGS import models
from RIGS import models, versioning
from datetime import date, timedelta, datetime, time
from decimal import *
@@ -15,71 +19,82 @@ class ProfileTestCase(TestCase):
class VatRateTestCase(TestCase):
def setUp(self):
models.VatRate.objects.create(start_at='2014-03-01', rate=0.20, comment='test1')
models.VatRate.objects.create(start_at='2016-03-01', rate=0.15, comment='test2')
@classmethod
def setUpTestData(cls):
cls.rates = {
0: models.VatRate.objects.create(start_at='2014-03-01', rate=0.20, comment='test1'),
1: models.VatRate.objects.create(start_at='2016-03-01', rate=0.15, comment='test2'),
}
def test_find_correct(self):
r = models.VatRate.objects.find_rate('2015-03-01')
self.assertEqual(r.comment, 'test1')
self.assertEqual(r, self.rates[0])
r = models.VatRate.objects.find_rate('2016-03-01')
self.assertEqual(r.comment, 'test2')
self.assertEqual(r, self.rates[1])
def test_percent_correct(self):
r = models.VatRate.objects.get(rate=0.20)
self.assertEqual(r.as_percent, 20)
self.assertEqual(self.rates[0].as_percent, 20)
class EventTestCase(TestCase):
def setUp(self):
self.all_events = set(range(1, 18))
self.current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18)
self.not_current_events = set(self.all_events) - set(self.current_events)
@classmethod
def setUpTestData(cls):
cls.all_events = set(range(1, 18))
cls.current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18)
cls.not_current_events = set(cls.all_events) - set(cls.current_events)
self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
self.profile = models.Profile.objects.create(username="testuser1", email="1@test.com")
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com")
# produce 7 normal events - 5 current
models.Event.objects.create(name="TE E1", start_date=date.today() + timedelta(days=6),
description="start future no end")
models.Event.objects.create(name="TE E2", start_date=date.today(), description="start today no end")
models.Event.objects.create(name="TE E3", start_date=date.today(), end_date=date.today(),
description="start today with end today")
models.Event.objects.create(name="TE E4", start_date='2014-03-20', description="start past no end")
models.Event.objects.create(name="TE E5", start_date='2014-03-20', end_date='2014-03-21',
description="start past with end past")
models.Event.objects.create(name="TE E6", start_date=date.today() - timedelta(days=2),
end_date=date.today() + timedelta(days=2), description="start past, end future")
models.Event.objects.create(name="TE E7", start_date=date.today() + timedelta(days=2),
end_date=date.today() + timedelta(days=2), description="start + end in future")
cls.events = {
# produce 7 normal events - 5 current
1: models.Event.objects.create(name="TE E1", start_date=date.today() + timedelta(days=6),
description="start future no end"),
2: models.Event.objects.create(name="TE E2", start_date=date.today(), description="start today no end"),
3: models.Event.objects.create(name="TE E3", start_date=date.today(), end_date=date.today(),
description="start today with end today"),
4: models.Event.objects.create(name="TE E4", start_date='2014-03-20', description="start past no end"),
5: models.Event.objects.create(name="TE E5", start_date='2014-03-20', end_date='2014-03-21',
description="start past with end past"),
6: models.Event.objects.create(name="TE E6", start_date=date.today() - timedelta(days=2),
end_date=date.today() + timedelta(days=2),
description="start past, end future"),
7: models.Event.objects.create(name="TE E7", start_date=date.today() + timedelta(days=2),
end_date=date.today() + timedelta(days=2),
description="start + end in future"),
# 2 cancelled - 1 current
models.Event.objects.create(name="TE E8", start_date=date.today() + timedelta(days=2),
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
description="cancelled in future")
models.Event.objects.create(name="TE E9", start_date=date.today() - timedelta(days=1),
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
description="cancelled and started")
# 2 cancelled - 1 current
8: models.Event.objects.create(name="TE E8", start_date=date.today() + timedelta(days=2),
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
description="cancelled in future"),
9: models.Event.objects.create(name="TE E9", start_date=date.today() - timedelta(days=1),
end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED,
description="cancelled and started"),
# 5 dry hire - 3 current
models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True, description="dryhire today")
models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True, checked_in_by=self.profile,
description="dryhire today, checked in")
models.Event.objects.create(name="TE E12", start_date=date.today() - timedelta(days=1), dry_hire=True,
status=models.Event.BOOKED, description="dryhire past")
models.Event.objects.create(name="TE E13", start_date=date.today() - timedelta(days=2), dry_hire=True,
checked_in_by=self.profile, description="dryhire past checked in")
models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True,
status=models.Event.CANCELLED, description="dryhire today cancelled")
# 5 dry hire - 3 current
10: models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True,
description="dryhire today"),
11: models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True,
checked_in_by=cls.profile,
description="dryhire today, checked in"),
12: models.Event.objects.create(name="TE E12", start_date=date.today() - timedelta(days=1), dry_hire=True,
status=models.Event.BOOKED, description="dryhire past"),
13: models.Event.objects.create(name="TE E13", start_date=date.today() - timedelta(days=2), dry_hire=True,
checked_in_by=cls.profile, description="dryhire past checked in"),
14: models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True,
status=models.Event.CANCELLED, description="dryhire today cancelled"),
# 4 non rig - 3 current
models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False, description="non rig today")
models.Event.objects.create(name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False,
description="non rig tomorrow")
models.Event.objects.create(name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False,
description="non rig yesterday")
models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False, status=models.Event.CANCELLED,
description="non rig today cancelled")
# 4 non rig - 3 current
15: models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False,
description="non rig today"),
16: models.Event.objects.create(name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False,
description="non rig tomorrow"),
17: models.Event.objects.create(name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False,
description="non rig yesterday"),
18: models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False,
status=models.Event.CANCELLED,
description="non rig today cancelled"),
}
def test_count(self):
# Santiy check we have the expected events created
@@ -101,17 +116,23 @@ class EventTestCase(TestCase):
def test_related_venue(self):
v1 = models.Venue.objects.create(name="TE V1")
v2 = models.Venue.objects.create(name="TE V2")
events = models.Event.objects.all()
for event in events[:2]:
event.venue = v1
event.save()
for event in events[3:4]:
event.venue = v2
e1 = []
e2 = []
for (key, event) in self.events.items():
if event.pk % 2:
event.venue = v1
e1.append(event)
else:
event.venue = v2
e2.append(event)
event.save()
events = models.Event.objects.all()
self.assertItemsEqual(events[:2], v1.latest_events)
self.assertItemsEqual(events[3:4], v2.latest_events)
self.assertCountEqual(e1, v1.latest_events)
self.assertCountEqual(e2, v2.latest_events)
for (key, event) in self.events.items():
event.venue = None
def test_related_vatrate(self):
self.assertEqual(self.vatrate, models.Event.objects.all()[0].vat_rate)
@@ -120,33 +141,43 @@ class EventTestCase(TestCase):
p1 = models.Person.objects.create(name="TE P1")
p2 = models.Person.objects.create(name="TE P2")
events = models.Event.objects.all()
for event in events[:2]:
event.person = p1
event.save()
for event in events[3:4]:
event.person = p2
e1 = []
e2 = []
for (key, event) in self.events.items():
if event.pk % 2:
event.person = p1
e1.append(event)
else:
event.person = p2
e2.append(event)
event.save()
events = models.Event.objects.all()
self.assertItemsEqual(events[:2], p1.latest_events)
self.assertItemsEqual(events[3:4], p2.latest_events)
self.assertCountEqual(e1, p1.latest_events)
self.assertCountEqual(e2, p2.latest_events)
for (key, event) in self.events.items():
event.person = None
def test_related_organisation(self):
o1 = models.Organisation.objects.create(name="TE O1")
o2 = models.Organisation.objects.create(name="TE O2")
events = models.Event.objects.all()
for event in events[:2]:
event.organisation = o1
event.save()
for event in events[3:4]:
event.organisation = o2
e1 = []
e2 = []
for (key, event) in self.events.items():
if event.pk % 2:
event.organisation = o1
e1.append(event)
else:
event.organisation = o2
e2.append(event)
event.save()
events = models.Event.objects.all()
self.assertItemsEqual(events[:2], o1.latest_events)
self.assertItemsEqual(events[3:4], o2.latest_events)
self.assertCountEqual(e1, o1.latest_events)
self.assertCountEqual(e2, o2.latest_events)
for (key, event) in self.events.items():
event.organisation = None
def test_organisation_person_join(self):
p1 = models.Person.objects.create(name="TE P1")
@@ -184,49 +215,49 @@ class EventTestCase(TestCase):
self.assertEqual(len(o2.persons), 1)
def test_cancelled_property(self):
event = models.Event.objects.all()[0]
event.status = models.Event.CANCELLED
event.save()
event = models.Event.objects.all()[0]
edit = self.events[1]
edit.status = models.Event.CANCELLED
edit.save()
event = models.Event.objects.get(pk=edit.pk)
self.assertEqual(event.status, models.Event.CANCELLED)
self.assertTrue(event.cancelled)
event.status = models.Event.PROVISIONAL
event.save()
def test_confirmed_property(self):
event = models.Event.objects.all()[0]
event.status = models.Event.CONFIRMED
event.save()
event = models.Event.objects.all()[0]
edit = self.events[1]
edit.status = models.Event.CONFIRMED
edit.save()
event = models.Event.objects.get(pk=edit.pk)
self.assertEqual(event.status, models.Event.CONFIRMED)
self.assertTrue(event.confirmed)
event.status = models.Event.PROVISIONAL
event.save()
def test_earliest_time(self):
event = models.Event(name="TE ET", start_date=date(2016, 01, 01))
event = models.Event(name="TE ET", start_date=date(2016, 0o1, 0o1))
# Just a start date
self.assertEqual(event.earliest_time, date(2016, 01, 01))
self.assertEqual(event.earliest_time, date(2016, 0o1, 0o1))
# With start time
event.start_time = time(9, 00)
self.assertEqual(event.earliest_time, self.create_datetime(2016, 1, 1, 9, 00))
# With access time
event.access_at = self.create_datetime(2015, 12, 03, 9, 57)
event.access_at = self.create_datetime(2015, 12, 0o3, 9, 57)
self.assertEqual(event.earliest_time, event.access_at)
# With meet time
event.meet_at = self.create_datetime(2015, 12, 03, 9, 55)
event.meet_at = self.create_datetime(2015, 12, 0o3, 9, 55)
self.assertEqual(event.earliest_time, event.meet_at)
# Check order isn't important
event.start_date = date(2015, 12, 03)
self.assertEqual(event.earliest_time, self.create_datetime(2015, 12, 03, 9, 00))
event.start_date = date(2015, 12, 0o3)
self.assertEqual(event.earliest_time, self.create_datetime(2015, 12, 0o3, 9, 00))
def test_latest_time(self):
event = models.Event(name="TE LT", start_date=date(2016, 01, 01))
event = models.Event(name="TE LT", start_date=date(2016, 0o1, 0o1))
# Just start date
self.assertEqual(event.latest_time, event.start_date)
@@ -248,14 +279,14 @@ class EventTestCase(TestCase):
# basic checks
manager.create(name='TE IB2', start_date='2016-01-02', end_date='2016-01-04'),
manager.create(name='TE IB3', start_date='2015-12-31', end_date='2016-01-03'),
manager.create(name='TE IB4', start_date='2016-01-04', access_at='2016-01-03'),
manager.create(name='TE IB5', start_date='2016-01-04', meet_at='2016-01-02'),
manager.create(name='TE IB4', start_date='2016-01-04', access_at=self.create_datetime(2016, 0o1, 0o3, 00, 00)),
manager.create(name='TE IB5', start_date='2016-01-04', meet_at=self.create_datetime(2016, 0o1, 0o2, 00, 00)),
# negative check
manager.create(name='TE IB6', start_date='2015-12-31', end_date='2016-01-01'),
]
in_bounds = manager.events_in_bounds(datetime(2016, 1, 2), datetime(2016, 1, 3))
in_bounds = manager.events_in_bounds(self.create_datetime(2016, 1, 2, 0, 0), self.create_datetime(2016, 1, 3, 0, 0))
self.assertIn(events[0], in_bounds)
self.assertIn(events[1], in_bounds)
self.assertIn(events[2], in_bounds)
@@ -327,3 +358,220 @@ class EventPricingTestCase(TestCase):
def test_grand_total(self):
self.assertEqual(self.e1.total, Decimal('84.48'))
self.assertEqual(self.e2.total, Decimal('419.32'))
class EventAuthorisationTestCase(TestCase):
def setUp(self):
models.VatRate.objects.create(rate=0.20, comment="TP V1", start_at='2013-01-01')
self.profile = models.Profile.objects.get_or_create(
first_name='Test',
last_name='TEC User',
username='eventauthtest',
email='teccie@functional.test',
is_superuser=True # lazily grant all permissions
)[0]
self.person = models.Person.objects.create(name='Authorisation Test Person')
self.organisation = models.Organisation.objects.create(name='Authorisation Test Organisation')
self.event = models.Event.objects.create(name="AuthorisationTestCase", person=self.person,
start_date=date.today())
# Add some items
models.EventItem.objects.create(event=self.event, name="Authorisation test item", quantity=2, cost=123.45,
order=1)
def test_event_property(self):
auth1 = models.EventAuthorisation.objects.create(event=self.event, email="authroisation@model.test.case",
name="Test Auth 1", amount=self.event.total - 1,
sent_by=self.profile)
self.assertFalse(self.event.authorised)
auth1.amount = self.event.total
auth1.save()
self.assertTrue(self.event.authorised)
def test_last_edited(self):
with reversion.create_revision():
auth = models.EventAuthorisation.objects.create(event=self.event, email="authroisation@model.test.case",
name="Test Auth", amount=self.event.total, sent_by=self.profile)
self.assertIsNotNone(auth.last_edited_at)
class RIGSVersionTestCase(TestCase):
def setUp(self):
models.VatRate.objects.create(rate=0.20, comment="TP V1", start_at='2013-01-01')
self.profile = models.Profile.objects.get_or_create(
first_name='Test',
last_name='TEC User',
username='eventauthtest',
email='teccie@functional.test',
is_superuser=True # lazily grant all permissions
)[0]
with reversion.create_revision():
reversion.set_user(self.profile)
self.person = models.Person.objects.create(name='Authorisation Test Person')
with reversion.create_revision():
reversion.set_user(self.profile)
self.organisation = models.Organisation.objects.create(name='Authorisation Test Organisation')
with reversion.create_revision():
reversion.set_user(self.profile)
self.event = models.Event.objects.create(name="AuthorisationTestCase", person=self.person,
start_date=date.today())
with reversion.create_revision():
reversion.set_user(self.profile)
self.event.notes = "A new note on the event"
self.event.save()
def test_find_parent_version(self):
# Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
self.assertEqual(currentVersion._object_version.object.notes, "A new note on the event")
# Check the prev version is loaded correctly
previousVersion = currentVersion.parent
self.assertEqual(previousVersion._object_version.object.notes, None)
# Check that finding the parent of the first version fails gracefully
self.assertFalse(previousVersion.parent)
def test_changes_since(self):
# Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
changes = currentVersion.changes
self.assertEqual(len(changes.field_changes), 1)
def test_manager(self):
objs = versioning.RIGSVersion.objects.get_for_multiple_models([models.Event, models.Person, models.Organisation])
self.assertEqual(len(objs), 4)
def test_text_field_types(self):
with reversion.create_revision():
reversion.set_user(self.profile)
self.event.name = "New event name" # Simple text
self.event.description = "hello world" # Long text
self.event.save()
# Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
diff = currentVersion.changes
# There are two changes
self.assertEqual(len(diff.field_changes), 2)
self.assertFalse(currentVersion.changes.items_changed)
self.assertTrue(currentVersion.changes.fields_changed)
self.assertTrue(currentVersion.changes.anything_changed)
# Only one has "linebreaks"
self.assertEqual(sum([x.linebreaks for x in diff.field_changes]), 1)
# None are "long" (email address)
self.assertEqual(sum([x.long for x in diff.field_changes]), 0)
# Try changing email field in person
with reversion.create_revision():
reversion.set_user(self.profile)
self.person.email = "hello@world.com"
self.person.save()
# Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.person).latest(field_name='revision__date_created')
diff = currentVersion.changes
# Should be declared as long
self.assertTrue(diff.field_changes[0].long)
def test_text_diff(self):
with reversion.create_revision():
reversion.set_user(self.profile)
self.event.notes = "An old note on the event" # Simple text
self.event.save()
# Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
# Check the diff is correct
self.assertEqual(currentVersion.changes.field_changes[0].diff,
[{'type': 'equal', 'text': "A"},
{'type': 'delete', 'text': " new"},
{'type': 'insert', 'text': "n old"},
{'type': 'equal', 'text': " note on the event"}
])
def test_choice_field(self):
with reversion.create_revision():
reversion.set_user(self.profile)
self.event.status = models.Event.CONFIRMED
self.event.save()
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
self.assertEqual(currentVersion.changes.field_changes[0].old, 'Provisional')
self.assertEqual(currentVersion.changes.field_changes[0].new, 'Confirmed')
def test_creation_behaviour(self):
firstVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created').parent
diff = firstVersion.changes
# Mainly to check for exceptions:
self.assertTrue(len(diff.field_changes) > 0)
def test_event_items(self):
with reversion.create_revision():
reversion.set_user(self.profile)
item1 = models.EventItem.objects.create(event=self.event, name="TI I1", quantity=1, cost=1.00, order=1)
self.event.save()
# Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
diffs = currentVersion.changes.item_changes
self.assertEqual(len(diffs), 1)
self.assertTrue(currentVersion.changes.items_changed)
self.assertFalse(currentVersion.changes.fields_changed)
self.assertTrue(currentVersion.changes.anything_changed)
self.assertTrue(diffs[0].old is None)
self.assertEqual(diffs[0].new.name, "TI I1")
# Edit the item
with reversion.create_revision():
reversion.set_user(self.profile)
item1.name = "New Name"
item1.save()
self.event.save()
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
diffs = currentVersion.changes.item_changes
self.assertEqual(len(diffs), 1)
self.assertEqual(diffs[0].old.name, "TI I1")
self.assertEqual(diffs[0].new.name, "New Name")
# Check the diff
self.assertEqual(currentVersion.changes.item_changes[0].field_changes[0].diff,
[{'type': 'delete', 'text': "TI I1"},
{'type': 'insert', 'text': "New Name"},
])
# Delete the item
with reversion.create_revision():
item1.delete()
self.event.save()
# Find the most recent version
currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest(field_name='revision__date_created')
diffs = currentVersion.changes.item_changes
self.assertEqual(len(diffs), 1)
self.assertTrue(currentVersion.changes.items_changed)
self.assertFalse(currentVersion.changes.fields_changed)
self.assertTrue(currentVersion.changes.anything_changed)
self.assertEqual(diffs[0].old.name, "New Name")
self.assertTrue(diffs[0].new is None)

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