mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-01-17 05:22:16 +00:00
Compare commits
411 Commits
hotfix/for
...
imgbot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5dc879733 | ||
|
c537118037
|
|||
|
466a9a9693
|
|||
| d25381b2de | |||
|
|
eaf891daf7 | ||
|
|
801d2e8a7d | ||
|
|
3d329219b8 | ||
|
2ddc8923ba
|
|||
|
276a86c5be
|
|||
|
484f155e43
|
|||
|
fdbdaab52e
|
|||
|
|
a01e351e89 | ||
|
|
708a387774 | ||
|
|
af6fe582e0 | ||
|
|
905a144e7d | ||
| 0e64021f01 | |||
| 2eb87a51f8 | |||
| 30fac1d1b9 | |||
|
c4fec483ae
|
|||
|
3028fb92d9
|
|||
| 215697ba64 | |||
|
d966bddfd7
|
|||
|
0d5e48b89c
|
|||
|
bd2c94d3e3
|
|||
|
21d09d951d
|
|||
|
014b00bc30
|
|||
|
3f8fc82260
|
|||
| 41c1c44754 | |||
|
8a2b107516
|
|||
|
f8c52803a5
|
|||
|
85d1850f08
|
|||
|
e146d9314a
|
|||
|
2c3dff79ba
|
|||
|
9ee8cd0f8b
|
|||
|
d3391d9e3e
|
|||
|
|
0086461d6c | ||
|
8bafeabe5f
|
|||
|
f214f9a835
|
|||
|
b31d53a3c5
|
|||
|
62a891c6ec
|
|||
|
8c0c0941c2
|
|||
|
abb0e35690
|
|||
|
bec0d4aee5
|
|||
|
f1e43b707e
|
|||
| 796f5b44b0 | |||
| 6458f016f0 | |||
| 9ca953423f | |||
|
|
4c5d958c6d | ||
| 85ca7b0880 | |||
|
44f9509eda
|
|||
|
a2be4cbe5e
|
|||
|
|
bb2f369ab5 | ||
|
2fdb2f260f
|
|||
|
6de3cb5d8c
|
|||
|
7c38af66f6
|
|||
|
f1a624ec8f
|
|||
|
ab01beb2cd
|
|||
|
11636809ca
|
|||
|
d7458f6366
|
|||
| febf9cf3ed | |||
| 3322a5ddf8 | |||
|
|
673bee4215 | ||
|
|
bab31107f7 | ||
|
|
2d8473b698 | ||
| d81ecd9015 | |||
| b42c583897 | |||
| 57e966826e | |||
|
6a5de4a9d6
|
|||
| 56bbf4c17c | |||
|
|
698f0be281 | ||
|
|
483f06e96f | ||
|
|
22193f3c39 | ||
|
|
59b63fe7aa | ||
| 5976ce9ea2 | |||
|
780d05e27c
|
|||
| 8cfa4bd79d | |||
|
36f83ee59b
|
|||
|
6d768832f4
|
|||
|
38da8642fa
|
|||
|
f75e1d5bfc
|
|||
|
3f959f8d56
|
|||
|
b63a01120b
|
|||
| 911336ceec | |||
| 2bf0175786 | |||
| 8ad629a47e | |||
|
|
2414eb9724 | ||
| 3414204209 | |||
|
099a184f2e
|
|||
|
|
0e67da82e2 | ||
| 02e8e8aaf7 | |||
|
|
4acd9156d0 | ||
| 3be06a7b25 | |||
|
0fe7d55eab
|
|||
|
be4a7baf8e
|
|||
| a0491891e9 | |||
|
02d40d1b39
|
|||
| 8568c591a9 | |||
|
|
797ad778a9 | ||
| 4a4d4a5cf3 | |||
| ae151ed45e | |||
|
|
116c497590 | ||
| f6f3149036 | |||
|
|
81e7bf6d46 | ||
| 79f97bb05f | |||
|
6ba87b0a5a
|
|||
|
1e03b5107e
|
|||
|
39dbdd7ce4
|
|||
| 1a953073be | |||
| 630011aff7 | |||
|
|
e0c6a56263 | ||
| 87d460c799 | |||
| 295397b32d | |||
| 10add5ab33 | |||
| 7e3e8f37e2 | |||
| 3a25b85e95 | |||
| 16b950c3b2 | |||
| f616017423 | |||
| 1480ae17fa | |||
| 4ad12ab40a | |||
| 13205770f1 | |||
| 6bb0c88c72 | |||
| 82a30ca77d | |||
|
|
97c0dffbd3 | ||
|
|
3b28eafc82 | ||
| ca8253894a | |||
| 01a87e0e0b | |||
|
|
7c876348d7 | ||
|
ddc23ce4e5
|
|||
|
602ccc15ea
|
|||
|
|
b77615b9b9 | ||
|
|
228d72b7b2 | ||
|
62541194ee
|
|||
|
0d8fd99d92
|
|||
|
9d51a82f31
|
|||
|
c059227d5d
|
|||
|
2c334196d5
|
|||
| 4f036af85a | |||
| 5210afc772 | |||
|
|
4da8040351 | ||
|
|
1a49bb50e5 | ||
|
|
86b349f60e | ||
|
|
35997aa882 | ||
|
|
faa4573f6d | ||
|
|
7babaee44c | ||
|
|
c0fe176495 | ||
|
|
b269069b6a | ||
|
|
7e06b5a162 | ||
|
|
a18bb07d78 | ||
|
|
bd28d2054e | ||
|
|
14836f135c | ||
|
|
5f8a77586a | ||
|
|
e5b7fdbae1 | ||
|
|
f1c8dca8c4 | ||
|
|
843b76d8ea | ||
|
|
e81af9e479 | ||
|
|
efab8c8cef | ||
|
|
3f7531e157 | ||
|
|
988f3dced4 | ||
|
|
bd3240c2bc | ||
|
|
7fbbe2871f | ||
|
|
109ece508b | ||
|
|
971144c2e6 | ||
|
|
a3c6edda0b | ||
|
|
71bb4696b8 | ||
|
|
2a3ed0b763 | ||
|
|
632fa56353 | ||
|
|
f3020fc783 | ||
|
|
9fdf5e674e | ||
|
|
cfe03a8628 | ||
|
|
eccf224b63 | ||
|
|
0b2c86ebb5 | ||
|
|
f616857131 | ||
|
|
60fb90a50c | ||
|
|
66f024e961 | ||
|
|
06daacf611 | ||
|
|
c74bc945b6 | ||
|
|
3c605d2976 | ||
|
|
9720066fd7 | ||
|
|
b157e3b187 | ||
|
|
19030fdf2f | ||
|
|
42450b5a22 | ||
|
|
ce11df9bbc | ||
|
|
82e664c5e0 | ||
|
|
8098b33698 | ||
|
|
9c2603557c | ||
|
|
f4209f21dc | ||
|
|
1e3c021a76 | ||
|
|
d8f9256252 | ||
|
|
014bab6c1f | ||
|
|
8872084cab | ||
|
|
76ceb15000 | ||
|
|
7dff951f28 | ||
|
|
05feb7df1c | ||
|
|
ad3b38d222 | ||
|
|
a26e65073c | ||
|
|
7f3d628d01 | ||
|
|
abdf785723 | ||
|
|
85edba03a2 | ||
|
|
d61b21df0a | ||
|
|
7e68dfb851 | ||
|
|
69cbac31e7 | ||
|
|
ce4e0f5630 | ||
|
|
c99c5d573f | ||
|
|
221ef739b9 | ||
|
|
aa98039c35 | ||
|
|
7ef923b89f | ||
|
|
9ac86a6ad0 | ||
|
|
c0c143a166 | ||
|
|
0abfa2fd0c | ||
|
|
3333b29f24 | ||
|
|
880509d611 | ||
|
|
387f5b0d8e | ||
|
|
27b12d6bf4 | ||
|
|
c511f2f528 | ||
|
|
a61165f301 | ||
|
|
98245939fe | ||
|
|
b203832f79 | ||
|
|
c6846b85c7 | ||
|
|
0c15718d31 | ||
|
|
0f1db22452 | ||
|
|
e3970929e4 | ||
|
|
a64bf8e16f | ||
|
|
cf8edc8a1c | ||
|
|
ca9e309b1b | ||
|
|
0ccb669019 | ||
|
|
92acd17e64 | ||
|
|
ff83bcc14a | ||
|
|
f17e0e199c | ||
|
|
7b3b05677c | ||
|
|
403549f449 | ||
|
|
aee4ccb5a2 | ||
|
|
f8c2551eeb | ||
|
|
1bab3464b7 | ||
|
|
2b863b91b3 | ||
|
|
5c4181a5cc | ||
|
|
f9389e3996 | ||
|
|
131ff3e612 | ||
|
|
7dd7378e6c | ||
|
|
4d0da2fdc1 | ||
|
|
ddbc0699ee | ||
|
|
5f14001d01 | ||
|
|
91f1e6d290 | ||
|
|
00d46274f1 | ||
|
|
0aec836b1a | ||
|
|
ee930aa86a | ||
|
|
a88f4d0cb2 | ||
|
|
50c997b568 | ||
|
|
3035320e82 | ||
|
|
98182143ec | ||
|
|
bffbaeb4c6 | ||
|
|
8f1c640bea | ||
|
|
827a2cdd25 | ||
|
|
3202981fbe | ||
|
|
f0edd5020e | ||
|
|
cf1c7ae725 | ||
|
|
1ac637bfd7 | ||
|
|
37933650d3 | ||
|
|
1266dd419c | ||
|
|
74747e8700 | ||
|
|
34ffd62436 | ||
|
|
22c520e841 | ||
|
|
3fa9191150 | ||
|
|
ae4f05a661 | ||
|
|
c178a293a1 | ||
|
|
aebaa16311 | ||
|
|
175807f664 | ||
|
|
abb56af222 | ||
|
|
48139b29cf | ||
|
|
7ec09fb774 | ||
|
|
e37d1c663b | ||
|
|
bdd7f02fe2 | ||
|
|
b4ea818992 | ||
|
|
7cb9e97ecb | ||
|
|
7fdafd854e | ||
|
|
d85ebb63a1 | ||
|
|
c6b7bbc219 | ||
|
|
4d316c7a4a | ||
|
|
75a3059c88 | ||
|
|
b6e4c0ed14 | ||
|
|
0a45b047a2 | ||
|
|
4e79f00551 | ||
|
|
b4ab29393e | ||
|
|
703fb8561a | ||
|
|
f3c020b613 | ||
|
|
4b87b0a196 | ||
|
|
7cc715cedc | ||
|
|
4b032944ac | ||
|
|
cb23fd183e | ||
|
|
fbc039c274 | ||
|
|
b3156dbb0d | ||
|
|
fdce2fa53d | ||
|
|
55d24e96cb | ||
|
|
36d258253f | ||
|
|
865bb131a5 | ||
|
|
eb1e8935f4 | ||
|
|
f8aaf9f36e | ||
|
|
7a4f5ba8bf | ||
|
|
16a993123b | ||
|
|
e547e3e858 | ||
|
|
ea26823fec | ||
|
|
1f4e53ad27 | ||
|
|
374c31e8b4 | ||
|
|
0d726b2b60 | ||
|
|
38a8ac1eb4 | ||
|
|
872e5e72f3 | ||
|
|
d916c1ca19 | ||
|
|
9b1cc965c7 | ||
|
|
83028418fe | ||
|
|
7f680dcffb | ||
|
|
e573088c5e | ||
|
|
7ac9eef7a2 | ||
|
|
286e4314f5 | ||
|
|
6b05938953 | ||
|
|
602ba1d051 | ||
|
|
1710c3f01f | ||
|
|
f57ac3acb1 | ||
|
|
331dab20f7 | ||
|
|
d9076a4f5f | ||
|
|
56d4e438b6 | ||
|
|
430862b24d | ||
|
|
e12367bde7 | ||
|
|
c0f4884242 | ||
|
|
36638e4df6 | ||
|
|
a0440e1587 | ||
|
|
6e78f16c33 | ||
|
|
82b6f1cbf8 | ||
|
|
5be3842aea | ||
|
|
067e03b757 | ||
|
|
391d9ef28f | ||
|
|
5d17d642ec | ||
|
|
22119a3d08 | ||
|
|
306c11bb2f | ||
|
|
3fa9795cde | ||
|
|
7fd0c50146 | ||
|
|
97b11eabbd | ||
|
|
3b2aa02ae5 | ||
|
|
cf11e8235f | ||
|
|
1670b190c2 | ||
|
|
e65e97b1a3 | ||
|
|
c2787d54b0 | ||
|
|
9b7c84cf08 | ||
|
|
3269e92ef2 | ||
|
|
0ae7bcaf7c | ||
|
|
9694d407ae | ||
|
|
82aa2785ea | ||
|
|
337dbd74fd | ||
|
|
caa55fe89a | ||
|
|
289b30e823 | ||
|
|
b939bc5a64 | ||
|
|
5e8f2312d3 | ||
|
|
90c8b19915 | ||
|
|
d82ef3f8d1 | ||
|
|
fc006dc53e | ||
|
|
3ce191aaf2 | ||
|
|
6fc89727f2 | ||
|
|
b235ac540f | ||
|
|
97decf8c52 | ||
|
|
97cdf34c18 | ||
|
|
92c77c07e0 | ||
|
|
0541a70cec | ||
|
|
e0cb2f4925 | ||
|
|
68a46af1a8 | ||
|
|
f61158b9c0 | ||
|
|
88954eca5c | ||
|
|
3fc04616b3 | ||
|
|
2d5f768523 | ||
|
|
5949ff74ec | ||
|
|
879ecd1f6d | ||
|
|
0e72c3f896 | ||
|
|
b93a716a3b | ||
|
|
0d92c3812a | ||
|
|
fc110a0bff | ||
|
|
008edd8bee | ||
|
|
ac7e85c24a | ||
|
|
73b8ce4add | ||
|
|
511ce554b1 | ||
|
|
536842971d | ||
|
|
3e224a33a7 | ||
|
|
3f4c362bfa | ||
|
|
8a838aa4bd | ||
|
|
7e379b33db | ||
|
|
5e9f7e2c63 | ||
|
|
3f752cd7b7 | ||
|
|
25a3ef3f0c | ||
|
|
1b28efb6af | ||
|
|
441a2be0b8 | ||
|
|
1bdc4bd293 | ||
|
|
f0bb4c5b02 | ||
|
|
4660322964 | ||
|
|
59efc2c485 | ||
|
|
69b0ff9fae | ||
|
|
4b94ea7ef2 | ||
|
|
0244f5cfca | ||
|
|
17c7a3c524 | ||
|
|
a02087bf2a | ||
|
|
585f909d3f | ||
|
|
eb10c8e21f | ||
|
|
f7ea0cb834 | ||
|
|
64f3842a13 | ||
|
|
6370679b62 | ||
|
|
e77728c52c | ||
|
|
92f4e26883 | ||
|
|
8cfda69717 | ||
|
|
463c4d147c | ||
|
|
6da688cc9e | ||
|
|
c1d164bd73 | ||
|
|
d43e4b2465 | ||
|
|
98ee9bb0db | ||
|
|
cd2aed00d7 | ||
|
|
0ee37b1cd3 | ||
|
|
486c66b198 |
@@ -1,32 +0,0 @@
|
||||
---
|
||||
engines:
|
||||
csslint:
|
||||
enabled: true
|
||||
duplication:
|
||||
enabled: true
|
||||
config:
|
||||
languages:
|
||||
- ruby
|
||||
- javascript
|
||||
- python
|
||||
- php
|
||||
eslint:
|
||||
enabled: true
|
||||
fixme:
|
||||
enabled: true
|
||||
radon:
|
||||
enabled: true
|
||||
rubocop:
|
||||
enabled: true
|
||||
ratings:
|
||||
paths:
|
||||
- "**.css"
|
||||
- "**.inc"
|
||||
- "**.js"
|
||||
- "**.jsx"
|
||||
- "**.module"
|
||||
- "**.php"
|
||||
- "**.py"
|
||||
- "**.rb"
|
||||
exclude_paths:
|
||||
- config/
|
||||
@@ -1,6 +1,5 @@
|
||||
[run]
|
||||
source =
|
||||
./
|
||||
|
||||
omit =
|
||||
*/migrations/*
|
||||
omit = */migrations/*
|
||||
*/tests/*
|
||||
*/site-packages/*
|
||||
*/distutils/*
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
--exclude-exts=.min.css
|
||||
--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes
|
||||
@@ -1 +0,0 @@
|
||||
**/*{.,-}min.js
|
||||
213
.eslintrc
213
.eslintrc
@@ -1,213 +0,0 @@
|
||||
ecmaFeatures:
|
||||
modules: true
|
||||
jsx: true
|
||||
|
||||
env:
|
||||
amd: true
|
||||
browser: true
|
||||
es6: true
|
||||
jquery: true
|
||||
node: true
|
||||
|
||||
# http://eslint.org/docs/rules/
|
||||
rules:
|
||||
# Possible Errors
|
||||
comma-dangle: [2, never]
|
||||
no-cond-assign: 2
|
||||
no-console: 0
|
||||
no-constant-condition: 2
|
||||
no-control-regex: 2
|
||||
no-debugger: 2
|
||||
no-dupe-args: 2
|
||||
no-dupe-keys: 2
|
||||
no-duplicate-case: 2
|
||||
no-empty: 2
|
||||
no-empty-character-class: 2
|
||||
no-ex-assign: 2
|
||||
no-extra-boolean-cast: 2
|
||||
no-extra-parens: 0
|
||||
no-extra-semi: 2
|
||||
no-func-assign: 2
|
||||
no-inner-declarations: [2, functions]
|
||||
no-invalid-regexp: 2
|
||||
no-irregular-whitespace: 2
|
||||
no-negated-in-lhs: 2
|
||||
no-obj-calls: 2
|
||||
no-regex-spaces: 2
|
||||
no-sparse-arrays: 2
|
||||
no-unexpected-multiline: 2
|
||||
no-unreachable: 2
|
||||
use-isnan: 2
|
||||
valid-jsdoc: 0
|
||||
valid-typeof: 2
|
||||
|
||||
# Best Practices
|
||||
accessor-pairs: 2
|
||||
block-scoped-var: 0
|
||||
complexity: [2, 6]
|
||||
consistent-return: 0
|
||||
curly: 0
|
||||
default-case: 0
|
||||
dot-location: 0
|
||||
dot-notation: 0
|
||||
eqeqeq: 2
|
||||
guard-for-in: 2
|
||||
no-alert: 2
|
||||
no-caller: 2
|
||||
no-case-declarations: 2
|
||||
no-div-regex: 2
|
||||
no-else-return: 0
|
||||
no-empty-label: 2
|
||||
no-empty-pattern: 2
|
||||
no-eq-null: 2
|
||||
no-eval: 2
|
||||
no-extend-native: 2
|
||||
no-extra-bind: 2
|
||||
no-fallthrough: 2
|
||||
no-floating-decimal: 0
|
||||
no-implicit-coercion: 0
|
||||
no-implied-eval: 2
|
||||
no-invalid-this: 0
|
||||
no-iterator: 2
|
||||
no-labels: 0
|
||||
no-lone-blocks: 2
|
||||
no-loop-func: 2
|
||||
no-magic-number: 0
|
||||
no-multi-spaces: 0
|
||||
no-multi-str: 0
|
||||
no-native-reassign: 2
|
||||
no-new-func: 2
|
||||
no-new-wrappers: 2
|
||||
no-new: 2
|
||||
no-octal-escape: 2
|
||||
no-octal: 2
|
||||
no-proto: 2
|
||||
no-redeclare: 2
|
||||
no-return-assign: 2
|
||||
no-script-url: 2
|
||||
no-self-compare: 2
|
||||
no-sequences: 0
|
||||
no-throw-literal: 0
|
||||
no-unused-expressions: 2
|
||||
no-useless-call: 2
|
||||
no-useless-concat: 2
|
||||
no-void: 2
|
||||
no-warning-comments: 0
|
||||
no-with: 2
|
||||
radix: 2
|
||||
vars-on-top: 0
|
||||
wrap-iife: 2
|
||||
yoda: 0
|
||||
|
||||
# Strict
|
||||
strict: 0
|
||||
|
||||
# Variables
|
||||
init-declarations: 0
|
||||
no-catch-shadow: 2
|
||||
no-delete-var: 2
|
||||
no-label-var: 2
|
||||
no-shadow-restricted-names: 2
|
||||
no-shadow: 0
|
||||
no-undef-init: 2
|
||||
no-undef: 0
|
||||
no-undefined: 0
|
||||
no-unused-vars: 0
|
||||
no-use-before-define: 0
|
||||
|
||||
# Node.js and CommonJS
|
||||
callback-return: 2
|
||||
global-require: 2
|
||||
handle-callback-err: 2
|
||||
no-mixed-requires: 0
|
||||
no-new-require: 0
|
||||
no-path-concat: 2
|
||||
no-process-exit: 2
|
||||
no-restricted-modules: 0
|
||||
no-sync: 0
|
||||
|
||||
# Stylistic Issues
|
||||
array-bracket-spacing: 0
|
||||
block-spacing: 0
|
||||
brace-style: 0
|
||||
camelcase: 0
|
||||
comma-spacing: 0
|
||||
comma-style: 0
|
||||
computed-property-spacing: 0
|
||||
consistent-this: 0
|
||||
eol-last: 0
|
||||
func-names: 0
|
||||
func-style: 0
|
||||
id-length: 0
|
||||
id-match: 0
|
||||
indent: 0
|
||||
jsx-quotes: 0
|
||||
key-spacing: 0
|
||||
linebreak-style: 0
|
||||
lines-around-comment: 0
|
||||
max-depth: 0
|
||||
max-len: 0
|
||||
max-nested-callbacks: 0
|
||||
max-params: 0
|
||||
max-statements: [2, 30]
|
||||
new-cap: 0
|
||||
new-parens: 0
|
||||
newline-after-var: 0
|
||||
no-array-constructor: 0
|
||||
no-bitwise: 0
|
||||
no-continue: 0
|
||||
no-inline-comments: 0
|
||||
no-lonely-if: 0
|
||||
no-mixed-spaces-and-tabs: 0
|
||||
no-multiple-empty-lines: 0
|
||||
no-negated-condition: 0
|
||||
no-nested-ternary: 0
|
||||
no-new-object: 0
|
||||
no-plusplus: 0
|
||||
no-restricted-syntax: 0
|
||||
no-spaced-func: 0
|
||||
no-ternary: 0
|
||||
no-trailing-spaces: 0
|
||||
no-underscore-dangle: 0
|
||||
no-unneeded-ternary: 0
|
||||
object-curly-spacing: 0
|
||||
one-var: 0
|
||||
operator-assignment: 0
|
||||
operator-linebreak: 0
|
||||
padded-blocks: 0
|
||||
quote-props: 0
|
||||
quotes: 0
|
||||
require-jsdoc: 0
|
||||
semi-spacing: 0
|
||||
semi: 0
|
||||
sort-vars: 0
|
||||
space-after-keywords: 0
|
||||
space-before-blocks: 0
|
||||
space-before-function-paren: 0
|
||||
space-before-keywords: 0
|
||||
space-in-parens: 0
|
||||
space-infix-ops: 0
|
||||
space-return-throw-case: 0
|
||||
space-unary-ops: 0
|
||||
spaced-comment: 0
|
||||
wrap-regex: 0
|
||||
|
||||
# ECMAScript 6
|
||||
arrow-body-style: 0
|
||||
arrow-parens: 0
|
||||
arrow-spacing: 0
|
||||
constructor-super: 0
|
||||
generator-star-spacing: 0
|
||||
no-arrow-condition: 0
|
||||
no-class-assign: 0
|
||||
no-const-assign: 0
|
||||
no-dupe-class-members: 0
|
||||
no-this-before-super: 0
|
||||
no-var: 0
|
||||
object-shorthand: 0
|
||||
prefer-arrow-callback: 0
|
||||
prefer-const: 0
|
||||
prefer-reflect: 0
|
||||
prefer-spread: 0
|
||||
prefer-template: 0
|
||||
require-yield: 0
|
||||
58
.github/workflows/django.yml
vendored
Normal file
58
.github/workflows/django.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Django CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9.1
|
||||
- uses: actions/cache@v2
|
||||
id: pcache
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip pipenv
|
||||
pipenv install -d
|
||||
# if: steps.pcache.outputs.cache-hit != 'true'
|
||||
- name: Cache Static Files
|
||||
id: static-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: 'pipeline/built_assets'
|
||||
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
||||
- uses: bahmutov/npm-install@v1
|
||||
if: steps.static-cache.outputs.cache-hit != 'true'
|
||||
- run: node node_modules/gulp/bin/gulp build
|
||||
if: steps.static-cache.outputs.cache-hit != 'true'
|
||||
- name: Basic Checks
|
||||
run: |
|
||||
pipenv run pycodestyle . --exclude=migrations,node_modules
|
||||
pipenv run python manage.py check
|
||||
pipenv run python manage.py makemigrations --check --dry-run
|
||||
pipenv run python manage.py collectstatic --noinput
|
||||
- name: Run Tests
|
||||
run: pipenv run pytest -n auto -vv --cov
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: failure-screenshots ${{ matrix.test-group }}
|
||||
path: screenshots/
|
||||
retention-days: 5
|
||||
- name: Coveralls
|
||||
run: pipenv run coveralls --service=github
|
||||
19
.gitignore
vendored
19
.gitignore
vendored
@@ -25,6 +25,8 @@ var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
node_modules/
|
||||
data/
|
||||
|
||||
# Continer extras
|
||||
.vagrant
|
||||
@@ -44,6 +46,7 @@ htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
.pytest_cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
@@ -66,19 +69,9 @@ target/
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
# User-specific stuff:
|
||||
# .idea/workspace.xml
|
||||
# .idea/tasks.xml
|
||||
# .idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
# .idea/dataSources.ids
|
||||
# .idea/dataSources.xml
|
||||
# .idea/sqlDataSources.xml
|
||||
# .idea/dynamic.xml
|
||||
# .idea/uiDesigner.xml
|
||||
#Built dependencies
|
||||
pipeline/built_assets
|
||||
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
@@ -106,3 +99,5 @@ atlassian-ide-plugin.xml
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
.vscode/
|
||||
screenshots/
|
||||
|
||||
1
.idea/.name
generated
1
.idea/.name
generated
@@ -1 +0,0 @@
|
||||
PyRIGS
|
||||
5
.idea/encodings.xml
generated
5
.idea/encodings.xml
generated
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
|
||||
</project>
|
||||
|
||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/pyrigs.iml" filepath="$PROJECT_DIR$/.idea/pyrigs.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
5
.idea/scopes/scope_settings.xml
generated
5
.idea/scopes/scope_settings.xml
generated
@@ -1,5 +0,0 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<state>
|
||||
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
|
||||
</state>
|
||||
</component>
|
||||
7
.idea/vcs.xml
generated
7
.idea/vcs.xml
generated
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
||||
1156
.rubocop.yml
1156
.rubocop.yml
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
*.sqlite3
|
||||
*.scss
|
||||
*.md
|
||||
*.rb
|
||||
Vagrantfile
|
||||
config/vagrant/*
|
||||
config/vagrant.yml
|
||||
**/tests
|
||||
conftest.py
|
||||
pytest.ini
|
||||
Dockerfile
|
||||
|
||||
21
.travis.yml
21
.travis.yml
@@ -1,21 +0,0 @@
|
||||
language: python
|
||||
python:
|
||||
"2.7"
|
||||
|
||||
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"
|
||||
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install coveralls codeclimate-test-reporter
|
||||
|
||||
before_script:
|
||||
- python manage.py collectstatic --noinput
|
||||
|
||||
script:
|
||||
- coverage run manage.py test RIGS
|
||||
|
||||
after_success:
|
||||
- coveralls
|
||||
- codeclimate-test-reporter
|
||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM python:3.6
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ADD . /app
|
||||
|
||||
RUN pip install -r requirements.txt && \
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||
104
Pipfile
Normal file
104
Pipfile
Normal file
@@ -0,0 +1,104 @@
|
||||
[[source]]
|
||||
url = "https://pypi.python.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
ansicolors = "~=1.1.8"
|
||||
asgiref = "~=3.3.1"
|
||||
"backports.tempfile" = "~=1.0"
|
||||
"backports.weakref" = "~=1.0.post1"
|
||||
beautifulsoup4 = "~=4.9.3"
|
||||
Brotli = "~=1.0.9"
|
||||
cachetools = "~=4.2.1"
|
||||
certifi = "~=2020.12.5"
|
||||
chardet = "~=4.0.0"
|
||||
configparser = "~=5.0.1"
|
||||
contextlib2 = "~=0.6.0.post1"
|
||||
cssselect = "~=1.1.0"
|
||||
cssutils = "~=1.0.2"
|
||||
dj-database-url = "~=0.5.0"
|
||||
dj-static = "~=0.0.6"
|
||||
Django = "~=3.2"
|
||||
django-debug-toolbar = "~=3.2"
|
||||
django-filter = "~=2.4.0"
|
||||
django-ical = "~=1.7.1"
|
||||
django-recurrence = "~=1.10.3"
|
||||
django-registration-redux = "~=2.9"
|
||||
django-reversion = "~=3.0.9"
|
||||
django-toolbelt = "~=0.0.1"
|
||||
django-widget-tweaks = "~=1.4.8"
|
||||
django-htmlmin = "~=0.11.0"
|
||||
envparse = "~=0.2.0"
|
||||
gunicorn = "~=20.0.4"
|
||||
icalendar = "~=4.0.7"
|
||||
idna = "~=2.10"
|
||||
lxml = "~=4.7.1"
|
||||
Markdown = "~=3.3.3"
|
||||
msgpack = "~=1.0.2"
|
||||
pep517 = "~=0.9.1"
|
||||
Pillow = "~=9.0.0"
|
||||
premailer = "~=3.7.0"
|
||||
progress = "~=1.5"
|
||||
psutil = "~=5.8.0"
|
||||
psycopg2 = "~=2.8.6"
|
||||
Pygments = "~=2.7.4"
|
||||
pyparsing = "~=2.4.7"
|
||||
PyPDF2 = "~=1.26.0"
|
||||
PyPOM = "~=2.2.0"
|
||||
python-dateutil = "~=2.8.1"
|
||||
pytoml = "~=0.1.21"
|
||||
pytz = "~=2020.5"
|
||||
reportlab = "~=3.5.59"
|
||||
requests = "~=2.25.1"
|
||||
retrying = "~=1.3.3"
|
||||
simplejson = "~=3.17.2"
|
||||
six = "~=1.15.0"
|
||||
soupsieve = "~=2.1"
|
||||
sqlparse = "~=0.4.2"
|
||||
static3 = "~=0.7.0"
|
||||
svg2rlg = "~=0.3"
|
||||
tini = "~=3.0.1"
|
||||
tornado = "~=6.1"
|
||||
urllib3 = "~=1.26.5"
|
||||
whitenoise = "~=5.2.0"
|
||||
yolk = "~=0.4.3"
|
||||
"z3c.rml" = "~=4.1.2"
|
||||
zipp = "~=3.4.0"
|
||||
"zope.component" = "~=4.6.2"
|
||||
"zope.deferredimport" = "~=4.3.1"
|
||||
"zope.deprecation" = "~=4.4.0"
|
||||
"zope.event" = "~=4.5.0"
|
||||
"zope.hookable" = "~=5.0.1"
|
||||
"zope.interface" = "~=5.2.0"
|
||||
"zope.proxy" = "~=4.3.5"
|
||||
"zope.schema" = "~=6.0.1"
|
||||
sentry-sdk = "*"
|
||||
diff-match-patch = "*"
|
||||
python-barcode = "*"
|
||||
django-hCaptcha = "*"
|
||||
importlib-metadata = "*"
|
||||
django-hcaptcha = "*"
|
||||
|
||||
[dev-packages]
|
||||
selenium = "~=3.141.0"
|
||||
pycodestyle = "*"
|
||||
coveralls = "*"
|
||||
django-coverage-plugin = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-django = "*"
|
||||
pluggy = "*"
|
||||
pytest-splinter = "*"
|
||||
pytest = "*"
|
||||
pytest-reverse = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
||||
|
||||
[dev-packages.pytest-xdist]
|
||||
extras = [ "psutil",]
|
||||
version = "*"
|
||||
|
||||
[dev-packages.PyPOM]
|
||||
extras = [ "splinter",]
|
||||
version = "*"
|
||||
1714
Pipfile.lock
generated
Normal file
1714
Pipfile.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
Procfile
1
Procfile
@@ -1 +1,2 @@
|
||||
release: python manage.py migrate
|
||||
web: gunicorn PyRIGS.wsgi --log-file -
|
||||
|
||||
@@ -1,41 +1,80 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
|
||||
def user_passes_test_with_403(test_func, login_url=None):
|
||||
from RIGS import models
|
||||
|
||||
|
||||
def get_oembed(login_url, request, oembed_view, kwargs):
|
||||
context = {}
|
||||
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'],
|
||||
reverse(oembed_view, kwargs=kwargs))
|
||||
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
||||
resp = render(request, 'login_redirect.html', context=context)
|
||||
return resp
|
||||
|
||||
|
||||
def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
|
||||
def _dec(view_func):
|
||||
def _checklogin(request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
return view_func(request, *args, **kwargs)
|
||||
else:
|
||||
if oembed_view is not None:
|
||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||
else:
|
||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||
|
||||
_checklogin.__doc__ = view_func.__doc__
|
||||
_checklogin.__dict__ = view_func.__dict__
|
||||
return _checklogin
|
||||
|
||||
return _dec
|
||||
|
||||
|
||||
def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
|
||||
"""
|
||||
Decorator for views that checks that the user passes the given test.
|
||||
|
||||
Anonymous users will be redirected to login_url, while users that fail
|
||||
the test will be given a 403 error.
|
||||
If embed_view is set, then a JS redirect will be used, and a application/json+oembed
|
||||
meta tag set with the url of oembed_view
|
||||
(oembed_view will be passed the kwargs from the main function)
|
||||
"""
|
||||
if not login_url:
|
||||
from django.conf import settings
|
||||
login_url = settings.LOGIN_URL
|
||||
|
||||
def _dec(view_func):
|
||||
def _checklogin(request, *args, **kwargs):
|
||||
if test_func(request.user):
|
||||
return view_func(request, *args, **kwargs)
|
||||
elif not request.user.is_authenticated():
|
||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||
elif not request.user.is_authenticated:
|
||||
if oembed_view is not None:
|
||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||
else:
|
||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||
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__
|
||||
_checklogin.__dict__ = view_func.__dict__
|
||||
return _checklogin
|
||||
|
||||
return _dec
|
||||
|
||||
def permission_required_with_403(perm, login_url=None):
|
||||
|
||||
def permission_required_with_403(perm, login_url=None, oembed_view=None):
|
||||
"""
|
||||
Decorator for views that checks whether a user has a particular permission
|
||||
enabled, redirecting to the log-in page or rendering a 403 as necessary.
|
||||
"""
|
||||
return user_passes_test_with_403(lambda u: u.has_perm(perm), login_url=login_url)
|
||||
return user_passes_test_with_403(lambda u: u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)
|
||||
|
||||
from RIGS import models
|
||||
|
||||
def api_key_required(function):
|
||||
"""
|
||||
@@ -43,12 +82,13 @@ def api_key_required(function):
|
||||
Failed users will be given a 403 error.
|
||||
Should only be used for urls which include <api_pk> and <api_key> kwargs
|
||||
"""
|
||||
|
||||
def wrap(request, *args, **kwargs):
|
||||
|
||||
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:
|
||||
@@ -64,4 +104,21 @@ def api_key_required(function):
|
||||
if user_object.api_key != key:
|
||||
return error_resp
|
||||
return function(request, *args, **kwargs)
|
||||
return wrap
|
||||
|
||||
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, 'eventauthorisation_request_error.html')
|
||||
return error_resp
|
||||
|
||||
return function(request, *args, **kwargs)
|
||||
|
||||
return wrap
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
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')
|
||||
|
||||
0
PyRIGS/management/__init__.py
Normal file
0
PyRIGS/management/__init__.py
Normal file
0
PyRIGS/management/commands/__init__.py
Normal file
0
PyRIGS/management/commands/__init__.py
Normal file
@@ -8,90 +8,101 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.7/ref/settings/
|
||||
"""
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from envparse import env
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 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 = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e')
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = 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
|
||||
DEBUG = env('DEBUG', cast=bool, default=True)
|
||||
STAGING = env('STAGING', cast=bool, default=False)
|
||||
CI = env('CI', cast=bool, default=False)
|
||||
|
||||
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
|
||||
|
||||
if STAGING:
|
||||
ALLOWED_HOSTS.append('.herokuapp.com')
|
||||
|
||||
if DEBUG:
|
||||
ALLOWED_HOSTS.append('localhost')
|
||||
ALLOWED_HOSTS.append('example.com')
|
||||
ALLOWED_HOSTS.append('127.0.0.1')
|
||||
|
||||
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']
|
||||
|
||||
ADMINS = (
|
||||
('Tom Price', 'tomtom5152@gmail.com')
|
||||
)
|
||||
|
||||
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
|
||||
('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
|
||||
if DEBUG:
|
||||
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'whitenoise.runserver_nostatic',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.humanize',
|
||||
'versioning',
|
||||
'users',
|
||||
'RIGS',
|
||||
'assets',
|
||||
'training',
|
||||
|
||||
'debug_toolbar',
|
||||
'registration',
|
||||
'reversion',
|
||||
'captcha',
|
||||
'widget_tweaks',
|
||||
'raven.contrib.django.raven_compat',
|
||||
'hcaptcha',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
|
||||
MIDDLEWARE = (
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'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',
|
||||
'htmlmin.middleware.HtmlMinifyMiddleware',
|
||||
'htmlmin.middleware.MarkRequestMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'PyRIGS.urls'
|
||||
|
||||
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
'NAME': str(BASE_DIR / 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
if not DEBUG:
|
||||
import dj_database_url
|
||||
|
||||
DATABASES['default'] = dj_database_url.config()
|
||||
|
||||
# Logging
|
||||
# Logging
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
@@ -119,12 +130,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,43 +150,63 @@ LOGGING = {
|
||||
}
|
||||
}
|
||||
|
||||
import raven
|
||||
# Tests lock up SQLite otherwise
|
||||
if STAGING or CI:
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
|
||||
}
|
||||
}
|
||||
elif DEBUG:
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
|
||||
}
|
||||
}
|
||||
else:
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
|
||||
'LOCATION': 'cache_table',
|
||||
}
|
||||
}
|
||||
|
||||
RAVEN_CONFIG = {
|
||||
'dsn': os.environ.get('RAVEN_DSN'),
|
||||
# If you are using git, you can also automatically configure the
|
||||
# release based on the git info.
|
||||
# 'release': raven.fetch_git_sha(os.path.dirname(os.path.dirname(__file__))),
|
||||
}
|
||||
# Error/performance monitoring
|
||||
sentry_sdk.init(
|
||||
dsn=env('SENTRY_DSN', default=""),
|
||||
integrations=[DjangoIntegration()],
|
||||
traces_sample_rate=1.0,
|
||||
)
|
||||
|
||||
# User system
|
||||
AUTH_USER_MODEL = 'RIGS.Profile'
|
||||
|
||||
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)
|
||||
NOCAPTCHA = True
|
||||
# CAPTCHA settings
|
||||
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001')
|
||||
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000')
|
||||
|
||||
# Email
|
||||
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_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)))
|
||||
EMAIL_USE_SSL = bool(int(os.environ.get('EMAIL_USE_SSL', 0)))
|
||||
DEFAULT_FROM_EMAIL = os.environ.get('EMAIL_FROM')
|
||||
EMAIL_HOST = env('EMAIL_HOST')
|
||||
EMAIL_PORT = env('EMAIL_PORT', cast=int, default=25)
|
||||
EMAIL_HOST_USER = env('EMAIL_HOST_USER')
|
||||
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
|
||||
EMAIL_USE_TLS = env('EMAIL_USE_TLS', cast=bool, default=False)
|
||||
EMAIL_USE_SSL = env('EMAIL_USE_SSL', cast=bool, default=False)
|
||||
DEFAULT_FROM_EMAIL = env('EMAIL_FROM')
|
||||
else:
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
EMAIL_COOLDOWN = datetime.timedelta(minutes=15)
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
||||
|
||||
@@ -191,33 +222,43 @@ 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",
|
||||
)
|
||||
|
||||
# Need to allow seconds as datetime-local input type spits out a time that has seconds
|
||||
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/
|
||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
|
||||
STATIC_DIRS = (
|
||||
os.path.join(BASE_DIR, 'static/')
|
||||
)
|
||||
STATIC_ROOT = str(BASE_DIR / 'static/')
|
||||
STATICFILES_DIRS = [
|
||||
str(BASE_DIR / 'pipeline/built_assets'),
|
||||
]
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
)
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [
|
||||
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'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
0
PyRIGS/tests/__init__.py
Normal file
0
PyRIGS/tests/__init__.py
Normal file
105
PyRIGS/tests/base.py
Normal file
105
PyRIGS/tests/base.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.test import LiveServerTestCase
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
|
||||
from RIGS import models as rigsmodels
|
||||
from . import pages
|
||||
|
||||
from pytest_django.asserts import assertContains
|
||||
|
||||
|
||||
def create_datetime(year, month, day, hour, minute):
|
||||
tz = pytz.timezone(settings.TIME_ZONE)
|
||||
return tz.localize(datetime(year, month, day, hour, minute)).astimezone(tz)
|
||||
|
||||
|
||||
def create_browser():
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument("--window-size=1920,1080")
|
||||
options.add_argument("--headless")
|
||||
if settings.CI:
|
||||
options.add_argument("--no-sandbox")
|
||||
driver = webdriver.Chrome(options=options)
|
||||
return driver
|
||||
|
||||
|
||||
class BaseTest(LiveServerTestCase):
|
||||
def setUp(self):
|
||||
super().setUpClass()
|
||||
self.driver = create_browser()
|
||||
self.wait = WebDriverWait(self.driver, 15)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.driver.quit()
|
||||
|
||||
|
||||
class AutoLoginTest(BaseTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.profile = rigsmodels.Profile(
|
||||
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
|
||||
self.profile.set_password("EventTestPassword")
|
||||
self.profile.save()
|
||||
login_page = pages.LoginPage(self.driver, self.live_server_url).open()
|
||||
login_page.login("EventTest", "EventTestPassword")
|
||||
|
||||
|
||||
# FIXME Refactor as a pytest fixture
|
||||
def screenshot_failure(func):
|
||||
def wrapper_func(self, *args, **kwargs):
|
||||
try:
|
||||
func(self, *args, **kwargs)
|
||||
except Exception as e:
|
||||
screenshot_name = func.__module__ + "." + func.__qualname__
|
||||
screenshot_file = "screenshots/" + func.__qualname__ + ".png"
|
||||
if not pathlib.Path("screenshots").is_dir():
|
||||
os.mkdir("screenshots")
|
||||
self.driver.save_screenshot(screenshot_file)
|
||||
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
|
||||
raise e
|
||||
|
||||
return wrapper_func
|
||||
|
||||
|
||||
def screenshot_failure_cls(cls):
|
||||
for attr in cls.__dict__:
|
||||
if callable(getattr(cls, attr)) and attr.startswith("test"):
|
||||
setattr(cls, attr, screenshot_failure(getattr(cls, attr)))
|
||||
return cls
|
||||
|
||||
|
||||
def assert_times_almost_equal(first_time, second_time):
|
||||
assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0)
|
||||
|
||||
|
||||
def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url):
|
||||
# Test the meta tag is in place
|
||||
response = client.get(event_url, follow=True, HTTP_HOST='example.com')
|
||||
assertContains(response, 'application/json+oembed')
|
||||
assertContains(response, oembed_url)
|
||||
# Test that the JSON exists
|
||||
response = client.get(oembed_url, follow=True, HTTP_HOST='example.com')
|
||||
assert response.status_code == 200
|
||||
assertContains(response, event_embed_url)
|
||||
# Should also work for non-existant events
|
||||
response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
|
||||
assert response.status_code == 200
|
||||
assertContains(response, alt_event_embed_url)
|
||||
|
||||
|
||||
def login(client, django_user_model):
|
||||
pwd = 'testuser'
|
||||
usr = 'TestUser'
|
||||
user = django_user_model.objects.create_user(username=usr, email="TestUser@test.com", password=pwd,
|
||||
is_superuser=True,
|
||||
is_active=True, is_staff=True)
|
||||
assert client.login(username=usr, password=pwd)
|
||||
return user
|
||||
86
PyRIGS/tests/pages.py
Normal file
86
PyRIGS/tests/pages.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from pypom import Page
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from PyRIGS.tests import regions
|
||||
|
||||
|
||||
class BasePage(Page):
|
||||
form_items = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self.form_items:
|
||||
element = self.form_items[name]
|
||||
form_element = element[0](self, self.find_element(*element[1]))
|
||||
return form_element.value
|
||||
else:
|
||||
return super().__getattribute__(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in self.form_items:
|
||||
element = self.form_items[name]
|
||||
form_element = element[0](self, self.find_element(*element[1]))
|
||||
form_element.set_value(value)
|
||||
else:
|
||||
self.__dict__[name] = value
|
||||
|
||||
|
||||
class FormPage(BasePage):
|
||||
_errors_selector = (By.CLASS_NAME, "alert-danger")
|
||||
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
|
||||
|
||||
def remove_all_required(self):
|
||||
self.driver.execute_script(
|
||||
"Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
||||
self.driver.execute_script(
|
||||
"Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
||||
|
||||
def submit(self):
|
||||
previous_errors = self.errors
|
||||
submit = self.find_element(*self._submit_locator)
|
||||
ActionChains(self.driver).move_to_element(submit).perform()
|
||||
submit.click()
|
||||
self.wait.until(animation_is_finished())
|
||||
self.wait.until(lambda x: self.errors != previous_errors or self.success)
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
try:
|
||||
error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector))
|
||||
return error_page.errors
|
||||
except NoSuchElementException:
|
||||
return None
|
||||
|
||||
|
||||
class LoginPage(BasePage):
|
||||
URL_TEMPLATE = '/user/login'
|
||||
|
||||
_username_locator = (By.ID, 'id_username')
|
||||
_password_locator = (By.ID, 'id_password')
|
||||
_submit_locator = (By.ID, 'id_submit')
|
||||
_error_locator = (By.CSS_SELECTOR, '.errorlist>li')
|
||||
|
||||
def login(self, username, password):
|
||||
username_element = self.find_element(*self._username_locator)
|
||||
username_element.clear()
|
||||
username_element.send_keys(username)
|
||||
|
||||
password_element = self.find_element(*self._password_locator)
|
||||
password_element.clear()
|
||||
password_element.send_keys(password)
|
||||
|
||||
self.find_element(*self._submit_locator).click()
|
||||
|
||||
|
||||
class animation_is_finished():
|
||||
def __call__(self, driver):
|
||||
number_animating = driver.execute_script('return $(":animated").length')
|
||||
finished = number_animating == 0
|
||||
if finished:
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
return finished
|
||||
262
PyRIGS/tests/regions.py
Normal file
262
PyRIGS/tests/regions.py
Normal file
@@ -0,0 +1,262 @@
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from pypom import Region
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions
|
||||
from selenium.webdriver.support.select import Select
|
||||
|
||||
|
||||
def parse_bool_from_string(string):
|
||||
# Used to convert from attribute strings to boolean values, written after I found this:
|
||||
# >>> bool("false")
|
||||
# True
|
||||
if string == "true":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_time_format():
|
||||
# Default
|
||||
time_format = "%H%M"
|
||||
if settings.CI: # The CI is American
|
||||
time_format = "%I%M%p"
|
||||
return time_format
|
||||
|
||||
|
||||
def get_date_format():
|
||||
date_format = "%d%m%Y"
|
||||
if settings.CI: # And try as I might I can't stop it being so
|
||||
date_format = "%m%d%Y"
|
||||
return date_format
|
||||
|
||||
|
||||
class BootstrapSelectElement(Region):
|
||||
_main_button_locator = (By.CSS_SELECTOR, 'button.dropdown-toggle')
|
||||
_option_box_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu')
|
||||
_option_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu.inner>li>a.dropdown-item')
|
||||
_select_all_locator = (By.CLASS_NAME, 'bs-select-all')
|
||||
_deselect_all_locator = (By.CLASS_NAME, 'bs-deselect-all')
|
||||
_search_locator = (By.CSS_SELECTOR, '.bs-searchbox>input')
|
||||
_status_locator = (By.CLASS_NAME, 'status')
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
return parse_bool_from_string(self.find_element(*self._main_button_locator).get_attribute("aria-expanded"))
|
||||
|
||||
def toggle(self):
|
||||
original_state = self.is_open
|
||||
option_box = self.find_element(*self._option_box_locator)
|
||||
if not original_state:
|
||||
self.wait.until(expected_conditions.invisibility_of_element(option_box))
|
||||
else:
|
||||
self.wait.until(expected_conditions.visibility_of(option_box))
|
||||
return self.find_element(*self._main_button_locator).click()
|
||||
|
||||
def open(self):
|
||||
if not self.is_open:
|
||||
self.toggle()
|
||||
|
||||
def close(self):
|
||||
if self.is_open:
|
||||
self.toggle()
|
||||
|
||||
def select_all(self):
|
||||
self.find_element(*self._select_all_locator).click()
|
||||
|
||||
def deselect_all(self):
|
||||
self.find_element(*self._deselect_all_locator).click()
|
||||
|
||||
def search(self, query):
|
||||
# self.wait.until(expected_conditions.visibility_of_element_located(self._status_locator))
|
||||
search_box = self.find_element(*self._search_locator)
|
||||
self.open()
|
||||
search_box.clear()
|
||||
search_box.send_keys(query)
|
||||
self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator))
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
options = list(self.find_elements(*self._option_locator))
|
||||
return [self.BootstrapSelectOption(self, i) for i in options]
|
||||
|
||||
def set_option(self, name, selected):
|
||||
options = [x for x in self.options if x.name == name]
|
||||
assert len(options) == 1
|
||||
options[0].set_selected(selected)
|
||||
|
||||
class BootstrapSelectOption(Region):
|
||||
_text_locator = (By.CLASS_NAME, 'text')
|
||||
|
||||
@property
|
||||
def selected(self):
|
||||
return parse_bool_from_string(self.root.get_attribute("aria-selected"))
|
||||
|
||||
def toggle(self):
|
||||
self.root.click()
|
||||
|
||||
def set_selected(self, selected):
|
||||
if self.selected != selected:
|
||||
self.toggle()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.find_element(*self._text_locator).text
|
||||
|
||||
|
||||
class TextBox(Region):
|
||||
@property
|
||||
def value(self):
|
||||
return self.root.get_attribute("value")
|
||||
|
||||
def set_value(self, value):
|
||||
self.root.clear()
|
||||
self.root.send_keys(value)
|
||||
|
||||
|
||||
class SimpleMDETextArea(Region):
|
||||
@property
|
||||
def value(self):
|
||||
return self.driver.execute_script("return document.querySelector('#' + arguments[0]).nextSibling.nextSibling.CodeMirror.getDoc().getValue();", self.root.get_attribute("id"))
|
||||
|
||||
def set_value(self, value):
|
||||
self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.nextSibling.CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
|
||||
|
||||
|
||||
class CheckBox(Region):
|
||||
def toggle(self):
|
||||
self.root.click()
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return parse_bool_from_string(self.root.get_attribute("checked"))
|
||||
|
||||
def set_value(self, value):
|
||||
if value != self.value:
|
||||
self.toggle()
|
||||
|
||||
|
||||
class RadioSelect(Region): # Currently only works for yes/no radio selects
|
||||
def set_value(self, value):
|
||||
if value:
|
||||
value = "0"
|
||||
else:
|
||||
value = "1"
|
||||
self.find_element(By.XPATH, "//label[@for='{}_{}']".format(self.root.get_attribute("id"), value)).click()
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
try:
|
||||
return parse_bool_from_string(self.find_element(By.CSS_SELECTOR, '.custom-control-input:checked').get_attribute("value").lower())
|
||||
except NoSuchElementException:
|
||||
return None
|
||||
|
||||
|
||||
class DatePicker(Region):
|
||||
@property
|
||||
def value(self):
|
||||
return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d")
|
||||
|
||||
def set_value(self, value):
|
||||
self.root.clear()
|
||||
self.root.send_keys(value.strftime(get_date_format()))
|
||||
|
||||
|
||||
class TimePicker(Region):
|
||||
@property
|
||||
def value(self):
|
||||
return datetime.datetime.strptime(self.root.get_attribute("value"), "%H:%M")
|
||||
|
||||
def set_value(self, value):
|
||||
self.root.clear()
|
||||
self.root.send_keys(value.strftime(get_time_format()))
|
||||
|
||||
|
||||
class DateTimePicker(Region):
|
||||
@property
|
||||
def value(self):
|
||||
return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d %H:%M")
|
||||
|
||||
def set_value(self, value):
|
||||
self.root.clear()
|
||||
|
||||
date = value.date().strftime(get_date_format())
|
||||
time = value.time().strftime(get_time_format())
|
||||
|
||||
self.root.send_keys(date)
|
||||
self.root.send_keys(Keys.TAB)
|
||||
self.root.send_keys(time)
|
||||
|
||||
|
||||
class SingleSelectPicker(Region):
|
||||
@property
|
||||
def value(self):
|
||||
picker = Select(self.root)
|
||||
return picker.first_selected_option.text
|
||||
|
||||
def set_value(self, value):
|
||||
picker = Select(self.root)
|
||||
picker.select_by_visible_text(value)
|
||||
|
||||
|
||||
class ErrorPage(Region):
|
||||
_error_item_selector = (By.CSS_SELECTOR, "dl>span")
|
||||
|
||||
class ErrorItem(Region):
|
||||
_field_selector = (By.CSS_SELECTOR, "dt")
|
||||
_error_selector = (By.CSS_SELECTOR, "dd>ul>li")
|
||||
|
||||
@property
|
||||
def field_name(self):
|
||||
return self.find_element(*self._field_selector).text
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
return [x.text for x in self.find_elements(*self._error_selector)]
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
error_items = [self.ErrorItem(self, x) for x in self.find_elements(*self._error_item_selector)]
|
||||
errors = {}
|
||||
for error in error_items:
|
||||
errors[error.field_name] = error.errors
|
||||
return errors
|
||||
|
||||
|
||||
class Modal(Region):
|
||||
_submit_locator = (By.CSS_SELECTOR, '.btn-primary')
|
||||
_header_selector = (By.TAG_NAME, 'h4')
|
||||
|
||||
form_items = {
|
||||
'name': (TextBox, (By.ID, 'id_name'))
|
||||
}
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
return self.find_element(*self._header_selector).text
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
return self.root.is_displayed()
|
||||
|
||||
def submit(self):
|
||||
self.root.find_element(*self._submit_locator).click()
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self.form_items:
|
||||
element = self.form_items[name]
|
||||
form_element = element[0](self, self.find_element(*element[1]))
|
||||
return form_element.value
|
||||
else:
|
||||
return super().__getattribute__(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in self.form_items:
|
||||
element = self.form_items[name]
|
||||
form_element = element[0](self, self.find_element(*element[1]))
|
||||
form_element.set_value(value)
|
||||
else:
|
||||
self.__dict__[name] = value
|
||||
137
PyRIGS/tests/test_unit.py
Normal file
137
PyRIGS/tests/test_unit.py
Normal file
@@ -0,0 +1,137 @@
|
||||
import pytest
|
||||
from django.core.management import call_command
|
||||
from django.template.defaultfilters import striptags
|
||||
from django.urls import URLPattern, URLResolver
|
||||
from django.urls import reverse
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
from pytest_django.asserts import assertRedirects, assertContains, assertNotContains
|
||||
from pytest_django.asserts import assertTemplateUsed, assertInHTML
|
||||
|
||||
from PyRIGS import urls
|
||||
from RIGS.models import Event, Profile
|
||||
from assets.models import Asset
|
||||
from django.db import connection
|
||||
from django.template.defaultfilters import striptags
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
|
||||
def find_urls_recursive(patterns):
|
||||
urls_to_check = []
|
||||
for url in patterns:
|
||||
if isinstance(url, URLResolver):
|
||||
urls_to_check += find_urls_recursive(url.url_patterns)
|
||||
elif isinstance(url, URLPattern):
|
||||
# Skip some things that actually don't need auth (mainly OEmbed JSONs that are essentially just a redirect)
|
||||
if url.name is not None and url.name != "closemodal" and "json" not in str(url):
|
||||
urls_to_check.append(url)
|
||||
return urls_to_check
|
||||
|
||||
|
||||
def get_request_url(url):
|
||||
pattern = str(url.pattern)
|
||||
try:
|
||||
kwargz = {}
|
||||
if ":pk>" in pattern:
|
||||
kwargz['pk'] = 1
|
||||
if ":model>" in pattern:
|
||||
kwargz['model'] = "event"
|
||||
return reverse(url.name, kwargs=kwargz)
|
||||
except NoReverseMatch:
|
||||
print("Couldn't test url " + pattern)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData',
|
||||
'deleteSampleData', 'generateSampleTrainingData', 'generate_sample_training_users'])
|
||||
def test_production_exception(command):
|
||||
from django.core.management.base import CommandError
|
||||
with pytest.raises(CommandError, match=".*production"):
|
||||
call_command(command)
|
||||
|
||||
|
||||
class TestSampleDataGenerator(TestCase):
|
||||
@override_settings(DEBUG=True)
|
||||
def test_sample_data(self):
|
||||
call_command('generateSampleData')
|
||||
assert Asset.objects.all().count() > 50
|
||||
assert Event.objects.all().count() > 100
|
||||
call_command('deleteSampleData')
|
||||
assert Asset.objects.all().count() == 0
|
||||
assert Event.objects.all().count() == 0
|
||||
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@pytest.mark.skip(reason="broken")
|
||||
def test_unauthenticated(client): # Nothing should be available to the unauthenticated
|
||||
call_command('generateSampleData')
|
||||
for url in find_urls_recursive(urls.urlpatterns):
|
||||
request_url = get_request_url(url)
|
||||
if request_url and 'user' not in request_url: # User module is full of edge cases
|
||||
response = client.get(request_url, follow=True, HTTP_HOST='example.com')
|
||||
assertContains(response, 'Login')
|
||||
if 'application/json+oembed' in response.content.decode():
|
||||
assertTemplateUsed(response, 'login_redirect.html')
|
||||
else:
|
||||
if "embed" in str(url):
|
||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||
else:
|
||||
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
|
||||
assertRedirects(response, expected_url)
|
||||
call_command('deleteSampleData')
|
||||
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@pytest.mark.skip(reason="broken")
|
||||
def test_basic_access(client):
|
||||
call_command('generateSampleData')
|
||||
assert client.login(username="basic", password="basic")
|
||||
|
||||
url = reverse('asset_list')
|
||||
response = client.get(url)
|
||||
# Check edit and duplicate buttons NOT shown in list
|
||||
assertNotContains(response, 'Edit')
|
||||
assertNotContains(response,
|
||||
'Duplicate') # If this line is randomly failing, check the debug toolbar HTML hasn't crept in
|
||||
|
||||
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
|
||||
response = client.get(url)
|
||||
assertNotContains(response, 'Purchase Details')
|
||||
assertNotContains(response, 'View Revision History')
|
||||
|
||||
urlz = {'asset_history', 'asset_update', 'asset_duplicate'}
|
||||
for url_name in urlz:
|
||||
request_url = reverse(url_name, kwargs={'pk': Asset.objects.first().asset_id})
|
||||
response = client.get(request_url, follow=True)
|
||||
assert response.status_code == 403
|
||||
|
||||
request_url = reverse('supplier_create')
|
||||
response = client.get(request_url, follow=True)
|
||||
assert response.status_code == 403
|
||||
|
||||
request_url = reverse('supplier_update', kwargs={'pk': 1})
|
||||
response = client.get(request_url, follow=True)
|
||||
assert response.status_code == 403
|
||||
client.logout()
|
||||
call_command('deleteSampleData')
|
||||
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@pytest.mark.skip(reason="broken")
|
||||
def test_keyholder_access(client):
|
||||
call_command('generateSampleData')
|
||||
assert client.login(username="keyholder", password="keyholder")
|
||||
|
||||
url = reverse('asset_list')
|
||||
response = client.get(url)
|
||||
# Check edit and duplicate buttons shown in list
|
||||
assertContains(response, 'Edit')
|
||||
assertContains(response, 'Duplicate')
|
||||
|
||||
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
|
||||
response = client.get(url)
|
||||
assertContains(response, 'Purchase Details')
|
||||
assertContains(response, 'View Revision History')
|
||||
client.logout()
|
||||
call_command('deleteSampleData')
|
||||
@@ -1,24 +1,41 @@
|
||||
from django.conf.urls import patterns, include, url
|
||||
from django.contrib import admin
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.conf import settings
|
||||
from registration.backends.default.views import RegistrationView
|
||||
import RIGS
|
||||
from RIGS import regbackend
|
||||
from django.conf.urls import include
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
urlpatterns = patterns('',
|
||||
# Examples:
|
||||
# url(r'^$', 'PyRIGS.views.home', name='home'),
|
||||
# url(r'^blog/', include('blog.urls')),
|
||||
from PyRIGS import views
|
||||
|
||||
url(r'^', include('RIGS.urls')),
|
||||
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')),
|
||||
urlpatterns = [
|
||||
path('', include('versioning.urls')),
|
||||
path('', include('RIGS.urls')),
|
||||
path('assets/', include('assets.urls')),
|
||||
path('training/', include('training.urls')),
|
||||
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
)
|
||||
path('', login_required(views.Index.as_view()), name='index'),
|
||||
|
||||
# API
|
||||
path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()),
|
||||
name="api_secure"),
|
||||
path('api/<str:model>/<int:pk>/', login_required(views.SecureAPIRequest.as_view()),
|
||||
name="api_secure"),
|
||||
|
||||
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
|
||||
path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'),
|
||||
|
||||
path('', include('users.urls')),
|
||||
|
||||
path('admin/', admin.site.urls),
|
||||
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
import debug_toolbar
|
||||
urlpatterns += [
|
||||
path('__debug__/', include(debug_toolbar.urls)),
|
||||
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
|
||||
]
|
||||
|
||||
269
PyRIGS/views.py
Normal file
269
PyRIGS/views.py
Normal file
@@ -0,0 +1,269 @@
|
||||
import datetime
|
||||
import operator
|
||||
from functools import reduce
|
||||
|
||||
import simplejson
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.core import serializers
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
||||
from django.views import generic
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
|
||||
from RIGS import models
|
||||
from assets import models as asset_models
|
||||
from training import models as training_models
|
||||
|
||||
|
||||
def is_ajax(request):
|
||||
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
|
||||
|
||||
|
||||
class Index(generic.TemplateView): # Displays the current rig count along with a few other bits and pieces
|
||||
template_name = 'index.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(Index, self).get_context_data(**kwargs)
|
||||
context['rig_count'] = models.Event.objects.rig_count()
|
||||
return context
|
||||
|
||||
|
||||
class SecureAPIRequest(generic.View):
|
||||
models = {
|
||||
'venue': models.Venue,
|
||||
'person': models.Person,
|
||||
'organisation': models.Organisation,
|
||||
'profile': models.Profile,
|
||||
'event': models.Event,
|
||||
'supplier': asset_models.Supplier,
|
||||
'training_item': training_models.TrainingItem,
|
||||
}
|
||||
|
||||
perms = {
|
||||
'venue': 'RIGS.view_venue',
|
||||
'person': 'RIGS.view_person',
|
||||
'organisation': 'RIGS.view_organisation',
|
||||
'profile': 'RIGS.view_profile',
|
||||
'event': None,
|
||||
'supplier': None,
|
||||
'training_item': None, # TODO
|
||||
}
|
||||
|
||||
'''
|
||||
Validate the request is allowed based on user permissions.
|
||||
Raises 403 if denied.
|
||||
Potential to add API key validation at a later date.
|
||||
'''
|
||||
|
||||
def __validate__(self, request, key, perm):
|
||||
if request.user.is_active:
|
||||
if request.user.is_superuser or perm is None:
|
||||
return True
|
||||
elif request.user.has_perm(perm):
|
||||
return True
|
||||
raise PermissionDenied()
|
||||
|
||||
def get(self, request, model, pk=None, param=None):
|
||||
# Request permission validation things
|
||||
key = request.GET.get('apikey', None)
|
||||
perm = self.perms[model]
|
||||
self.__validate__(request, key, perm)
|
||||
|
||||
# Response format where applicable
|
||||
format = request.GET.get('format', 'json')
|
||||
fields = request.GET.get('fields', None)
|
||||
if fields:
|
||||
fields = fields.split(",")
|
||||
filters = request.GET.get('filters', [])
|
||||
if filters:
|
||||
filters = filters.split(",")
|
||||
|
||||
# Supply data for one record
|
||||
if pk:
|
||||
object = get_object_or_404(self.models[model], pk=pk)
|
||||
data = serializers.serialize(format, [object], fields=fields)
|
||||
return HttpResponse(data, content_type="application/" + format)
|
||||
|
||||
# Supply data for autocomplete ajax request in json form
|
||||
term = request.GET.get('q', None)
|
||||
if term:
|
||||
if fields is None: # Default to just name
|
||||
fields = ['name']
|
||||
|
||||
# Build a list of Q objects for use later
|
||||
queries = []
|
||||
for part in term.split(" "):
|
||||
qs = []
|
||||
for field in fields:
|
||||
q = Q(**{field + "__icontains": part})
|
||||
qs.append(q)
|
||||
|
||||
queries.append(reduce(operator.or_, qs))
|
||||
|
||||
for f in filters:
|
||||
q = Q(**{f: True})
|
||||
queries.append(q)
|
||||
|
||||
# Build the data response list
|
||||
results = []
|
||||
query = reduce(operator.and_, queries)
|
||||
objects = self.models[model].objects.filter(query)
|
||||
for o in objects:
|
||||
data = {
|
||||
'pk': o.pk,
|
||||
'value': o.pk,
|
||||
'text': o.name,
|
||||
}
|
||||
try: # See if there is a valid update URL
|
||||
data['update'] = reverse("%s_update" % model, kwargs={'pk': o.pk})
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
results.append(data)
|
||||
|
||||
# return a data response
|
||||
json = simplejson.dumps(results)
|
||||
return HttpResponse(json, content_type="application/json") # Always json
|
||||
|
||||
start = request.GET.get('start', None)
|
||||
end = request.GET.get('end', None)
|
||||
|
||||
if model == "event" and start and end:
|
||||
# Probably a calendar request
|
||||
start_datetime = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S")
|
||||
end_datetime = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
objects = self.models[model].objects.events_in_bounds(start_datetime, end_datetime)
|
||||
|
||||
results = []
|
||||
for item in objects:
|
||||
data = {
|
||||
'pk': item.pk,
|
||||
'title': item.name,
|
||||
'is_rig': item.is_rig,
|
||||
'status': str(item.get_status_display()),
|
||||
'earliest': item.earliest_time.isoformat(),
|
||||
'latest': item.latest_time.isoformat(),
|
||||
'url': str(item.get_absolute_url())
|
||||
}
|
||||
|
||||
results.append(data)
|
||||
json = simplejson.dumps(results)
|
||||
return HttpResponse(json, content_type="application/json") # Always json
|
||||
|
||||
return HttpResponse(model)
|
||||
|
||||
|
||||
class ModalURLMixin:
|
||||
def get_close_url(self, update, detail):
|
||||
if is_ajax(self.request):
|
||||
url = reverse_lazy('closemodal')
|
||||
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
|
||||
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
|
||||
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
|
||||
else:
|
||||
url = reverse_lazy(detail, kwargs={
|
||||
'pk': self.object.pk,
|
||||
})
|
||||
return url
|
||||
|
||||
|
||||
class GenericListView(generic.ListView):
|
||||
template_name = 'generic_list.html'
|
||||
paginate_by = 20
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(GenericListView, self).get_context_data(**kwargs)
|
||||
context['page_title'] = self.model.__name__ + "s"
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
q = self.request.GET.get('q', "")
|
||||
|
||||
filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(
|
||||
phone__startswith=q) | Q(phone__endswith=q)
|
||||
|
||||
# try and parse an int
|
||||
try:
|
||||
val = int(q)
|
||||
filter = filter | Q(pk=val)
|
||||
except: # noqa
|
||||
# not an integer
|
||||
pass
|
||||
|
||||
object_list = self.model.objects.filter(filter)
|
||||
|
||||
orderBy = self.request.GET.get('orderBy', "name")
|
||||
if orderBy != "":
|
||||
object_list = object_list.order_by(orderBy)
|
||||
return object_list
|
||||
|
||||
|
||||
class GenericDetailView(generic.DetailView):
|
||||
template_name = "generic_detail.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(GenericDetailView, self).get_context_data(**kwargs)
|
||||
context['page_title'] = "{} | {}".format(self.model.__name__, self.object.name)
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
return context
|
||||
|
||||
|
||||
class GenericUpdateView(generic.UpdateView):
|
||||
template_name = "generic_form.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(GenericUpdateView, self).get_context_data(**kwargs)
|
||||
context['page_title'] = "Edit {}".format(self.model.__name__)
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
return context
|
||||
|
||||
|
||||
class GenericCreateView(generic.CreateView):
|
||||
template_name = "generic_form.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(GenericCreateView, self).get_context_data(**kwargs)
|
||||
context['page_title'] = "Create {}".format(self.model.__name__)
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
return context
|
||||
|
||||
|
||||
class SearchHelp(generic.TemplateView):
|
||||
template_name = 'search_help.html'
|
||||
|
||||
|
||||
class CloseModal(generic.TemplateView):
|
||||
"""
|
||||
Called from a modal window (e.g. when an item is submitted to an event/invoice).
|
||||
May optionally also include some javascript in a success message to cause a load of
|
||||
the new information onto the page.
|
||||
"""
|
||||
template_name = 'closemodal.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return {'messages': messages.get_messages(self.request)}
|
||||
|
||||
|
||||
class OEmbedView(generic.View):
|
||||
def get(self, request, pk=None):
|
||||
embed_url = reverse(self.url_name, args=[pk])
|
||||
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
|
||||
|
||||
data = {
|
||||
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
|
||||
'version': '1.0',
|
||||
'type': 'rich',
|
||||
'height': '250'
|
||||
}
|
||||
|
||||
json = simplejson.JSONEncoderForHTML().encode(data)
|
||||
return HttpResponse(json, content_type="application/json")
|
||||
@@ -7,9 +7,10 @@ For more information on this file, see
|
||||
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())
|
||||
|
||||
104
README.md
104
README.md
@@ -1,96 +1,18 @@
|
||||
# TEC PA & Lighting - PyRIGS #
|
||||
[](https://travis-ci.org/nottinghamtec/PyRIGS)
|
||||
[](https://coveralls.io/github/nottinghamtec/PyRIGS?branch=develop)
|
||||

|
||||
[](https://coveralls.io/github/nottinghamtec/PyRIGS)
|
||||
[](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability)
|
||||
|
||||
Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails.
|
||||
Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets.
|
||||
|
||||
The purpose of this project is to make the system more compatible and easier to understand such that should future changes be needed they can be made without having to understand the intricacies of Rails.
|
||||
For setup information and other such helpful stuff check the [Wiki](https://github.com/nottinghamtec/PyRIGS/wiki)
|
||||
|
||||
At this stage the project is very early on, and the main focus has been on getting a working system that can be tested and put into use ASAP due to the imminent failure of the existing system. Because of this, the documentation is still quite weak, but this should be fixed as time goes on.
|
||||
# Apps
|
||||
- PyRIGS: Base app, stores 'global' information
|
||||
- RIGS: Rigboard stuff - event calendar etc
|
||||
- assets: Database of our kit, testing data etc
|
||||
- versioning: Our custom logic built on top of django-reversion. Semi-modular.
|
||||
- users: Our custom logic for registration and profiles. Semi-modular.
|
||||
- training: SoonTM
|
||||
|
||||
This document is intended to get you up and running, but if don't care about what I have to say, just clone the sodding repository and have a poke around with what's in it, but for GODS SAKE DO NOT PUSH WITHOUT TESTING.
|
||||
|
||||
### What is this repository for? ###
|
||||
For the rapid development of the application for medium term deployment, the main branch is being used.
|
||||
Once the application is deployed in a production environment, other branches should be used to properly stage edits and pushes of new features. When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production.
|
||||
|
||||
Most of the documents here assume a basic knowledge of how Python and Django work (hint, if I don't say something, Google it, you will find 10000's of answers). The documentation is purely to be specific to TEC's application of the framework.
|
||||
|
||||
### Editing ###
|
||||
It is recommended that you use the PyCharm IDE by JetBrains. Whilst other editors are available, this is the best for integration with Django as it can automatically manage all the pesky admin commands that frequently need running, as well as nice integration with git.
|
||||
|
||||
For the more experienced developer/somebody who doesn't want a full IDE and wants it to open in less than the age of the universe, I can strongly recommend [Sublime Text](http://www.sublimetext.com/). It has a bit of a steeper learning curve, and won't manage anything Django/git related out of the box, but once you get the hang of it is by far the fastest and most powerful editor I have used (for any type of project).
|
||||
|
||||
Please contact TJP for details on how to acquire these.
|
||||
|
||||
### Python Environment ###
|
||||
Whilst the Python version used is not critical to the running of the application, using the same version usually helps avoid a lot of issues. Mainly the C implementation of Python 2 (CPython 2) has been used (specifically the Python 2.7 standard). Most of the application has been written with Python 3 in mind however, and should run without issue. Some level of testing on Python 3 has been done, but there is no guarantee it will work (for more information on this please see [[Python Version]] on the wiki)
|
||||
|
||||
Once you have your Python distribution installed, go ahead an follow the steps to set up a virtualenv, which will isolate the project from the system environment.
|
||||
|
||||
#### PyCharm ####
|
||||
If you are using the prefered PyCharm IDE, then this should be quite easy.
|
||||
|
||||
1. Select "File/Settings" -> "Project Interpreter"
|
||||
2. Click the small cog in the top right
|
||||
3. Select "Create VirtualEnv"
|
||||
4. Enter a name and a location. This doesn't matter where, just make sure it makes sense and you remember it incase you need it later (I recommend calling it "pyrigs" in "~/.virtualenvs/pyrigs")
|
||||
5. Select the base interpreter to your Python 3 base interpreter (Python 2 will work, just be careful)
|
||||
6. Click OK, you *don't* want to inherit global packages or make it available to all projects.
|
||||
7. Open a file such as manage.py. PyCharm should winge that dependances aren't installed. This might take a while to register, but give it change. When it does, click the button to install them and let it do it's thing. If for some reason PyCharm should decide that it doesn't want to help you here, see below for the console instructions on how to do this manually.
|
||||
|
||||
To run the Django application follow these steps
|
||||
|
||||
1. Select "Run/Edit Configurations"
|
||||
2. Create a new "Django server", give it a sensible name for when you need it later.
|
||||
3. You might need to set the interpreter to be your virtualenv.
|
||||
4. Click "OK"
|
||||
5. Run the application
|
||||
|
||||
#### Console Based ####
|
||||
If you aren't using PyCharm, or want to use a console for some reason, this is really easy, there is even [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) to help things along. Simply run
|
||||
```
|
||||
virtualenv <dir>
|
||||
```
|
||||
Where dir is the directory you wish to create the virtualenv in.
|
||||
|
||||
Next activate the virtualenv.
|
||||
```
|
||||
Windows
|
||||
<virtualenv_dir>/Scripts/activate.bat
|
||||
|
||||
Unix
|
||||
source <virtualenv_dir>/bin/activate
|
||||
```
|
||||
Finally install the requirements using pip
|
||||
```
|
||||
cd <pyrigs project directory>
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
This might take a while, but be patient and you should then be ready to go.
|
||||
|
||||
To run the server under normal conditions when you are already in the virtualenv (see above)
|
||||
```
|
||||
python manage.py runserver
|
||||
```
|
||||
Please refer to Django documentation for a full list of options available here.
|
||||
|
||||
### Sample Data ###
|
||||
Sample data is available to aid local development and user acceptance testing. To load this data into your local database, first ensure the database is empty:
|
||||
```
|
||||
python manage.py flush
|
||||
```
|
||||
Then load the sample data using the command:
|
||||
```
|
||||
python manage.py generateSampleData
|
||||
```
|
||||
4 user accounts are created for convenience:
|
||||
|Username |Password |
|
||||
|---------|---------|
|
||||
|superuser|superuser|
|
||||
|finance |finance |
|
||||
|keyholder|keyholder|
|
||||
|basic |basic |
|
||||
|
||||
### 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.
|
||||
[](https://forthebadge.com) [](https://forthebadge.com)
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
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 django.contrib.admin import helpers
|
||||
from django.template.response import TemplateResponse
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.contrib.admin import helpers
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from django.forms import ModelForm
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from reversion import revisions as reversion
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
# 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.Invoice)
|
||||
admin.site.register(models.Payment)
|
||||
from RIGS import models
|
||||
from users import forms as user_forms
|
||||
|
||||
|
||||
admin.site.register(models.VatRate, VersionAdmin)
|
||||
admin.site.register(models.Event, VersionAdmin)
|
||||
admin.site.register(models.EventItem, VersionAdmin)
|
||||
admin.site.register(models.Invoice, VersionAdmin)
|
||||
|
||||
|
||||
def approve_user(modeladmin, request, queryset):
|
||||
queryset.update(is_approved=True)
|
||||
|
||||
|
||||
approve_user.short_description = "Approve selected users"
|
||||
|
||||
|
||||
@admin.register(models.Profile)
|
||||
class ProfileAdmin(UserAdmin):
|
||||
# Don't know how to add 'is_approved' whilst preserving the default list...
|
||||
list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password')}),
|
||||
(_('Personal info'), {
|
||||
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
|
||||
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
|
||||
(_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
|
||||
'groups', 'user_permissions')}),
|
||||
(_('Important dates'), {
|
||||
'fields': ('last_login', 'date_joined')}),
|
||||
@@ -37,11 +47,12 @@ class ProfileAdmin(UserAdmin):
|
||||
'fields': ('username', 'password1', 'password2'),
|
||||
}),
|
||||
)
|
||||
form = forms.ProfileChangeForm
|
||||
add_form = forms.ProfileCreationForm
|
||||
form = user_forms.ProfileChangeForm
|
||||
add_form = user_forms.ProfileCreationForm
|
||||
actions = [approve_user]
|
||||
|
||||
|
||||
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 +104,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, 'admin_associate_merge.html', context)
|
||||
|
||||
|
||||
@admin.register(models.Person)
|
||||
@@ -113,3 +123,13 @@ class VenueAdmin(AssociateAdmin):
|
||||
class OrganisationAdmin(AssociateAdmin):
|
||||
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
|
||||
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account']
|
||||
|
||||
|
||||
@admin.register(models.RiskAssessment)
|
||||
class RiskAssessmentAdmin(VersionAdmin):
|
||||
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
|
||||
|
||||
|
||||
@admin.register(models.EventChecklist)
|
||||
class EventChecklistAdmin(VersionAdmin):
|
||||
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
|
||||
|
||||
8
RIGS/apps.py
Normal file
8
RIGS/apps.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RIGSAppConfig(AppConfig):
|
||||
name = 'RIGS'
|
||||
|
||||
def ready(self):
|
||||
import RIGS.signals
|
||||
175
RIGS/finance.py
175
RIGS/finance.py
@@ -1,84 +1,79 @@
|
||||
import cStringIO as StringIO
|
||||
import datetime
|
||||
import re
|
||||
|
||||
import reversion
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.views import generic
|
||||
from django.db.models import Q
|
||||
from z3c.rml import rml2pdf
|
||||
|
||||
from RIGS import models
|
||||
|
||||
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
|
||||
|
||||
|
||||
class InvoiceIndex(generic.ListView):
|
||||
model = models.Invoice
|
||||
template_name = 'RIGS/invoice_list_active.html'
|
||||
template_name = 'invoice_list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(InvoiceIndex, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
total = 0
|
||||
for i in context['object_list']:
|
||||
total += i.balance
|
||||
context['total'] = total
|
||||
context['count'] = len(list(context['object_list']))
|
||||
context['page_title'] = "Outstanding Invoices ({} Events, £{:.2f})".format(len(list(context['object_list'])), total)
|
||||
context['description'] = "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger"
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||
sql = "SELECT * FROM " \
|
||||
"(SELECT " \
|
||||
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
||||
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
||||
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
||||
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
||||
"AS sub " \
|
||||
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
||||
"ORDER BY invoice_date"
|
||||
|
||||
query = self.model.objects.raw(sql)
|
||||
|
||||
return query
|
||||
return self.model.objects.outstanding_invoices()
|
||||
|
||||
|
||||
class InvoiceDetail(generic.DetailView):
|
||||
model = models.Invoice
|
||||
template_name = 'invoice_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
|
||||
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date}) "
|
||||
if self.object.void:
|
||||
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
|
||||
elif self.object.is_closed:
|
||||
context['page_title'] += "<span class='badge badge-success float-right'>PAID</span>"
|
||||
else:
|
||||
context['page_title'] += "<span class='badge badge-info float-right'>OUTSTANDING</span>"
|
||||
return context
|
||||
|
||||
|
||||
class InvoicePrint(generic.View):
|
||||
def get(self, request, pk):
|
||||
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, {
|
||||
template = get_template('event_print.xml')
|
||||
|
||||
context = {
|
||||
'object': object,
|
||||
'fonts': {
|
||||
'opensans': {
|
||||
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
|
||||
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
|
||||
}
|
||||
},
|
||||
'invoice': invoice,
|
||||
'current_user': request.user,
|
||||
})
|
||||
'filename': 'Invoice {} for {} {}.pdf'.format(invoice.display_id, object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name))
|
||||
}
|
||||
|
||||
rml = template.render(context)
|
||||
buffer = StringIO.StringIO()
|
||||
|
||||
buffer = rml2pdf.parseString(rml)
|
||||
|
||||
pdfData = buffer.read()
|
||||
|
||||
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="{}"'.format(context['filename'])
|
||||
response.write(pdfData)
|
||||
return response
|
||||
|
||||
@@ -91,72 +86,95 @@ class InvoiceVoid(generic.View):
|
||||
object.save()
|
||||
|
||||
if object.void:
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_list'))
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
|
||||
return HttpResponseRedirect(reverse('invoice_list'))
|
||||
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': object.pk}))
|
||||
|
||||
|
||||
class InvoiceDelete(generic.DeleteView):
|
||||
model = models.Invoice
|
||||
template_name = 'invoice_confirm_delete.html'
|
||||
|
||||
def get(self, request, pk):
|
||||
obj = self.get_object()
|
||||
if obj.payment_set.all().count() > 0:
|
||||
messages.info(self.request, 'To delete an invoice, delete the payments first.')
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
|
||||
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk}))
|
||||
return super(InvoiceDelete, self).get(pk)
|
||||
|
||||
def post(self, request, pk):
|
||||
obj = self.get_object()
|
||||
if obj.payment_set.all().count() > 0:
|
||||
messages.info(self.request, 'To delete an invoice, delete the payments first.')
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
|
||||
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk}))
|
||||
return super(InvoiceDelete, self).post(pk)
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.POST.get('next')
|
||||
|
||||
|
||||
class InvoiceArchive(generic.ListView):
|
||||
model = models.Invoice
|
||||
template_name = 'RIGS/invoice_list_archive.html'
|
||||
template_name = 'invoice_list_archive.html'
|
||||
paginate_by = 25
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['page_title'] = "Invoice Archive"
|
||||
context['description'] = "This page displays all invoices: outstanding, paid, and void"
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
q = self.request.GET.get('q', "")
|
||||
|
||||
filter = Q(event__name__icontains=q)
|
||||
|
||||
# try and parse an int
|
||||
try:
|
||||
val = int(q)
|
||||
filter = filter | Q(pk=val)
|
||||
filter = filter | Q(event__pk=val)
|
||||
except: # noqa
|
||||
# not an integer
|
||||
pass
|
||||
|
||||
try:
|
||||
if q[0] == "N":
|
||||
val = int(q[1:])
|
||||
filter = Q(event__pk=val) # If string is Nxxxxx then filter by event number
|
||||
elif q[0] == "#":
|
||||
val = int(q[1:])
|
||||
filter = Q(pk=val) # If string is #xxxxx then filter by invoice number
|
||||
except: # noqa
|
||||
pass
|
||||
|
||||
object_list = self.model.objects.filter(filter).order_by('-invoice_date')
|
||||
|
||||
return object_list
|
||||
|
||||
|
||||
class InvoiceWaiting(generic.ListView):
|
||||
model = models.Event
|
||||
# paginate_by = 25
|
||||
template_name = 'RIGS/event_invoice.html'
|
||||
paginate_by = 25
|
||||
template_name = 'invoice_list_waiting.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(InvoiceWaiting, self).get_context_data(**kwargs)
|
||||
total = 0
|
||||
for obj in self.get_objects():
|
||||
objects = self.get_queryset()
|
||||
for obj in objects:
|
||||
total += obj.sum_total
|
||||
context['total'] = total
|
||||
context['count'] = len(self.get_objects())
|
||||
context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(objects), total)
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_objects()
|
||||
|
||||
def get_objects(self):
|
||||
# @todo find a way to select items
|
||||
events = self.model.objects.filter(
|
||||
(
|
||||
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
||||
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
|
||||
) & Q(invoice__isnull=True) # Has not already been invoiced
|
||||
& Q(is_rig=True) # Is a rig (not non-rig)
|
||||
|
||||
).order_by('start_date') \
|
||||
.select_related('person',
|
||||
'organisation',
|
||||
'venue', 'mic') \
|
||||
.prefetch_related('items')
|
||||
|
||||
return events
|
||||
return self.model.objects.waiting_invoices()
|
||||
|
||||
|
||||
class InvoiceEvent(generic.View):
|
||||
@transaction.atomic()
|
||||
@reversion.create_revision()
|
||||
def get(self, *args, **kwargs):
|
||||
reversion.set_user(self.request.user)
|
||||
epk = kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
invoice, created = models.Invoice.objects.get_or_create(event=event)
|
||||
@@ -165,29 +183,50 @@ class InvoiceEvent(generic.View):
|
||||
invoice.invoice_date = datetime.date.today()
|
||||
messages.success(self.request, 'Invoice created successfully')
|
||||
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))
|
||||
if kwargs.get('void'):
|
||||
invoice.void = not invoice.void
|
||||
invoice.save()
|
||||
messages.warning(self.request, 'Invoice voided')
|
||||
|
||||
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': invoice.pk}))
|
||||
|
||||
|
||||
class PaymentCreate(generic.CreateView):
|
||||
model = models.Payment
|
||||
fields = ['invoice', 'date', 'amount', 'method']
|
||||
template_name = 'payment_form.html'
|
||||
|
||||
def get_initial(self):
|
||||
initial = super(generic.CreateView, self).get_initial()
|
||||
initial = super().get_initial()
|
||||
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
|
||||
if invoicepk == None:
|
||||
if invoicepk is None:
|
||||
raise Http404()
|
||||
invoice = get_object_or_404(models.Invoice, pk=invoicepk)
|
||||
initial.update({'invoice': invoice})
|
||||
return initial
|
||||
|
||||
@transaction.atomic()
|
||||
@reversion.create_revision()
|
||||
def form_valid(self, form, *args, **kwargs):
|
||||
reversion.add_to_revision(form.cleaned_data['invoice'])
|
||||
reversion.set_comment("Payment added")
|
||||
return super().form_valid(form, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
messages.info(self.request, "location.reload()")
|
||||
return reverse_lazy('closemodal')
|
||||
return reverse('closemodal')
|
||||
|
||||
|
||||
class PaymentDelete(generic.DeleteView):
|
||||
model = models.Payment
|
||||
template_name = 'payment_confirm_delete.html'
|
||||
|
||||
@transaction.atomic()
|
||||
@reversion.create_revision()
|
||||
def delete(self, *args, **kwargs):
|
||||
reversion.add_to_revision(self.get_object().invoice)
|
||||
reversion.set_comment("Payment removed")
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.POST.get('next')
|
||||
|
||||
218
RIGS/forms.py
218
RIGS/forms.py
@@ -1,51 +1,24 @@
|
||||
__author__ = 'Ghost'
|
||||
from datetime import datetime
|
||||
|
||||
import simplejson
|
||||
from django import forms
|
||||
from django.utils import formats
|
||||
from django.conf import settings
|
||||
from django.core import serializers
|
||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
||||
from registration.forms import RegistrationFormUniqueEmail
|
||||
from captcha.fields import ReCaptchaField
|
||||
import simplejson
|
||||
from django.utils import timezone
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from RIGS import models
|
||||
from training.models import TrainingLevel
|
||||
|
||||
|
||||
# Registration
|
||||
class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
|
||||
captcha = ReCaptchaField()
|
||||
|
||||
class Meta:
|
||||
model = models.Profile
|
||||
fields = ('username', 'email', 'first_name', 'last_name', 'initials', 'phone')
|
||||
|
||||
def clean_initials(self):
|
||||
"""
|
||||
Validate that the supplied initials are unique.
|
||||
"""
|
||||
if models.Profile.objects.filter(initials__iexact=self.cleaned_data['initials']):
|
||||
raise forms.ValidationError("These initials are already in use. Please supply different initials.")
|
||||
return self.cleaned_data['initials']
|
||||
|
||||
|
||||
# Login form
|
||||
class PasswordReset(PasswordResetForm):
|
||||
captcha = ReCaptchaField(label='Captcha')
|
||||
|
||||
|
||||
class ProfileCreationForm(UserCreationForm):
|
||||
class Meta(UserCreationForm.Meta):
|
||||
model = models.Profile
|
||||
|
||||
|
||||
class ProfileChangeForm(UserChangeForm):
|
||||
class Meta(UserChangeForm.Meta):
|
||||
model = models.Profile
|
||||
# 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.TimeInput(attrs={'type': 'time'}, format='%H:%M')
|
||||
forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M')
|
||||
|
||||
|
||||
# Events Shit
|
||||
class EventForm(forms.ModelForm):
|
||||
datetime_input_formats = formats.get_format_lazy("DATETIME_INPUT_FORMATS") + settings.DATETIME_INPUT_FORMATS
|
||||
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)
|
||||
|
||||
@@ -118,8 +91,16 @@ class EventForm(forms.ModelForm):
|
||||
|
||||
return item
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data.get("is_rig") and not (
|
||||
self.cleaned_data.get('person') or self.cleaned_data.get('organisation')):
|
||||
raise forms.ValidationError(
|
||||
'You haven\'t provided any client contact details. Please add a person or organisation.',
|
||||
code='contact')
|
||||
return super().clean()
|
||||
|
||||
def save(self, commit=True):
|
||||
m = super(EventForm, self).save(commit=False)
|
||||
m = super().save(commit=False)
|
||||
|
||||
if (commit):
|
||||
m.save()
|
||||
@@ -140,4 +121,161 @@ 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().__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')
|
||||
|
||||
|
||||
class EventRiskAssessmentForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for name, field in self.fields.items():
|
||||
if str(name) == 'supervisor_consulted':
|
||||
field.widget = forms.CheckboxInput()
|
||||
elif field.__class__ == forms.BooleanField:
|
||||
field.widget = forms.RadioSelect(choices=[
|
||||
(True, 'Yes'),
|
||||
(False, 'No')
|
||||
], attrs={'class': 'custom-control-input', 'required': 'true'})
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data.get('big_power'):
|
||||
if not self.cleaned_data.get('power_mic').level_qualifications.filter(level__department=TrainingLevel.POWER).exists():
|
||||
self.add_error('power_mic', forms.ValidationError("Your Power MIC must be a Power Technician.", code="power_tech_required"))
|
||||
# Check expected values
|
||||
unexpected_values = []
|
||||
for field, value in models.RiskAssessment.expected_values.items():
|
||||
if self.cleaned_data.get(field) != value:
|
||||
unexpected_values.append("<li>{}</li>".format(self._meta.model._meta.get_field(field).help_text))
|
||||
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
|
||||
raise forms.ValidationError("Your answers to these questions: <ul>{}</ul> require consulting with a supervisor.".format(''.join([str(elem) for elem in unexpected_values])), code='unusual_answers')
|
||||
return super(EventRiskAssessmentForm, self).clean()
|
||||
|
||||
class Meta:
|
||||
model = models.RiskAssessment
|
||||
fields = '__all__'
|
||||
exclude = ['reviewed_at', 'reviewed_by']
|
||||
|
||||
|
||||
class EventChecklistForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['date'].widget.format = '%Y-%m-%d'
|
||||
for name, field in self.fields.items():
|
||||
if field.__class__ == forms.NullBooleanField:
|
||||
# Only display yes/no to user, the 'none' is only ever set in the background
|
||||
field.widget = forms.CheckboxInput()
|
||||
# Parsed from incoming form data by clean, then saved into models when the form is saved
|
||||
items = {}
|
||||
|
||||
related_models = {
|
||||
'venue': models.Venue,
|
||||
'power_mic': models.Profile,
|
||||
}
|
||||
|
||||
# Two possible formats
|
||||
def parsedatetime(self, date_string):
|
||||
try:
|
||||
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S'))
|
||||
except ValueError:
|
||||
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M'))
|
||||
|
||||
# There's probably a thousand better ways to do this, but this one is mine
|
||||
def clean(self):
|
||||
vehicles = {key: val for key, val in self.data.items()
|
||||
if key.startswith('vehicle')}
|
||||
for key in vehicles:
|
||||
pk = int(key.split('_')[1])
|
||||
driver_key = 'driver_' + str(pk)
|
||||
if(self.data[driver_key] == ''):
|
||||
raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch')
|
||||
else:
|
||||
try:
|
||||
item = models.EventChecklistVehicle.objects.get(pk=pk)
|
||||
except models.EventChecklistVehicle.DoesNotExist:
|
||||
item = models.EventChecklistVehicle()
|
||||
|
||||
item.vehicle = vehicles['vehicle_' + str(pk)]
|
||||
item.driver = models.Profile.objects.get(pk=self.data[driver_key])
|
||||
item.full_clean('checklist')
|
||||
|
||||
# item does not have a database pk yet as it isn't saved
|
||||
self.items['v' + str(pk)] = item
|
||||
|
||||
crewmembers = {key: val for key, val in self.data.items()
|
||||
if key.startswith('crewmember')}
|
||||
other_fields = ['start', 'role', 'end']
|
||||
for key in crewmembers:
|
||||
pk = int(key.split('_')[1])
|
||||
|
||||
for field in other_fields:
|
||||
value = self.data['{}_{}'.format(field, pk)]
|
||||
if value == '':
|
||||
raise forms.ValidationError('Add a {} to crewmember {}'.format(field, pk), code='{}_mismatch'.format(field))
|
||||
|
||||
try:
|
||||
item = models.EventChecklistCrew.objects.get(pk=pk)
|
||||
except models.EventChecklistCrew.DoesNotExist:
|
||||
item = models.EventChecklistCrew()
|
||||
|
||||
item.crewmember = models.Profile.objects.get(pk=self.data['crewmember_' + str(pk)])
|
||||
item.start = self.parsedatetime(self.data['start_' + str(pk)])
|
||||
item.role = self.data['role_' + str(pk)]
|
||||
item.end = self.parsedatetime(self.data['end_' + str(pk)])
|
||||
item.full_clean('checklist')
|
||||
|
||||
# item does not have a database pk yet as it isn't saved
|
||||
self.items['c' + str(pk)] = item
|
||||
|
||||
return super(EventChecklistForm, self).clean()
|
||||
|
||||
def save(self, commit=True):
|
||||
checklist = super(EventChecklistForm, self).save(commit=False)
|
||||
if (commit):
|
||||
# Remove all existing, to be recreated from the form
|
||||
checklist.vehicles.all().delete()
|
||||
checklist.crew.all().delete()
|
||||
checklist.save()
|
||||
|
||||
for key in self.items:
|
||||
item = self.items[key]
|
||||
reversion.add_to_revision(item)
|
||||
# finish and save new database items
|
||||
item.checklist = checklist
|
||||
item.full_clean()
|
||||
item.save()
|
||||
|
||||
self.items.clear()
|
||||
|
||||
return checklist
|
||||
|
||||
class Meta:
|
||||
model = models.EventChecklist
|
||||
fields = '__all__'
|
||||
exclude = ['reviewed_at', 'reviewed_by']
|
||||
|
||||
226
RIGS/hs.py
Normal file
226
RIGS/hs.py
Normal file
@@ -0,0 +1,226 @@
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.views import generic
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from RIGS import models, forms
|
||||
|
||||
|
||||
class EventRiskAssessmentCreate(generic.CreateView):
|
||||
model = models.RiskAssessment
|
||||
template_name = 'risk_assessment_form.html'
|
||||
form_class = forms.EventRiskAssessmentForm
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
epk = kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
|
||||
# Check if RA exists
|
||||
ra = models.RiskAssessment.objects.filter(event=event).first()
|
||||
|
||||
if ra is not None:
|
||||
return HttpResponseRedirect(reverse_lazy('ra_edit', kwargs={'pk': ra.pk}))
|
||||
|
||||
return super(EventRiskAssessmentCreate, self).get(self)
|
||||
|
||||
def get_form(self, **kwargs):
|
||||
form = super(EventRiskAssessmentCreate, self).get_form(**kwargs)
|
||||
epk = self.kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
form.instance.event = event
|
||||
return form
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventRiskAssessmentCreate, self).get_context_data(**kwargs)
|
||||
epk = self.kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
context['event'] = event
|
||||
context['page_title'] = 'Create Risk Assessment for Event {}'.format(event.display_id)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
class EventRiskAssessmentEdit(generic.UpdateView):
|
||||
model = models.RiskAssessment
|
||||
template_name = 'risk_assessment_form.html'
|
||||
form_class = forms.EventRiskAssessmentForm
|
||||
|
||||
def get_success_url(self):
|
||||
ra = self.get_object()
|
||||
ra.reviewed_by = None
|
||||
ra.reviewed_at = None
|
||||
ra.save()
|
||||
return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventRiskAssessmentEdit, self).get_context_data(**kwargs)
|
||||
rpk = self.kwargs.get('pk')
|
||||
ra = models.RiskAssessment.objects.get(pk=rpk)
|
||||
context['event'] = ra.event
|
||||
context['edit'] = True
|
||||
context['page_title'] = 'Edit Risk Assessment for Event {}'.format(ra.event.display_id)
|
||||
return context
|
||||
|
||||
|
||||
class EventRiskAssessmentDetail(generic.DetailView):
|
||||
model = models.RiskAssessment
|
||||
template_name = 'risk_assessment_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventRiskAssessmentDetail, self).get_context_data(**kwargs)
|
||||
context['page_title'] = "Risk Assessment for Event <a href='{}'>{} {}</a>".format(self.object.event.get_absolute_url(), self.object.event.display_id, self.object.event.name)
|
||||
return context
|
||||
|
||||
|
||||
class EventRiskAssessmentList(generic.ListView):
|
||||
paginate_by = 20
|
||||
model = models.RiskAssessment
|
||||
template_name = 'hs_object_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventRiskAssessmentList, self).get_context_data(**kwargs)
|
||||
context['title'] = 'Risk Assessment'
|
||||
context['view'] = 'ra_detail'
|
||||
context['edit'] = 'ra_edit'
|
||||
context['review'] = 'ra_review'
|
||||
context['perm'] = 'perms.RIGS.review_riskassessment'
|
||||
return context
|
||||
|
||||
|
||||
class EventRiskAssessmentReview(generic.View):
|
||||
def get(self, *args, **kwargs):
|
||||
rpk = kwargs.get('pk')
|
||||
ra = models.RiskAssessment.objects.get(pk=rpk)
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(self.request.user)
|
||||
ra.reviewed_by = self.request.user
|
||||
ra.reviewed_at = timezone.now()
|
||||
ra.save()
|
||||
return HttpResponseRedirect(reverse_lazy('ra_list'))
|
||||
|
||||
|
||||
class EventChecklistDetail(generic.DetailView):
|
||||
model = models.EventChecklist
|
||||
template_name = 'event_checklist_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventChecklistDetail, self).get_context_data(**kwargs)
|
||||
context['page_title'] = "Event Checklist for Event <a href='{}'>{} {}</a>".format(self.object.event.get_absolute_url(), self.object.event.display_id, self.object.event.name)
|
||||
return context
|
||||
|
||||
|
||||
class EventChecklistEdit(generic.UpdateView):
|
||||
model = models.EventChecklist
|
||||
template_name = 'event_checklist_form.html'
|
||||
form_class = forms.EventChecklistForm
|
||||
|
||||
def get_success_url(self):
|
||||
ec = self.get_object()
|
||||
ec.reviewed_by = None
|
||||
ec.reviewed_at = None
|
||||
ec.save()
|
||||
return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventChecklistEdit, self).get_context_data(**kwargs)
|
||||
pk = self.kwargs.get('pk')
|
||||
ec = models.EventChecklist.objects.get(pk=pk)
|
||||
context['event'] = ec.event
|
||||
context['edit'] = True
|
||||
context['page_title'] = 'Edit Event Checklist for Event {}'.format(ec.event.display_id)
|
||||
form = context['form']
|
||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
||||
for field, model in form.related_models.items():
|
||||
value = form[field].value()
|
||||
if value is not None and value != '':
|
||||
context[field] = model.objects.get(pk=value)
|
||||
return context
|
||||
|
||||
|
||||
class EventChecklistCreate(generic.CreateView):
|
||||
model = models.EventChecklist
|
||||
template_name = 'event_checklist_form.html'
|
||||
form_class = forms.EventChecklistForm
|
||||
|
||||
# From both business logic and programming POVs, RAs must exist before ECs!
|
||||
def get(self, *args, **kwargs):
|
||||
epk = kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
|
||||
# Check if RA exists
|
||||
ra = models.RiskAssessment.objects.filter(event=event).first()
|
||||
|
||||
if ra is None:
|
||||
messages.error(self.request, 'A Risk Assessment must exist prior to creating any Event Checklists for {}! Please create one now.'.format(event))
|
||||
return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
|
||||
|
||||
return super(EventChecklistCreate, self).get(self)
|
||||
|
||||
def get_form(self, **kwargs):
|
||||
form = super(EventChecklistCreate, self).get_form(**kwargs)
|
||||
epk = self.kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
form.instance.event = event
|
||||
return form
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventChecklistCreate, self).get_context_data(**kwargs)
|
||||
epk = self.kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
context['event'] = event
|
||||
context['page_title'] = 'Create Event Checklist for Event {}'.format(event.display_id)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
class EventChecklistList(generic.ListView):
|
||||
paginate_by = 20
|
||||
model = models.EventChecklist
|
||||
template_name = 'hs_object_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventChecklistList, self).get_context_data(**kwargs)
|
||||
context['title'] = 'Event Checklist'
|
||||
context['view'] = 'ec_detail'
|
||||
context['edit'] = 'ec_edit'
|
||||
context['review'] = 'ec_review'
|
||||
context['perm'] = 'perms.RIGS.review_eventchecklist'
|
||||
return context
|
||||
|
||||
|
||||
class EventChecklistReview(generic.View):
|
||||
def get(self, *args, **kwargs):
|
||||
rpk = kwargs.get('pk')
|
||||
ec = models.EventChecklist.objects.get(pk=rpk)
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(self.request.user)
|
||||
ec.reviewed_by = self.request.user
|
||||
ec.reviewed_at = timezone.now()
|
||||
ec.save()
|
||||
return HttpResponseRedirect(reverse_lazy('ec_list'))
|
||||
|
||||
|
||||
class HSList(generic.ListView):
|
||||
paginate_by = 20
|
||||
model = models.Event
|
||||
template_name = 'hs_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(HSList, self).get_context_data(**kwargs)
|
||||
context['page_title'] = 'H&S Overview'
|
||||
return context
|
||||
94
RIGS/ical.py
94
RIGS/ical.py
@@ -1,17 +1,18 @@
|
||||
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.utils import timezone
|
||||
from django.conf import settings
|
||||
import datetime
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django_ical.views import ICalFeed
|
||||
|
||||
from RIGS import models
|
||||
|
||||
import datetime, 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 +28,41 @@ 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):
|
||||
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 '')
|
||||
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)
|
||||
@@ -70,7 +73,8 @@ class CalendarICS(ICalFeed):
|
||||
|
||||
filter = filter & typeFilters & statusFilters
|
||||
|
||||
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
||||
'venue', 'mic')
|
||||
|
||||
def item_title(self, item):
|
||||
title = ''
|
||||
@@ -87,9 +91,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 +101,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 isinstance(item.latest_time, datetime.date): # Ical end_datetime is non-inclusive, so add a day
|
||||
return item.latest_time + datetime.timedelta(days=1)
|
||||
|
||||
return item.latest_time
|
||||
|
||||
def item_location(self,item):
|
||||
def item_location(self, item):
|
||||
return item.venue
|
||||
|
||||
def item_description(self, item):
|
||||
@@ -111,34 +115,38 @@ 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'
|
||||
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'
|
||||
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 +157,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
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
__author__ = 'ghost'
|
||||
|
||||
import unittest
|
||||
from importer import fix_email
|
||||
|
||||
|
||||
class EmailFixerTest(unittest.TestCase):
|
||||
def test_correct(self):
|
||||
e = fix_email("tom@ghost.uk.net")
|
||||
self.assertEqual(e, "tom@ghost.uk.net")
|
||||
|
||||
def test_partial(self):
|
||||
e = fix_email("psytp")
|
||||
self.assertEqual(e, "psytp@nottingham.ac.uk")
|
||||
|
||||
def test_none(self):
|
||||
old = None
|
||||
new = fix_email(old)
|
||||
self.assertEqual(old, new)
|
||||
|
||||
def test_empty(self):
|
||||
old = ""
|
||||
new = fix_email(old)
|
||||
self.assertEqual(old, new)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
43
RIGS/management/commands/deleteSampleData.py
Normal file
43
RIGS/management/commands/deleteSampleData.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from assets import models
|
||||
from RIGS import models as rigsmodels
|
||||
from training import models as tmodels
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Deletes testing sample data'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
from django.conf import settings
|
||||
|
||||
if not settings.DEBUG:
|
||||
raise CommandError('You cannot run this command in production')
|
||||
|
||||
self.delete_objects(models.AssetCategory)
|
||||
self.delete_objects(models.AssetStatus)
|
||||
self.delete_objects(models.Supplier)
|
||||
self.delete_objects(models.Connector)
|
||||
self.delete_objects(models.Asset)
|
||||
self.delete_objects(rigsmodels.VatRate)
|
||||
self.delete_objects(rigsmodels.Profile)
|
||||
self.delete_objects(rigsmodels.Person)
|
||||
self.delete_objects(rigsmodels.Organisation)
|
||||
self.delete_objects(rigsmodels.Venue)
|
||||
self.delete_objects(Group)
|
||||
self.delete_objects(rigsmodels.Event)
|
||||
self.delete_objects(rigsmodels.EventItem)
|
||||
self.delete_objects(rigsmodels.Invoice)
|
||||
self.delete_objects(rigsmodels.Payment)
|
||||
self.delete_objects(rigsmodels.RiskAssessment)
|
||||
self.delete_objects(rigsmodels.EventChecklist)
|
||||
self.delete_objects(tmodels.TrainingCategory)
|
||||
self.delete_objects(tmodels.TrainingItem)
|
||||
self.delete_objects(tmodels.TrainingLevel)
|
||||
self.delete_objects(tmodels.TrainingItemQualification)
|
||||
self.delete_objects(tmodels.TrainingLevelRequirement)
|
||||
|
||||
def delete_objects(self, model):
|
||||
for obj in model.objects.all():
|
||||
obj.delete()
|
||||
@@ -1,248 +1,15 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.db import transaction
|
||||
import reversion
|
||||
|
||||
import datetime
|
||||
import random
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Adds sample data to use for testing'
|
||||
can_import_settings = True
|
||||
|
||||
people = []
|
||||
organisations = []
|
||||
venues = []
|
||||
profiles = []
|
||||
|
||||
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
|
||||
|
||||
with transaction.atomic():
|
||||
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"]
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
|
||||
newPerson = models.Person.objects.create(name=name)
|
||||
if i % 3 == 0:
|
||||
newPerson.email = "address@person.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
newPerson.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
newPerson.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
newPerson.phone = "01234 567894"
|
||||
|
||||
newPerson.save()
|
||||
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"]
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
newOrganisation = models.Organisation.objects.create(name=name)
|
||||
if i % 2 == 0:
|
||||
newOrganisation.has_su_account = True
|
||||
|
||||
if i % 3 == 0:
|
||||
newOrganisation.email = "address@organisation.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
newOrganisation.phone = "01234 567894"
|
||||
|
||||
newOrganisation.save()
|
||||
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"]
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
newVenue = models.Venue.objects.create(name=name)
|
||||
if i % 2 == 0:
|
||||
newVenue.three_phase_available = True
|
||||
|
||||
if i % 3 == 0:
|
||||
newVenue.email = "address@venue.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
newVenue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
newVenue.phone = "01234 567894"
|
||||
|
||||
newVenue.save()
|
||||
self.venues.append(newVenue)
|
||||
|
||||
def setupGroups(self):
|
||||
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"]
|
||||
|
||||
for permId in keyholderPerms:
|
||||
self.keyholder_group.permissions.add(Permission.objects.get(codename=permId))
|
||||
|
||||
for permId in financePerms:
|
||||
self.finance_group.permissions.add(Permission.objects.get(codename=permId))
|
||||
|
||||
def setupGenericProfiles(self):
|
||||
names = ["Clara Oswin Oswald","Rory Williams","Amy Pond","River Song","Martha Jones","Donna Noble","Jack Harkness","Mickey Smith","Rose Tyler"]
|
||||
for i, name in enumerate(names):
|
||||
newProfile = models.Profile.objects.create(username=name.replace(" ",""), first_name=name.split(" ")[0], last_name=name.split(" ")[-1],
|
||||
email=name.replace(" ","")+"@example.com",
|
||||
initials="".join([ j[0].upper() for j in name.split() ]))
|
||||
if i % 2 == 0:
|
||||
newProfile.phone = "01234 567894"
|
||||
|
||||
newProfile.save()
|
||||
self.profiles.append(newProfile)
|
||||
|
||||
def setupUsefulProfiles(self):
|
||||
superUser = models.Profile.objects.create(username="superuser", first_name="Super", last_name="User", initials="SU",
|
||||
email="superuser@example.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
superUser.set_password('superuser')
|
||||
superUser.save()
|
||||
|
||||
financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User", initials="FU",
|
||||
email="financeuser@example.com", is_active=True)
|
||||
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.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.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!"]
|
||||
|
||||
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}]
|
||||
|
||||
dayDelta = -120 # start adding events from 4 months ago
|
||||
|
||||
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)]
|
||||
|
||||
startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
|
||||
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
|
||||
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))
|
||||
|
||||
|
||||
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
|
||||
newEvent.organisation = random.choice(self.organisations)
|
||||
|
||||
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
|
||||
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.dry_hire = (random.randint(0,7)==0) # 1 in 7 are dry hire
|
||||
|
||||
if random.randint(0,1) > 0: # 1 in 2 have description
|
||||
newEvent.description = random.choice(descriptions)
|
||||
|
||||
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)]
|
||||
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)]
|
||||
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
|
||||
newInvoice = models.Invoice.objects.create(event=newEvent)
|
||||
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
|
||||
models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance, date=datetime.date.today())
|
||||
call_command('generateSampleUserData')
|
||||
call_command('generateSampleRIGSData')
|
||||
call_command('generateSampleAssetsData')
|
||||
call_command('generateSampleTrainingData')
|
||||
|
||||
292
RIGS/management/commands/generateSampleRIGSData.py
Normal file
292
RIGS/management/commands/generateSampleRIGSData.py
Normal file
@@ -0,0 +1,292 @@
|
||||
import datetime
|
||||
import random
|
||||
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Adds sample data to use for testing'
|
||||
can_import_settings = True
|
||||
|
||||
people = []
|
||||
organisations = []
|
||||
venues = []
|
||||
events = []
|
||||
profiles = models.Profile.objects.all()
|
||||
|
||||
def handle(self, *args, **options):
|
||||
print("Generating rigboard data")
|
||||
from django.conf import settings
|
||||
|
||||
if not (settings.DEBUG or settings.STAGING):
|
||||
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
|
||||
|
||||
with transaction.atomic():
|
||||
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
self.setup_people()
|
||||
self.setup_organisations()
|
||||
self.setup_venues()
|
||||
self.setup_events()
|
||||
print("Done generating rigboard data")
|
||||
|
||||
def setup_people(self):
|
||||
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe",
|
||||
"Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore",
|
||||
"Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan",
|
||||
"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(models.Profile.objects.all()))
|
||||
person = models.Person.objects.create(name=name)
|
||||
|
||||
if i % 3 == 0:
|
||||
person.email = "address@person.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
person.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
person.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
person.phone = "01234 567894"
|
||||
|
||||
person.save()
|
||||
self.people.append(person)
|
||||
|
||||
def setup_organisations(self):
|
||||
names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars",
|
||||
"ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc",
|
||||
"Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp",
|
||||
"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(models.Profile.objects.all()))
|
||||
new_organisation = models.Organisation.objects.create(name=name)
|
||||
|
||||
if i % 2 == 0:
|
||||
new_organisation.has_su_account = True
|
||||
|
||||
if i % 3 == 0:
|
||||
new_organisation.email = "address@organisation.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
new_organisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
new_organisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
new_organisation.phone = "01234 567894"
|
||||
|
||||
new_organisation.save()
|
||||
self.organisations.append(new_organisation)
|
||||
|
||||
def setup_venues(self):
|
||||
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch",
|
||||
"The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands",
|
||||
"The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins",
|
||||
"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))
|
||||
new_venue = models.Venue.objects.create(name=name)
|
||||
|
||||
if i % 2 == 0:
|
||||
new_venue.three_phase_available = True
|
||||
|
||||
if i % 3 == 0:
|
||||
new_venue.email = "address@venue.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
new_venue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
new_venue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
new_venue.phone = "01234 567894"
|
||||
|
||||
new_venue.save()
|
||||
self.venues.append(new_venue)
|
||||
|
||||
def setup_events(self):
|
||||
names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball",
|
||||
"Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
|
||||
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show",
|
||||
"Awards Ceremony", "Debating Event", "Club Night", "DJ Evening", "Building Projection",
|
||||
"Choir Concert"]
|
||||
descriptions = ["A brief description 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!"]
|
||||
|
||||
item_options = [
|
||||
{'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}]
|
||||
|
||||
day_delta = -120 # start adding events from 4 months ago
|
||||
|
||||
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)]
|
||||
|
||||
start_date = datetime.date.today() + datetime.timedelta(days=day_delta)
|
||||
day_delta = day_delta + random.randint(0, 3)
|
||||
|
||||
new_event = models.Event.objects.create(name=name, start_date=start_date)
|
||||
|
||||
if random.randint(0, 2) > 1: # 1 in 3 have a start time
|
||||
new_event.start_time = datetime.time(random.randint(15, 20))
|
||||
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
|
||||
new_event.end_time = datetime.time(random.randint(21, 23))
|
||||
elif random.randint(0, 1) > 0: # half of the others finish early the next day
|
||||
new_event.end_date = new_event.start_date + datetime.timedelta(days=1)
|
||||
new_event.end_time = datetime.time(random.randint(0, 5))
|
||||
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
|
||||
new_event.end_date = new_event.start_date + datetime.timedelta(days=random.randint(1, 4))
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have MIC
|
||||
new_event.mic = random.choice(self.profiles)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have organisation
|
||||
new_event.organisation = random.choice(self.organisations)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have person
|
||||
new_event.person = random.choice(self.people)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have venue
|
||||
new_event.venue = random.choice(self.venues)
|
||||
|
||||
# Could have any status, equally weighted
|
||||
new_event.status = random.choice(
|
||||
[models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
|
||||
|
||||
new_event.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
|
||||
|
||||
if random.randint(0, 1) > 0: # 1 in 2 have description
|
||||
new_event.description = random.choice(descriptions)
|
||||
|
||||
if random.randint(0, 1) > 0: # 1 in 2 have notes
|
||||
new_event.notes = random.choice(notes)
|
||||
|
||||
new_event.save()
|
||||
|
||||
# Now add some items
|
||||
for j in range(random.randint(1, 5)):
|
||||
item_data = item_options[random.randint(0, len(item_options) - 1)]
|
||||
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
|
||||
new_item.save()
|
||||
|
||||
while new_event.sum_total < 0:
|
||||
item_data = item_options[random.randint(0, len(item_options) - 1)]
|
||||
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
|
||||
new_item.save()
|
||||
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
if new_event.start_date < datetime.date.today(): # think about adding an invoice
|
||||
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
|
||||
new_invoice = models.Invoice.objects.create(event=new_event)
|
||||
if new_event.status is models.Event.CANCELLED: # void cancelled events
|
||||
new_invoice.void = True
|
||||
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
|
||||
models.Payment.objects.create(invoice=new_invoice, amount=new_invoice.balance,
|
||||
date=datetime.date.today())
|
||||
if i == 1 or random.randint(0, 5) > 0: # Event 1 and 1 in 5 have a RA
|
||||
models.RiskAssessment.objects.create(event=new_event, supervisor_consulted=bool(random.getrandbits(1)),
|
||||
nonstandard_equipment=bool(random.getrandbits(1)),
|
||||
nonstandard_use=bool(random.getrandbits(1)),
|
||||
contractors=bool(random.getrandbits(1)),
|
||||
other_companies=bool(random.getrandbits(1)),
|
||||
crew_fatigue=bool(random.getrandbits(1)),
|
||||
big_power=bool(random.getrandbits(1)),
|
||||
generators=bool(random.getrandbits(1)),
|
||||
other_companies_power=bool(random.getrandbits(1)),
|
||||
nonstandard_equipment_power=bool(random.getrandbits(1)),
|
||||
multiple_electrical_environments=bool(random.getrandbits(1)),
|
||||
noise_monitoring=bool(random.getrandbits(1)),
|
||||
known_venue=bool(random.getrandbits(1)),
|
||||
safe_loading=bool(random.getrandbits(1)),
|
||||
safe_storage=bool(random.getrandbits(1)),
|
||||
area_outside_of_control=bool(random.getrandbits(1)),
|
||||
barrier_required=bool(random.getrandbits(1)),
|
||||
nonstandard_emergency_procedure=bool(random.getrandbits(1)),
|
||||
special_structures=bool(random.getrandbits(1)),
|
||||
suspended_structures=bool(random.getrandbits(1)),
|
||||
outside=bool(random.getrandbits(1)))
|
||||
if i == 0 or random.randint(0, 1) > 0: # Event 1 and 1 in 10 have a Checklist
|
||||
models.EventChecklist.objects.create(event=new_event, power_mic=random.choice(self.profiles),
|
||||
safe_parking=bool(random.getrandbits(1)),
|
||||
safe_packing=bool(random.getrandbits(1)),
|
||||
exits=bool(random.getrandbits(1)),
|
||||
trip_hazard=bool(random.getrandbits(1)),
|
||||
warning_signs=bool(random.getrandbits(1)),
|
||||
ear_plugs=bool(random.getrandbits(1)),
|
||||
hs_location="Locked away safely",
|
||||
extinguishers_location="Somewhere, I forgot",
|
||||
earthing=bool(random.getrandbits(1)),
|
||||
pat=bool(random.getrandbits(1)),
|
||||
date=timezone.now(), venue=random.choice(self.venues))
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.core.validators
|
||||
|
||||
@@ -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={
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
import RIGS.models
|
||||
import versioning
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -25,6 +26,6 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
import RIGS.models
|
||||
import versioning
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -21,6 +22,6 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
import RIGS.models
|
||||
import versioning
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -33,15 +34,15 @@ 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={
|
||||
},
|
||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventItem',
|
||||
@@ -52,7 +53,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={
|
||||
},
|
||||
@@ -70,12 +71,12 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.core.validators
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.db.models.deletion
|
||||
|
||||
26
RIGS/migrations/0025_auto_20160331_1302.py
Normal file
26
RIGS/migrations/0025_auto_20160331_1302.py
Normal 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(),
|
||||
),
|
||||
]
|
||||
27
RIGS/migrations/0025_eventauthorisation.py
Normal file
27
RIGS/migrations/0025_eventauthorisation.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
21
RIGS/migrations/0026_auto_20170510_1846.py
Normal file
21
RIGS/migrations/0026_auto_20170510_1846.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
RIGS/migrations/0026_remove_eventauthorisation_created_at.py
Normal file
18
RIGS/migrations/0026_remove_eventauthorisation_created_at.py
Normal 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',
|
||||
),
|
||||
]
|
||||
19
RIGS/migrations/0027_eventauthorisation_event_singular.py
Normal file
19
RIGS/migrations/0027_eventauthorisation_event_singular.py
Normal 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),
|
||||
),
|
||||
]
|
||||
21
RIGS/migrations/0029_eventauthorisation_sent_by.py
Normal file
21
RIGS/migrations/0029_eventauthorisation_sent_by.py
Normal 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,
|
||||
),
|
||||
]
|
||||
30
RIGS/migrations/0030_auth_request_sending.py
Normal file
30
RIGS/migrations/0030_auth_request_sending.py
Normal 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),
|
||||
),
|
||||
]
|
||||
16
RIGS/migrations/0031_merge_20170512_2102.py
Normal file
16
RIGS/migrations/0031_merge_20170512_2102.py
Normal 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 = [
|
||||
]
|
||||
69
RIGS/migrations/0032_auto_20170904_2355.py
Normal file
69
RIGS/migrations/0032_auto_20170904_2355.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
RIGS/migrations/0033_auto_20180325_0016.py
Normal file
18
RIGS/migrations/0033_auto_20180325_0016.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
RIGS/migrations/0034_event_risk_assessment_edit_url.py
Normal file
18
RIGS/migrations/0034_event_risk_assessment_edit_url.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.0.5 on 2019-07-28 21:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0033_auto_20180325_0016'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='risk_assessment_edit_url',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
18
RIGS/migrations/0035_auto_20191124_1319.py
Normal file
18
RIGS/migrations/0035_auto_20191124_1319.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.0.13 on 2019-11-24 13:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0034_event_risk_assessment_edit_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='risk_assessment_edit_url',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='risk assessment'),
|
||||
),
|
||||
]
|
||||
23
RIGS/migrations/0036_profile_is_approved.py
Normal file
23
RIGS/migrations/0036_profile_is_approved.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.0.13 on 2020-01-10 14:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0035_auto_20191124_1319'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='is_approved',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='last_emailed',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
19
RIGS/migrations/0037_approve_legacy.py
Normal file
19
RIGS/migrations/0037_approve_legacy.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.0.13 on 2020-01-11 18:29
|
||||
# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
|
||||
from django.db import migrations
|
||||
|
||||
def approve_legacy(apps, schema_editor):
|
||||
Profile = apps.get_model('RIGS', 'Profile')
|
||||
for person in Profile.objects.all():
|
||||
person.is_approved = True
|
||||
person.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0036_profile_is_approved'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(approve_legacy, migrations.RunPython.noop)
|
||||
]
|
||||
37
RIGS/migrations/0038_auto_20200306_2000.py
Normal file
37
RIGS/migrations/0038_auto_20200306_2000.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 2.0.13 on 2020-03-06 20:00
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0037_approve_legacy'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='event',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='invoice',
|
||||
options={'ordering': ['-invoice_date']},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='organisation',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='person',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='profile',
|
||||
options={'verbose_name': 'user', 'verbose_name_plural': 'users'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='venue',
|
||||
options={},
|
||||
),
|
||||
]
|
||||
191
RIGS/migrations/0039_auto_20210123_1910.py
Normal file
191
RIGS/migrations/0039_auto_20210123_1910.py
Normal file
@@ -0,0 +1,191 @@
|
||||
# Generated by Django 3.1.2 on 2021-01-23 19:10
|
||||
|
||||
import RIGS.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import versioning
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0038_auto_20200306_2000'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EventChecklist',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField()),
|
||||
('safe_parking', models.BooleanField(blank=True, help_text='Vehicles parked safely?<br><small>(does not obstruct venue access)</small>', null=True)),
|
||||
('safe_packing', models.BooleanField(blank=True, help_text='Equipment packed away safely?<br><small>(including flightcases)</small>', null=True)),
|
||||
('exits', models.BooleanField(blank=True, help_text='Emergency exits clear?', null=True)),
|
||||
('trip_hazard', models.BooleanField(blank=True, help_text='Appropriate barriers around kit and cabling secured?', null=True)),
|
||||
('warning_signs', models.BooleanField(blank=True, help_text='Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>')),
|
||||
('ear_plugs', models.BooleanField(blank=True, help_text='Ear plugs issued to crew where needed?', null=True)),
|
||||
('hs_location', models.CharField(blank=True, help_text='Location of Safety Bag/Box', max_length=255, null=True)),
|
||||
('extinguishers_location', models.CharField(blank=True, help_text='Location of fire extinguishers', max_length=255, null=True)),
|
||||
('rcds', models.BooleanField(blank=True, help_text='RCDs installed where needed and tested?', null=True)),
|
||||
('supply_test', models.BooleanField(blank=True, help_text='Electrical supplies tested?<br><small>(using socket tester)</small>', null=True)),
|
||||
('earthing', models.BooleanField(blank=True, help_text='Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>', null=True)),
|
||||
('pat', models.BooleanField(blank=True, help_text='All equipment in PAT period?', null=True)),
|
||||
('source_rcd', models.BooleanField(blank=True, help_text='Source RCD protected?<br><small>(if cable is more than 3m long) </small>', null=True)),
|
||||
('labelling', models.BooleanField(blank=True, help_text='Appropriate and clear labelling on distribution and cabling?', null=True)),
|
||||
('fd_voltage_l1', models.IntegerField(blank=True, help_text='L1 - N', null=True, verbose_name='First Distro Voltage L1-N')),
|
||||
('fd_voltage_l2', models.IntegerField(blank=True, help_text='L2 - N', null=True, verbose_name='First Distro Voltage L2-N')),
|
||||
('fd_voltage_l3', models.IntegerField(blank=True, help_text='L3 - N', null=True, verbose_name='First Distro Voltage L3-N')),
|
||||
('fd_phase_rotation', models.BooleanField(blank=True, help_text='Phase Rotation<br><small>(if required)</small>', null=True, verbose_name='Phase Rotation')),
|
||||
('fd_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True, verbose_name='Earth Fault Loop Impedance')),
|
||||
('fd_pssc', models.IntegerField(blank=True, help_text='Prospective Short Circuit Current', null=True, verbose_name='PSCC')),
|
||||
('w1_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
|
||||
('w1_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
||||
('w1_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
||||
('w1_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
|
||||
('w2_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
|
||||
('w2_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
||||
('w2_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
||||
('w2_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
|
||||
('w3_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
|
||||
('w3_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
||||
('w3_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
||||
('w3_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
|
||||
('all_rcds_tested', models.BooleanField(blank=True, help_text='All circuit RCDs tested?<br><small>(using test button)</small>', null=True)),
|
||||
('public_sockets_tested', models.BooleanField(blank=True, help_text='Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>', null=True)),
|
||||
('reviewed_at', models.DateTimeField(null=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['event'],
|
||||
'permissions': [('review_eventchecklist', 'Can review Event Checklists')],
|
||||
},
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventChecklistCrew',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(max_length=255)),
|
||||
('start', models.DateTimeField()),
|
||||
('end', models.DateTimeField()),
|
||||
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.eventchecklist')),
|
||||
],
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventChecklistVehicle',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('vehicle', models.CharField(max_length=255)),
|
||||
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to='RIGS.eventchecklist')),
|
||||
],
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RiskAssessment',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nonstandard_equipment', models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by <a href='https://nottinghamtec.sharepoint.com/:f:/g/HealthAndSafety/Eo4xED_DrqFFsfYIjKzMZIIB6Gm_ZfR-a8l84RnzxtBjrA?e=Bf0Haw'>TEC's standard risk assessments and method statements?</a>")),
|
||||
('nonstandard_use', models.BooleanField(help_text='Are TEC using their equipment in a way that is abnormal?<br><small>i.e. Not covered by TECs standard health and safety documentation</small>')),
|
||||
('contractors', models.BooleanField(help_text='Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>')),
|
||||
('other_companies', models.BooleanField(help_text='Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>')),
|
||||
('crew_fatigue', models.BooleanField(help_text='Is crew fatigue likely to be a risk at any point during this event?')),
|
||||
('general_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
|
||||
('big_power', models.BooleanField(help_text='Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?')),
|
||||
('outside', models.BooleanField(help_text='Is the event outdoors?')),
|
||||
('generators', models.BooleanField(help_text='Will generators be used?')),
|
||||
('other_companies_power', models.BooleanField(help_text='Will TEC be supplying power to any other companies?')),
|
||||
('nonstandard_equipment_power', models.BooleanField(help_text='Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?')),
|
||||
('multiple_electrical_environments', models.BooleanField(help_text='Will the electrical installation occupy more than one electrical environment?')),
|
||||
('power_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
|
||||
('power_plan', models.URLField(blank=True, help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", null=True, validators=[RIGS.models.validate_url])),
|
||||
('noise_monitoring', models.BooleanField(help_text='Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?')),
|
||||
('sound_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
|
||||
('known_venue', models.BooleanField(help_text='Is this venue new to you (the MIC) or new to TEC?')),
|
||||
('safe_loading', models.BooleanField(help_text='Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)')),
|
||||
('safe_storage', models.BooleanField(help_text='Are there any problems with safe and secure equipment storage?')),
|
||||
('area_outside_of_control', models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")),
|
||||
('barrier_required', models.BooleanField(help_text='Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?')),
|
||||
('nonstandard_emergency_procedure', models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")),
|
||||
('special_structures', models.BooleanField(help_text='Does the event require use of winch stands, motors, MPT Towers, or staging?')),
|
||||
('suspended_structures', models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")),
|
||||
('persons_responsible_structures', models.TextField(blank=True, help_text='Who are the persons on site responsible for their use?', null=True)),
|
||||
('rigging_plan', models.URLField(blank=True, help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", null=True, validators=[RIGS.models.validate_url])),
|
||||
('reviewed_at', models.DateTimeField(null=True)),
|
||||
('supervisor_consulted', models.BooleanField(null=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['event'],
|
||||
'permissions': [('review_riskassessment', 'Can review Risk Assessments')],
|
||||
},
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventcrew',
|
||||
name='event',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventcrew',
|
||||
name='user',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='RIGSVersion',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='risk_assessment_edit_url',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='first_name',
|
||||
field=models.CharField(blank=True, max_length=150, verbose_name='first name'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='EventCrew',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='riskassessment',
|
||||
name='event',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='RIGS.event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='riskassessment',
|
||||
name='power_mic',
|
||||
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='power_mic', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='riskassessment',
|
||||
name='reviewed_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventchecklistvehicle',
|
||||
name='driver',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventchecklistcrew',
|
||||
name='crewmember',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crewed', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventchecklist',
|
||||
name='event',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to='RIGS.event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventchecklist',
|
||||
name='power_mic',
|
||||
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC?', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventchecklist',
|
||||
name='reviewed_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventchecklist',
|
||||
name='venue',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.venue'),
|
||||
),
|
||||
]
|
||||
67
RIGS/migrations/0040_auto_20210302_1148.py
Normal file
67
RIGS/migrations/0040_auto_20210302_1148.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-02 11:48
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def postgres_migration_prep(apps, schema_editor):
|
||||
model = apps.get_model("RIGS", "Event")
|
||||
for field in ["auth_request_to", "collector", "description", "notes", "purchase_order"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "EventAuthorisation")
|
||||
for field in ["account_code", "uni_id"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "EventChecklist")
|
||||
for field in ["extinguishers_location", "hs_location", "w1_description", "w2_description", "w3_description"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "EventItem")
|
||||
for field in ["description"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Organisation")
|
||||
for field in ["address", "email", "notes", "phone"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Payment")
|
||||
for field in ["method"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Person")
|
||||
for field in ["address", "email", "notes", "phone"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Profile")
|
||||
for field in ["phone"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "RiskAssessment")
|
||||
for field in ["general_notes", "persons_responsible_structures", "power_notes", "rigging_plan", "sound_notes"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Venue")
|
||||
for field in ["address", "email", "notes", "phone"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0039_auto_20210123_1910'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(postgres_migration_prep, migrations.RunPython.noop)
|
||||
]
|
||||
201
RIGS/migrations/0041_auto_20210302_1204.py
Normal file
201
RIGS/migrations/0041_auto_20210302_1204.py
Normal file
@@ -0,0 +1,201 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-02 12:04
|
||||
|
||||
import RIGS.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0040_auto_20210302_1148'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='meet_info',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='payment_method',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='payment_received',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='dark_theme',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='auth_request_to',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='collector',
|
||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='collected by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='purchase_order',
|
||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='PO'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventauthorisation',
|
||||
name='account_code',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventauthorisation',
|
||||
name='uni_id',
|
||||
field=models.CharField(blank=True, default='', max_length=10, verbose_name='University ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='extinguishers_location',
|
||||
field=models.CharField(blank=True, default='', help_text='Location of fire extinguishers', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='hs_location',
|
||||
field=models.CharField(blank=True, default='', help_text='Location of Safety Bag/Box', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w1_description',
|
||||
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w2_description',
|
||||
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w3_description',
|
||||
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventitem',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=15),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='method',
|
||||
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('SU', 'SU Core'), ('T', 'TEC Adjustment')], default='', max_length=2),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=15),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='api_key',
|
||||
field=models.CharField(blank=True, default='', editable=False, max_length=40),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='general_notes',
|
||||
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='persons_responsible_structures',
|
||||
field=models.TextField(blank=True, default='', help_text='Who are the persons on site responsible for their use?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='power_notes',
|
||||
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='power_plan',
|
||||
field=models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[RIGS.models.validate_url]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='rigging_plan',
|
||||
field=models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[RIGS.models.validate_url]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='sound_notes',
|
||||
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=15),
|
||||
),
|
||||
]
|
||||
34
RIGS/migrations/0042_auto_20211007_2338.py
Normal file
34
RIGS/migrations/0042_auto_20211007_2338.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.1.13 on 2021-10-07 22:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0041_auto_20210302_1204'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='fd_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w1_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w2_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w3_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
]
|
||||
|
||||
18
RIGS/migrations/0043_auto_20211027_1519.py
Normal file
18
RIGS/migrations/0043_auto_20211027_1519.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.13 on 2021-10-27 14:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0042_auto_20211007_2338'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='initials',
|
||||
field=models.CharField(max_length=5, null=True),
|
||||
),
|
||||
]
|
||||
18
RIGS/migrations/0044_profile_is_supervisor.py
Normal file
18
RIGS/migrations/0044_profile_is_supervisor.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.11 on 2022-01-09 14:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0043_auto_20211027_1519'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='is_supervisor',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
600
RIGS/models.py
600
RIGS/models.py
@@ -1,40 +1,49 @@
|
||||
import datetime
|
||||
import hashlib
|
||||
import pytz
|
||||
import random
|
||||
import string
|
||||
from collections import Counter
|
||||
from decimal import Decimal
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import reversion
|
||||
import pytz
|
||||
from django import forms
|
||||
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.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from reversion import revisions as reversion
|
||||
from reversion.models import Version
|
||||
|
||||
|
||||
# Create your models here.
|
||||
@python_2_unicode_compatible
|
||||
class Profile(AbstractUser):
|
||||
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
|
||||
phone = models.CharField(max_length=13, null=True, blank=True)
|
||||
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
|
||||
initials = models.CharField(max_length=5, null=True, blank=False)
|
||||
phone = models.CharField(max_length=13, blank=True, default='')
|
||||
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
||||
is_approved = models.BooleanField(default=False)
|
||||
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
||||
last_emailed = models.DateTimeField(blank=True, null=True)
|
||||
dark_theme = models.BooleanField(default=False)
|
||||
is_supervisor = models.BooleanField(default=False)
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
@classmethod
|
||||
def make_api_key(cls):
|
||||
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
|
||||
@@ -46,56 +55,61 @@ class Profile(AbstractUser):
|
||||
|
||||
@property
|
||||
def latest_events(self):
|
||||
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
|
||||
|
||||
@classmethod
|
||||
def admins(cls):
|
||||
return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
|
||||
|
||||
@classmethod
|
||||
def users_awaiting_approval_count(cls):
|
||||
return Profile.objects.filter(models.Q(is_approved=False)).count()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_profile', 'Can view Profile'),
|
||||
)
|
||||
|
||||
class RevisionMixin:
|
||||
@property
|
||||
def is_first_version(self):
|
||||
versions = Version.objects.get_for_object(self)
|
||||
return len(versions) == 1
|
||||
|
||||
@property
|
||||
def current_version(self):
|
||||
version = Version.objects.get_for_object(self).select_related('revision').first()
|
||||
return version
|
||||
|
||||
class RevisionMixin(object):
|
||||
@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 f"V{version.pk} | R{version.revision.pk}"
|
||||
|
||||
|
||||
@reversion.register
|
||||
@python_2_unicode_compatible
|
||||
class Person(models.Model, RevisionMixin):
|
||||
name = models.CharField(max_length=50)
|
||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||
email = models.EmailField(blank=True, null=True)
|
||||
phone = models.CharField(max_length=15, blank=True, default='')
|
||||
email = models.EmailField(blank=True, default='')
|
||||
|
||||
address = models.TextField(blank=True, null=True)
|
||||
address = models.TextField(blank=True, default='')
|
||||
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
notes = models.TextField(blank=True, default='')
|
||||
|
||||
def __str__(self):
|
||||
string = self.name
|
||||
@@ -121,24 +135,17 @@ class Person(models.Model, RevisionMixin):
|
||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('person_detail', kwargs={'pk': self.pk})
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_person', 'Can view Persons'),
|
||||
)
|
||||
return reverse('person_detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
@reversion.register
|
||||
@python_2_unicode_compatible
|
||||
class Organisation(models.Model, RevisionMixin):
|
||||
name = models.CharField(max_length=50)
|
||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||
email = models.EmailField(blank=True, null=True)
|
||||
phone = models.CharField(max_length=15, blank=True, default='')
|
||||
email = models.EmailField(blank=True, default='')
|
||||
|
||||
address = models.TextField(blank=True, null=True)
|
||||
address = models.TextField(blank=True, default='')
|
||||
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
notes = models.TextField(blank=True, default='')
|
||||
union_account = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
@@ -165,20 +172,14 @@ class Organisation(models.Model, RevisionMixin):
|
||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_organisation', 'Can view Organisations'),
|
||||
)
|
||||
return reverse('organisation_detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
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()
|
||||
try:
|
||||
return self.filter(start_at__lte=date).latest()
|
||||
except VatRate.DoesNotExist:
|
||||
@@ -188,14 +189,15 @@ 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)
|
||||
|
||||
objects = VatManager()
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
@property
|
||||
def as_percent(self):
|
||||
return self.rate * 100
|
||||
@@ -205,19 +207,17 @@ class VatRate(models.Model, RevisionMixin):
|
||||
get_latest_by = 'start_at'
|
||||
|
||||
def __str__(self):
|
||||
return self.comment + " " + str(self.start_at) + " @ " + str(self.as_percent) + "%"
|
||||
return f"{self.comment} {self.start_at} @ {self.as_percent}%"
|
||||
|
||||
|
||||
@reversion.register
|
||||
@python_2_unicode_compatible
|
||||
class Venue(models.Model, RevisionMixin):
|
||||
name = models.CharField(max_length=255)
|
||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
||||
email = models.EmailField(blank=True, null=True)
|
||||
phone = models.CharField(max_length=15, blank=True, default='')
|
||||
email = models.EmailField(blank=True, default='')
|
||||
three_phase_available = models.BooleanField(default=False)
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
notes = models.TextField(blank=True, default='')
|
||||
|
||||
address = models.TextField(blank=True, null=True)
|
||||
address = models.TextField(blank=True, default='')
|
||||
|
||||
def __str__(self):
|
||||
string = self.name
|
||||
@@ -230,29 +230,23 @@ class Venue(models.Model, RevisionMixin):
|
||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_venue', 'Can view Venues'),
|
||||
)
|
||||
return reverse('venue_detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
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(
|
||||
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Starts after with no end
|
||||
(models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(
|
||||
(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=datetime.date.today()) & ~models.Q(
|
||||
(models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(
|
||||
status=Event.CANCELLED)) | # Active dry hire
|
||||
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
|
||||
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
|
||||
models.Q(status=Event.CANCELLED, start_date__gte=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(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started
|
||||
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
return events
|
||||
|
||||
def events_in_bounds(self, start, end):
|
||||
@@ -275,21 +269,31 @@ 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(), 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(), 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(
|
||||
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
|
||||
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q(
|
||||
status=Event.CANCELLED)) # Active dry hire
|
||||
).count()
|
||||
return event_count
|
||||
|
||||
def waiting_invoices(self):
|
||||
events = self.filter(
|
||||
(
|
||||
models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
||||
models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before
|
||||
) & models.Q(invoice__isnull=True) & # Has not already been invoiced
|
||||
models.Q(is_rig=True) # Is a rig (not non-rig)
|
||||
).order_by('start_date') \
|
||||
.select_related('person', 'organisation', 'venue', 'mic') \
|
||||
.prefetch_related('items')
|
||||
|
||||
return events
|
||||
|
||||
|
||||
@reversion.register(follow=['items'])
|
||||
@python_2_unicode_compatible
|
||||
class Event(models.Model, RevisionMixin):
|
||||
# Done to make it much nicer on the database
|
||||
PROVISIONAL = 0
|
||||
@@ -304,11 +308,11 @@ 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)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
notes = models.TextField(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, default='')
|
||||
notes = models.TextField(blank=True, default='')
|
||||
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
||||
dry_hire = models.BooleanField(default=False)
|
||||
is_rig = models.BooleanField(default=True)
|
||||
@@ -322,18 +326,31 @@ class Event(models.Model, RevisionMixin):
|
||||
end_time = models.TimeField(blank=True, null=True)
|
||||
access_at = models.DateTimeField(blank=True, null=True)
|
||||
meet_at = models.DateTimeField(blank=True, null=True)
|
||||
meet_info = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
# Crew management
|
||||
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True)
|
||||
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)
|
||||
payment_received = models.CharField(max_length=255, blank=True, null=True)
|
||||
purchase_order = models.CharField(max_length=255, blank=True, null=True, verbose_name='PO')
|
||||
collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='collected by')
|
||||
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
|
||||
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
|
||||
|
||||
# Authorisation request details
|
||||
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
|
||||
auth_request_at = models.DateTimeField(null=True, blank=True)
|
||||
auth_request_to = models.EmailField(blank=True, default='')
|
||||
|
||||
@property
|
||||
def display_id(self):
|
||||
if self.pk:
|
||||
if self.is_rig:
|
||||
return str("N%05d" % self.pk)
|
||||
|
||||
return self.pk
|
||||
|
||||
return "????"
|
||||
|
||||
# Calculated values
|
||||
"""
|
||||
@@ -342,18 +359,7 @@ class Event(models.Model, RevisionMixin):
|
||||
|
||||
@property
|
||||
def sum_total(self):
|
||||
# Manual querying is required for efficiency whilst maintaining floating point arithmetic
|
||||
# if connection.vendor == 'postgresql':
|
||||
# sql = "SELECT SUM(quantity * cost) AS sum_total FROM \"RIGS_eventitem\" WHERE event_id=%i" % self.id
|
||||
# else:
|
||||
# sql = "SELECT id, SUM(quantity * cost) AS sum_total FROM RIGS_eventitem WHERE event_id=%i" % self.id
|
||||
# total = self.items.raw(sql)[0]
|
||||
# if total.sum_total:
|
||||
# return total.sum_total
|
||||
# total = 0.0
|
||||
# for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"):
|
||||
# total += item.sum
|
||||
total = EventItem.objects.filter(event=self).aggregate(
|
||||
total = self.items.aggregate(
|
||||
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
|
||||
output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
||||
)['sum_total']
|
||||
@@ -367,7 +373,10 @@ class Event(models.Model, RevisionMixin):
|
||||
|
||||
@property
|
||||
def vat(self):
|
||||
return self.sum_total * self.vat_rate.rate
|
||||
# No VAT is owed on internal transfers
|
||||
if self.internal:
|
||||
return 0
|
||||
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
|
||||
|
||||
"""
|
||||
Inc VAT
|
||||
@@ -375,7 +384,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 +394,10 @@ class Event(models.Model, RevisionMixin):
|
||||
def confirmed(self):
|
||||
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
||||
|
||||
@property
|
||||
def hs_done(self):
|
||||
return self.riskassessment is not None and len(self.checklists.all()) > 0
|
||||
|
||||
@property
|
||||
def has_start_time(self):
|
||||
return self.start_time is not None
|
||||
@@ -445,42 +458,61 @@ class Event(models.Model, RevisionMixin):
|
||||
else:
|
||||
return endDate
|
||||
|
||||
@property
|
||||
def internal(self):
|
||||
return bool(self.organisation and self.organisation.union_account)
|
||||
|
||||
@property
|
||||
def authorised(self):
|
||||
if self.internal:
|
||||
return self.authorisation.amount == self.total
|
||||
else:
|
||||
return bool(self.purchase_order)
|
||||
|
||||
objects = EventManager()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('event_detail', kwargs={'pk': self.pk})
|
||||
return reverse('event_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return unicode(self.pk) + ": " + self.name
|
||||
return f"{self.display_id}: {self.name}"
|
||||
|
||||
def clean(self):
|
||||
errdict = {}
|
||||
if self.end_date and self.start_date > self.end_date:
|
||||
raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.')
|
||||
errdict['end_date'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
|
||||
|
||||
startEndSameDay = not self.end_date or self.end_date == self.start_date
|
||||
hasStartAndEnd = self.has_start_time and self.has_end_time
|
||||
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
|
||||
raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.')
|
||||
errdict['end_time'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
|
||||
|
||||
if self.access_at is not None:
|
||||
if self.access_at.date() > self.start_date:
|
||||
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
|
||||
elif self.start_time is not None and self.start_date == self.access_at.date() and self.access_at.time() > self.start_time:
|
||||
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
|
||||
|
||||
if errdict != {}: # If there was an error when validation
|
||||
raise ValidationError(errdict)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Call :meth:`full_clean` before saving."""
|
||||
self.full_clean()
|
||||
super(Event, self).save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_event', 'Can view Events'),
|
||||
)
|
||||
|
||||
|
||||
class EventItem(models.Model):
|
||||
event = models.ForeignKey('Event', related_name='items', blank=True)
|
||||
@reversion.register
|
||||
class EventItem(models.Model, RevisionMixin):
|
||||
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
description = models.TextField(blank=True, default='')
|
||||
quantity = models.IntegerField()
|
||||
cost = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
order = models.IntegerField()
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
@property
|
||||
def total_cost(self):
|
||||
return self.cost * self.quantity
|
||||
@@ -489,24 +521,58 @@ class EventItem(models.Model):
|
||||
ordering = ['order']
|
||||
|
||||
def __str__(self):
|
||||
return str(self.event.pk) + "." + str(self.order) + ": " + self.event.name + " | " + self.name
|
||||
return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return f"item {self.name}"
|
||||
|
||||
|
||||
class EventCrew(models.Model):
|
||||
event = models.ForeignKey('Event', related_name='crew')
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL)
|
||||
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, default='', verbose_name="University ID")
|
||||
account_code = models.CharField(max_length=50, default='', blank=True)
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
|
||||
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('event_detail', kwargs={'pk': self.event_id})
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Invoice(models.Model):
|
||||
event = models.OneToOneField('Event')
|
||||
class InvoiceManager(models.Manager):
|
||||
def outstanding_invoices(self):
|
||||
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||
sql = "SELECT * FROM " \
|
||||
"(SELECT " \
|
||||
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
||||
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
||||
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
||||
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
||||
"AS sub " \
|
||||
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
||||
"ORDER BY invoice_date"
|
||||
|
||||
query = self.raw(sql)
|
||||
return query
|
||||
|
||||
|
||||
@reversion.register(follow=['payment_set'])
|
||||
class Invoice(models.Model, RevisionMixin):
|
||||
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
||||
invoice_date = models.DateField(auto_now_add=True)
|
||||
void = models.BooleanField(default=False)
|
||||
|
||||
reversion_perm = 'RIGS.view_invoice'
|
||||
|
||||
objects = InvoiceManager()
|
||||
|
||||
@property
|
||||
def sum_total(self):
|
||||
return self.event.sum_total
|
||||
@@ -530,18 +596,26 @@ class Invoice(models.Model):
|
||||
def is_closed(self):
|
||||
return self.balance == 0 or self.void
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('invoice_detail', kwargs={'pk': self.pk})
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return "#{} for Event {}".format(self.display_id, self.event.display_id)
|
||||
|
||||
def __str__(self):
|
||||
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
|
||||
|
||||
@property
|
||||
def display_id(self):
|
||||
return "{:05d}".format(self.pk)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_invoice', 'Can view Invoices'),
|
||||
)
|
||||
ordering = ['-invoice_date']
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Payment(models.Model):
|
||||
@reversion.register
|
||||
class Payment(models.Model, RevisionMixin):
|
||||
CASH = 'C'
|
||||
INTERNAL = 'I'
|
||||
EXTERNAL = 'E'
|
||||
@@ -555,10 +629,250 @@ 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)
|
||||
method = models.CharField(max_length=2, choices=METHODS, default='', blank=True)
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
def __str__(self):
|
||||
return "%s: %d" % (self.get_method_display(), self.amount)
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return str("payment of £{}".format(self.amount))
|
||||
|
||||
|
||||
def validate_url(value):
|
||||
if not value:
|
||||
return # Required error is done the field
|
||||
obj = urlparse(value)
|
||||
if obj.hostname not in ('nottinghamtec.sharepoint.com'):
|
||||
raise ValidationError('URL must point to a location on the TEC Sharepoint')
|
||||
|
||||
|
||||
@reversion.register
|
||||
class RiskAssessment(models.Model, RevisionMixin):
|
||||
SMALL = (0, 'Small')
|
||||
MEDIUM = (1, 'Medium')
|
||||
LARGE = (2, 'Large')
|
||||
SIZES = (SMALL, MEDIUM, LARGE)
|
||||
|
||||
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
||||
# General
|
||||
nonstandard_equipment = models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by <a href='https://nottinghamtec.sharepoint.com/:f:/g/HealthAndSafety/Eo4xED_DrqFFsfYIjKzMZIIB6Gm_ZfR-a8l84RnzxtBjrA?e=Bf0Haw'>"
|
||||
"TEC's standard risk assessments and method statements?</a>")
|
||||
nonstandard_use = models.BooleanField(help_text="Are TEC using their equipment in a way that is abnormal?<br><small>i.e. Not covered by TECs standard health and safety documentation</small>")
|
||||
contractors = models.BooleanField(help_text="Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>")
|
||||
other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>")
|
||||
crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?")
|
||||
general_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||
|
||||
# Power
|
||||
big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
|
||||
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
|
||||
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)")
|
||||
outside = models.BooleanField(help_text="Is the event outdoors?")
|
||||
generators = models.BooleanField(help_text="Will generators be used?")
|
||||
other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?")
|
||||
nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?")
|
||||
multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?")
|
||||
power_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||
power_plan = models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
||||
|
||||
# Sound
|
||||
noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?")
|
||||
sound_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
|
||||
|
||||
# Site
|
||||
known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
|
||||
safe_loading = models.BooleanField(help_text="Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)")
|
||||
safe_storage = models.BooleanField(help_text="Are there any problems with safe and secure equipment storage?")
|
||||
area_outside_of_control = models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")
|
||||
barrier_required = models.BooleanField(help_text="Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?")
|
||||
nonstandard_emergency_procedure = models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")
|
||||
|
||||
# Structures
|
||||
special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?")
|
||||
suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")
|
||||
persons_responsible_structures = models.TextField(blank=True, default='', help_text="Who are the persons on site responsible for their use?")
|
||||
rigging_plan = models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
|
||||
|
||||
# Blimey that was a lot of options
|
||||
|
||||
reviewed_at = models.DateTimeField(null=True)
|
||||
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||
verbose_name="Reviewer", on_delete=models.CASCADE)
|
||||
|
||||
supervisor_consulted = models.BooleanField(null=True)
|
||||
|
||||
expected_values = {
|
||||
'nonstandard_equipment': False,
|
||||
'nonstandard_use': False,
|
||||
'contractors': False,
|
||||
'other_companies': False,
|
||||
'crew_fatigue': False,
|
||||
'big_power': False,
|
||||
'generators': False,
|
||||
'other_companies_power': False,
|
||||
'nonstandard_equipment_power': False,
|
||||
'multiple_electrical_environments': False,
|
||||
'noise_monitoring': False,
|
||||
'known_venue': False,
|
||||
'safe_loading': False,
|
||||
'safe_storage': False,
|
||||
'area_outside_of_control': False,
|
||||
'barrier_required': False,
|
||||
'nonstandard_emergency_procedure': False,
|
||||
'special_structures': False,
|
||||
'suspended_structures': False,
|
||||
}
|
||||
inverted_fields = {key: value for (key, value) in expected_values.items() if not value}.keys()
|
||||
|
||||
def clean(self):
|
||||
# Check for idiots
|
||||
if not self.outside and self.generators:
|
||||
raise forms.ValidationError("Engage brain, please. <strong>No generators indoors!(!)</strong>")
|
||||
|
||||
class Meta:
|
||||
ordering = ['event']
|
||||
permissions = [
|
||||
('review_riskassessment', 'Can review Risk Assessments')
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def fieldz(self):
|
||||
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||
|
||||
@property
|
||||
def event_size(self):
|
||||
# Confirm event size. Check all except generators, since generators entails outside
|
||||
if self.outside or self.other_companies_power or self.nonstandard_equipment_power or self.multiple_electrical_environments:
|
||||
return self.LARGE[0]
|
||||
elif self.big_power:
|
||||
return self.MEDIUM[0]
|
||||
else:
|
||||
return self.SMALL[0]
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return str(self.event)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ra_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return "%i - %s" % (self.pk, self.event)
|
||||
|
||||
|
||||
@reversion.register(follow=['vehicles', 'crew'])
|
||||
class EventChecklist(models.Model, RevisionMixin):
|
||||
event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
|
||||
|
||||
# General
|
||||
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists',
|
||||
verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?")
|
||||
venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
|
||||
date = models.DateField()
|
||||
|
||||
# Safety Checks
|
||||
safe_parking = models.BooleanField(blank=True, null=True, help_text="Vehicles parked safely?<br><small>(does not obstruct venue access)</small>")
|
||||
safe_packing = models.BooleanField(blank=True, null=True, help_text="Equipment packed away safely?<br><small>(including flightcases)</small>")
|
||||
exits = models.BooleanField(blank=True, null=True, help_text="Emergency exits clear?")
|
||||
trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?")
|
||||
warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>")
|
||||
ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?")
|
||||
hs_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of Safety Bag/Box")
|
||||
extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers")
|
||||
|
||||
# Small Electrical Checks
|
||||
rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
|
||||
supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested?<br><small>(using socket tester)</small>")
|
||||
|
||||
# Shared electrical checks
|
||||
earthing = models.BooleanField(blank=True, null=True, help_text="Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>")
|
||||
pat = models.BooleanField(blank=True, null=True, help_text="All equipment in PAT period?")
|
||||
|
||||
# Medium Electrical Checks
|
||||
source_rcd = models.BooleanField(blank=True, null=True, help_text="Source RCD protected?<br><small>(if cable is more than 3m long) </small>")
|
||||
labelling = models.BooleanField(blank=True, null=True, help_text="Appropriate and clear labelling on distribution and cabling?")
|
||||
# First Distro
|
||||
fd_voltage_l1 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L1-N", help_text="L1 - N")
|
||||
fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N")
|
||||
fd_voltage_l3 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N")
|
||||
fd_phase_rotation = models.BooleanField(blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation<br><small>(if required)</small>")
|
||||
fd_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
|
||||
# Worst case points
|
||||
w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||
w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||
w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||
w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||
w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
|
||||
w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
|
||||
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
|
||||
w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
|
||||
|
||||
all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?<br><small>(using test button)</small>")
|
||||
public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>")
|
||||
|
||||
reviewed_at = models.DateTimeField(null=True)
|
||||
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||
verbose_name="Reviewer", on_delete=models.CASCADE)
|
||||
|
||||
inverted_fields = []
|
||||
|
||||
class Meta:
|
||||
ordering = ['event']
|
||||
permissions = [
|
||||
('review_eventchecklist', 'Can review Event Checklists')
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def fieldz(self):
|
||||
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return str(self.event)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ec_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return "%i - %s" % (self.pk, self.event)
|
||||
|
||||
|
||||
@reversion.register
|
||||
class EventChecklistVehicle(models.Model, RevisionMixin):
|
||||
checklist = models.ForeignKey('EventChecklist', related_name='vehicles', blank=True, on_delete=models.CASCADE)
|
||||
vehicle = models.CharField(max_length=255)
|
||||
driver = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='vehicles', on_delete=models.CASCADE)
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
def __str__(self):
|
||||
return "{} driven by {}".format(self.vehicle, str(self.driver))
|
||||
|
||||
|
||||
@reversion.register
|
||||
class EventChecklistCrew(models.Model, RevisionMixin):
|
||||
checklist = models.ForeignKey('EventChecklist', related_name='crew', blank=True, on_delete=models.CASCADE)
|
||||
crewmember = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='crewed', on_delete=models.CASCADE)
|
||||
role = models.CharField(max_length=255)
|
||||
start = models.DateTimeField()
|
||||
end = models.DateTimeField()
|
||||
|
||||
reversion_hide = True
|
||||
|
||||
def clean(self):
|
||||
if self.start > self.end:
|
||||
raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.')
|
||||
|
||||
def __str__(self):
|
||||
return "{} ({})".format(str(self.crewmember), self.role)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from RIGS.models import Profile
|
||||
from RIGS.forms import ProfileRegistrationFormUniqueEmail
|
||||
|
||||
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()
|
||||
|
||||
from registration.signals import user_registered
|
||||
user_registered.connect(user_created)
|
||||
395
RIGS/rigboard.py
395
RIGS/rigboard.py
@@ -1,68 +1,100 @@
|
||||
import os
|
||||
import cStringIO as StringIO
|
||||
from io import BytesIO
|
||||
import urllib2
|
||||
|
||||
from django.views import generic
|
||||
from django.core.urlresolvers 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.http import HttpResponse
|
||||
from django.db.models import Q
|
||||
from django.contrib import messages
|
||||
from z3c.rml import rml2pdf
|
||||
from PyPDF2 import PdfFileMerger, PdfFileReader
|
||||
|
||||
from RIGS import models, forms
|
||||
import copy
|
||||
import datetime
|
||||
import re
|
||||
import copy
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from io import BytesIO
|
||||
|
||||
import premailer
|
||||
import simplejson
|
||||
from PyPDF2 import PdfFileMerger, PdfFileReader
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core import signing
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import generic
|
||||
from z3c.rml import rml2pdf
|
||||
|
||||
from PyRIGS import decorators
|
||||
from PyRIGS.views import OEmbedView, is_ajax
|
||||
from RIGS import models, forms
|
||||
|
||||
__author__ = 'ghost'
|
||||
|
||||
|
||||
class RigboardIndex(generic.TemplateView):
|
||||
template_name = 'RIGS/rigboard.html'
|
||||
template_name = 'rigboard.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# get super context
|
||||
context = super(RigboardIndex, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# call out method to get current events
|
||||
context['events'] = models.Event.objects.current_events()
|
||||
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
|
||||
context['page_title'] = "Rigboard"
|
||||
return context
|
||||
|
||||
|
||||
class WebCalendar(generic.TemplateView):
|
||||
template_name = 'RIGS/calendar.html'
|
||||
template_name = '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 = super().get_context_data(**kwargs)
|
||||
context['view'] = kwargs.get('view', '')
|
||||
context['date'] = kwargs.get('date', '')
|
||||
return context
|
||||
|
||||
|
||||
class EventDetail(generic.DetailView):
|
||||
template_name = 'event_detail.html'
|
||||
model = models.Event
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
title = f"{self.object.display_id} | {self.object.name}"
|
||||
if self.object.dry_hire:
|
||||
title += " <span class='badge badge-secondary'>Dry Hire</span>"
|
||||
context['page_title'] = title
|
||||
return context
|
||||
|
||||
|
||||
class EventEmbed(EventDetail):
|
||||
template_name = 'event_embed.html'
|
||||
|
||||
|
||||
class EventOEmbed(OEmbedView):
|
||||
model = models.Event
|
||||
url_name = 'event_embed'
|
||||
|
||||
|
||||
class EventCreate(generic.CreateView):
|
||||
model = models.Event
|
||||
form_class = forms.EventForm
|
||||
template_name = 'event_form.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventCreate, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['page_title'] = "New Event"
|
||||
context['edit'] = True
|
||||
context['currentVAT'] = models.VatRate.objects.current_rate()
|
||||
|
||||
form = context['form']
|
||||
if re.search('"-\d+"', form['items_json'].value()):
|
||||
if hasattr(form, 'items_json') and re.search(r'"-\d+"', form['items_json'].value()):
|
||||
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
|
||||
|
||||
|
||||
# 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)
|
||||
@@ -75,93 +107,117 @@ class EventCreate(generic.CreateView):
|
||||
class EventUpdate(generic.UpdateView):
|
||||
model = models.Event
|
||||
form_class = forms.EventForm
|
||||
template_name = 'event_form.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventUpdate, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['page_title'] = f"Event {self.object.display_id}"
|
||||
context['edit'] = True
|
||||
|
||||
form = context['form']
|
||||
|
||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
||||
for field, model in form.related_models.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)
|
||||
|
||||
return context
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
if hasattr(context, 'duplicate') and not context['duplicate']:
|
||||
# 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 the client, any changes to the price will require reauthorisation.')
|
||||
return super().render_to_response(context, **response_kwargs)
|
||||
|
||||
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().get_object(queryset) # Get the object (the event you're duplicating)
|
||||
new = copy.copy(old) # Make a copy of the object in memory
|
||||
new.based_on = old # Make the new event based on the old event
|
||||
new.purchase_order = None # Remove old PO
|
||||
new.status = new.PROVISIONAL # Return status to provisional
|
||||
|
||||
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
|
||||
# Clear checked in by if it's a dry hire
|
||||
if new.dry_hire is True:
|
||||
new.checked_in_by = None
|
||||
new.collector = None
|
||||
|
||||
messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.')
|
||||
# Remove all the authorisation information from the new event
|
||||
new.auth_request_to = ''
|
||||
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.')
|
||||
|
||||
return new
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventDuplicate, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['page_title'] = f"Duplicate of Event {self.object.display_id}"
|
||||
context["duplicate"] = True
|
||||
return context
|
||||
|
||||
|
||||
class EventPrint(generic.View):
|
||||
def get(self, request, pk):
|
||||
object = get_object_or_404(models.Event, pk=pk)
|
||||
template = get_template('RIGS/event_print.xml')
|
||||
copies = ('TEC', 'Client')
|
||||
template = get_template('event_print.xml')
|
||||
|
||||
merger = PdfFileMerger()
|
||||
|
||||
for copy in copies:
|
||||
context = {
|
||||
'object': object,
|
||||
'quote': True,
|
||||
'current_user': request.user,
|
||||
'filename': 'Event_{}_{}_{}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
|
||||
}
|
||||
|
||||
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)
|
||||
buffer = rml2pdf.parseString(rml)
|
||||
merger.append(PdfFileReader(buffer))
|
||||
buffer.close()
|
||||
|
||||
# context['copy'] = copy # this is the way to do it once we upgrade to Django 1.8.3
|
||||
|
||||
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)
|
||||
|
||||
response = HttpResponse(content_type='application/pdf')
|
||||
|
||||
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
|
||||
|
||||
response['Content-Disposition'] = "filename=N%05d | %s.pdf" % (object.pk, escapedEventName)
|
||||
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
|
||||
response.write(merged.getvalue())
|
||||
return response
|
||||
|
||||
class EventArchive(generic.ArchiveIndexView):
|
||||
|
||||
class EventArchive(generic.ListView):
|
||||
template_name = "event_archive.html"
|
||||
model = models.Event
|
||||
date_field = "start_date"
|
||||
paginate_by = 25
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['start'] = self.request.GET.get('start', None)
|
||||
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
|
||||
context['statuses'] = models.Event.EVENT_STATUS_CHOICES
|
||||
context['page_title'] = 'Event Archive'
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
start = self.request.GET.get('start', None)
|
||||
end = self.request.GET.get('end', datetime.date.today())
|
||||
@@ -172,24 +228,195 @@ class EventArchive(generic.ArchiveIndexView):
|
||||
"Muppet! Check the dates, it has been fixed for you.")
|
||||
start, end = end, start # Stop the impending fail
|
||||
|
||||
filter = False
|
||||
filter = Q()
|
||||
if end != "":
|
||||
filter = Q(start_date__lte=end)
|
||||
filter &= Q(start_date__lte=end)
|
||||
if start:
|
||||
if filter:
|
||||
filter = filter & Q(start_date__gte=start)
|
||||
else:
|
||||
filter = Q(start_date__gte=start)
|
||||
filter &= Q(start_date__gte=start)
|
||||
|
||||
if filter:
|
||||
qs = self.model.objects.filter(filter).order_by('-start_date')
|
||||
else:
|
||||
qs = self.model.objects.all().order_by('-start_date')
|
||||
q = self.request.GET.get('q', "")
|
||||
|
||||
if q != "":
|
||||
qfilter = Q(name__icontains=q) | Q(description__icontains=q) | Q(notes__icontains=q)
|
||||
|
||||
# try and parse an int
|
||||
try:
|
||||
val = int(q)
|
||||
qfilter = qfilter | Q(pk=val)
|
||||
except: # noqa not an integer
|
||||
pass
|
||||
|
||||
try:
|
||||
if q[0] == "N":
|
||||
val = int(q[1:])
|
||||
qfilter = Q(pk=val) # If string is N###### then do a simple PK filter
|
||||
except: # noqa
|
||||
pass
|
||||
|
||||
filter &= qfilter
|
||||
|
||||
status = self.request.GET.getlist('status', "")
|
||||
|
||||
if len(status) > 0:
|
||||
filter &= Q(status__in=status)
|
||||
|
||||
qs = self.model.objects.filter(filter).order_by('-start_date')
|
||||
|
||||
# Preselect related for efficiency
|
||||
qs.select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
if len(qs) == 0:
|
||||
if not qs.exists():
|
||||
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
|
||||
|
||||
return qs
|
||||
return qs
|
||||
|
||||
|
||||
class EventAuthorise(generic.UpdateView):
|
||||
template_name = 'eventauthorisation_form.html'
|
||||
success_template = 'eventauthorisation_success.html'
|
||||
preview = False
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object = form.save()
|
||||
|
||||
self.template_name = self.success_template
|
||||
messages.add_message(self.request, messages.SUCCESS,
|
||||
'Success! Your event has been authorised. ' +
|
||||
f'You will also receive email confirmation to {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().get_context_data(**kwargs)
|
||||
context['event'] = self.event
|
||||
context['tos_url'] = settings.TERMS_OF_HIRE_URL
|
||||
context['page_title'] = f"{self.event.display_id}: {self.event.name}"
|
||||
if self.event.dry_hire:
|
||||
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
|
||||
context['preview'] = self.preview
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
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().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().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
|
||||
model = models.Event
|
||||
form_class = forms.EventAuthorisationRequestForm
|
||||
template_name = 'eventauthorisation_request.html'
|
||||
|
||||
@method_decorator(decorators.nottinghamtec_address_required)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def object(self):
|
||||
return self.get_object()
|
||||
|
||||
def get_success_url(self):
|
||||
if is_ajax(self.request):
|
||||
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 = timezone.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 event.person is not None and email == event.person.email:
|
||||
context['to_name'] = event.person.name
|
||||
elif event.organisation is not None and email == event.organisation.email:
|
||||
context['to_name'] = event.organisation.name
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
"N%05d | %s - Event Authorisation Request" % (self.object.pk, self.object.name),
|
||||
get_template("eventauthorisation_client_request.txt").render(context),
|
||||
to=[email],
|
||||
reply_to=[self.request.user.email],
|
||||
)
|
||||
css = finders.find('css/email.css')
|
||||
html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
|
||||
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 = "eventauthorisation_client_request.html"
|
||||
model = models.Event
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
css = finders.find('css/email.css')
|
||||
response = super().render_to_response(context, **response_kwargs)
|
||||
assert isinstance(response, HttpResponse)
|
||||
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['hmac'] = signing.dumps({
|
||||
'pk': self.object.pk,
|
||||
'email': self.request.GET.get('email', 'hello@world.test'),
|
||||
'sent_by': self.request.user.pk,
|
||||
})
|
||||
context['to_name'] = self.request.GET.get('to_name', None)
|
||||
context['target'] = 'event_authorise_form_preview'
|
||||
return context
|
||||
|
||||
143
RIGS/signals.py
Normal file
143
RIGS/signals.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from io import BytesIO
|
||||
|
||||
from PyPDF2 import PdfFileReader, PdfFileMerger
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.cache import cache
|
||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
||||
from django.db.models.signals import post_save
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from premailer import Premailer
|
||||
from registration.signals import user_activated
|
||||
from reversion import revisions as reversion
|
||||
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,
|
||||
'receipt': True,
|
||||
'current_user': False,
|
||||
}
|
||||
|
||||
template = get_template('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.event.person is not None and instance.email == instance.event.person.email:
|
||||
context['to_name'] = instance.event.person.name
|
||||
elif instance.event.organisation is not None and instance.email == instance.event.organisation.email:
|
||||
context['to_name'] = instance.event.organisation.name
|
||||
|
||||
subject = "N%05d | %s - Event Authorised" % (instance.event.pk, instance.event.name)
|
||||
|
||||
client_email = EmailMultiAlternatives(
|
||||
subject,
|
||||
get_template("eventauthorisation_client_success.txt").render(context),
|
||||
to=[instance.email],
|
||||
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
|
||||
)
|
||||
|
||||
css = finders.find('css/email.css')
|
||||
html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
|
||||
external_styles=css).transform()
|
||||
client_email.attach_alternative(html, 'text/html')
|
||||
|
||||
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
|
||||
|
||||
client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
|
||||
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("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)
|
||||
|
||||
# Set event to booked now that it's authorised
|
||||
instance.event.status = models.Event.BOOKED
|
||||
instance.event.save()
|
||||
|
||||
|
||||
def on_revision_commit(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
send_eventauthorisation_success_email(instance)
|
||||
|
||||
|
||||
post_save.connect(on_revision_commit, sender=models.EventAuthorisation)
|
||||
|
||||
|
||||
def send_admin_awaiting_approval_email(user, request, **kwargs):
|
||||
# Bit more controlled than just emailing all superusers
|
||||
for admin in models.Profile.admins():
|
||||
# Check we've ever emailed them before and if so, if cooldown has passed.
|
||||
if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now():
|
||||
context = {
|
||||
'request': request,
|
||||
'link_suffix': reverse("admin:RIGS_profile_changelist") + '?is_approved__exact=0',
|
||||
'number_of_users': models.Profile.users_awaiting_approval_count(),
|
||||
'to_name': admin.first_name
|
||||
}
|
||||
|
||||
email = EmailMultiAlternatives(
|
||||
"%s new users awaiting approval on RIGS" % (context['number_of_users']),
|
||||
get_template("admin_awaiting_approval.txt").render(context),
|
||||
to=[admin.email],
|
||||
reply_to=[user.email],
|
||||
)
|
||||
css = finders.find('css/email.css')
|
||||
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
|
||||
external_styles=css).transform()
|
||||
email.attach_alternative(html, 'text/html')
|
||||
email.send()
|
||||
|
||||
# Update last sent
|
||||
admin.last_emailed = timezone.now()
|
||||
admin.save()
|
||||
|
||||
|
||||
user_activated.connect(send_admin_awaiting_approval_email)
|
||||
|
||||
|
||||
def update_cache(sender, instance, created, **kwargs):
|
||||
cache.clear()
|
||||
|
||||
|
||||
for model in reversion.get_registered_models():
|
||||
post_save.connect(update_cache, sender=model)
|
||||
@@ -1,27 +0,0 @@
|
||||
# Require any additional compass plugins here.
|
||||
require 'bootstrap-sass'
|
||||
|
||||
# Set this to the root of your project when deployed:
|
||||
http_path = "/static/"
|
||||
css_dir = "css"
|
||||
sass_dir = "scss"
|
||||
images_dir = "img"
|
||||
javascripts_dir = "js"
|
||||
fonts_dir = "fonts"
|
||||
|
||||
# You can select your preferred output style here (can be overridden via the command line):
|
||||
# output_style = :expanded or :nested or :compact or :compressed
|
||||
output_style = :compressed
|
||||
|
||||
# To enable relative paths to assets via compass helper functions. Uncomment:
|
||||
# relative_assets = true
|
||||
|
||||
# To disable debugging comments that display the original location of your selectors. Uncomment:
|
||||
# line_comments = false
|
||||
|
||||
|
||||
# If you prefer the indented syntax, you might want to regenerate this
|
||||
# project again passing --syntax sass, or you can uncomment this:
|
||||
# preferred_syntax = :sass
|
||||
# and then run:
|
||||
# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass
|
||||
@@ -1,27 +0,0 @@
|
||||
/*!
|
||||
* Ajax Bootstrap Select
|
||||
*
|
||||
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
|
||||
*
|
||||
* @version 1.3.1
|
||||
* @author Adam Heim - https://github.com/truckingsim
|
||||
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
|
||||
* @copyright 2015 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
|
||||
*/
|
||||
.bootstrap-select .status {
|
||||
background: #f0f0f0;
|
||||
clear: both;
|
||||
color: #999;
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
margin-bottom: -5px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
366
RIGS/static/css/bootstrap-datetimepicker.min.css
vendored
366
RIGS/static/css/bootstrap-datetimepicker.min.css
vendored
@@ -1,366 +0,0 @@
|
||||
/*!
|
||||
* Datetimepicker for Bootstrap 3
|
||||
* ! version : 4.7.14
|
||||
* https://github.com/Eonasdan/bootstrap-datetimepicker/
|
||||
*/
|
||||
.bootstrap-datetimepicker-widget {
|
||||
list-style: none;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu {
|
||||
margin: 2px 0;
|
||||
padding: 4px;
|
||||
width: 19em;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
||||
width: 38em;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
||||
width: 38em;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
||||
width: 38em;
|
||||
}
|
||||
}
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu:before,
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before {
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-bottom: 7px solid #cccccc;
|
||||
border-bottom-color: rgba(0, 0, 0, 0.2);
|
||||
top: -7px;
|
||||
left: 7px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid white;
|
||||
top: -6px;
|
||||
left: 8px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.top:before {
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-top: 7px solid #cccccc;
|
||||
border-top-color: rgba(0, 0, 0, 0.2);
|
||||
bottom: -7px;
|
||||
left: 6px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.top:after {
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid white;
|
||||
bottom: -6px;
|
||||
left: 7px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before {
|
||||
left: auto;
|
||||
right: 6px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after {
|
||||
left: auto;
|
||||
right: 7px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .list-unstyled {
|
||||
margin: 0;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget a[data-action] {
|
||||
padding: 6px 0;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget a[data-action]:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .timepicker-hour,
|
||||
.bootstrap-datetimepicker-widget .timepicker-minute,
|
||||
.bootstrap-datetimepicker-widget .timepicker-second {
|
||||
width: 54px;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
margin: 0;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget button[data-action] {
|
||||
padding: 6px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
content: "Increment Hours";
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
content: "Increment Minutes";
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
content: "Decrement Hours";
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
content: "Decrement Minutes";
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
content: "Show Hours";
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
content: "Show Minutes";
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
content: "Toggle AM/PM";
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
content: "Clear the picker";
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .btn[data-action="today"]::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
content: "Set the date to today";
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .picker-switch {
|
||||
text-align: center;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .picker-switch::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
content: "Toggle Date and Time Screens";
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .picker-switch td {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: auto;
|
||||
width: auto;
|
||||
line-height: inherit;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget .picker-switch td span {
|
||||
line-height: 2.5;
|
||||
height: 2.5em;
|
||||
width: 100%;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td,
|
||||
.bootstrap-datetimepicker-widget table th {
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table th {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table th.picker-switch {
|
||||
width: 145px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table th.disabled,
|
||||
.bootstrap-datetimepicker-widget table th.disabled:hover {
|
||||
background: none;
|
||||
color: #777777;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table th.prev::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
content: "Previous Month";
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table th.next::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
content: "Next Month";
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table thead tr:first-child th {
|
||||
cursor: pointer;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table thead tr:first-child th:hover {
|
||||
background: #eeeeee;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td {
|
||||
height: 54px;
|
||||
line-height: 54px;
|
||||
width: 54px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td.cw {
|
||||
font-size: .8em;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
color: #777777;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td.day {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td.day:hover,
|
||||
.bootstrap-datetimepicker-widget table td.hour:hover,
|
||||
.bootstrap-datetimepicker-widget table td.minute:hover,
|
||||
.bootstrap-datetimepicker-widget table td.second:hover {
|
||||
background: #eeeeee;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td.old,
|
||||
.bootstrap-datetimepicker-widget table td.new {
|
||||
color: #777777;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td.today {
|
||||
position: relative;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td.today:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
border: 0 0 7px 7px solid transparent;
|
||||
border-bottom-color: #337ab7;
|
||||
border-top-color: rgba(0, 0, 0, 0.2);
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td.active,
|
||||
.bootstrap-datetimepicker-widget table td.active:hover {
|
||||
background-color: #337ab7;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td.active.today:before {
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td.disabled,
|
||||
.bootstrap-datetimepicker-widget table td.disabled:hover {
|
||||
background: none;
|
||||
color: #777777;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td span {
|
||||
display: inline-block;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
line-height: 54px;
|
||||
margin: 2px 1.5px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td span:hover {
|
||||
background: #eeeeee;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td span.active {
|
||||
background-color: #337ab7;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td span.old {
|
||||
color: #777777;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget table td span.disabled,
|
||||
.bootstrap-datetimepicker-widget table td span.disabled:hover {
|
||||
background: none;
|
||||
color: #777777;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.bootstrap-datetimepicker-widget.usetwentyfour td.hour {
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
}
|
||||
.input-group.date .input-group-addon {
|
||||
cursor: pointer;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
6
RIGS/static/css/bootstrap-select.min.css
vendored
6
RIGS/static/css/bootstrap-select.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,202 +0,0 @@
|
||||
/*!
|
||||
* FullCalendar v2.3.1 Print Stylesheet
|
||||
* Docs & License: http://fullcalendar.io/
|
||||
* (c) 2015 Adam Shaw
|
||||
*/
|
||||
|
||||
/*
|
||||
* Include this stylesheet on your page to get a more printer-friendly calendar.
|
||||
* When including this stylesheet, use the media='print' attribute of the <link> tag.
|
||||
* Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css.
|
||||
*/
|
||||
|
||||
.fc {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
|
||||
/* Global Event Restyling
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-event {
|
||||
background: #fff !important;
|
||||
color: #000 !important;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.fc-event .fc-resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Table & Day-Row Restyling
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
th,
|
||||
td,
|
||||
hr,
|
||||
thead,
|
||||
tbody,
|
||||
.fc-row {
|
||||
border-color: #ccc !important;
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
/* kill the overlaid, absolutely-positioned common components */
|
||||
.fc-bg,
|
||||
.fc-bgevent-skeleton,
|
||||
.fc-highlight-skeleton,
|
||||
.fc-helper-skeleton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* don't force a min-height on rows (for DayGrid) */
|
||||
.fc tbody .fc-row {
|
||||
height: auto !important; /* undo height that JS set in distributeHeight */
|
||||
min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */
|
||||
}
|
||||
|
||||
.fc tbody .fc-row .fc-content-skeleton {
|
||||
position: static; /* undo .fc-rigid */
|
||||
padding-bottom: 0 !important; /* use a more border-friendly method for this... */
|
||||
}
|
||||
|
||||
.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */
|
||||
padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */
|
||||
}
|
||||
|
||||
.fc tbody .fc-row .fc-content-skeleton table {
|
||||
/* provides a min-height for the row, but only effective for IE, which exaggerates this value,
|
||||
making it look more like 3em. for other browers, it will already be this tall */
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
|
||||
/* Undo month-view event limiting. Display all events and hide the "more" links
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-more-cell,
|
||||
.fc-more {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fc tr.fc-limited {
|
||||
display: table-row !important;
|
||||
}
|
||||
|
||||
.fc td.fc-limited {
|
||||
display: table-cell !important;
|
||||
}
|
||||
|
||||
.fc-popover {
|
||||
display: none; /* never display the "more.." popover in print mode */
|
||||
}
|
||||
|
||||
|
||||
/* TimeGrid Restyling
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* undo the min-height 100% trick used to fill the container's height */
|
||||
.fc-time-grid {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
/* don't display the side axis at all ("all-day" and time cells) */
|
||||
.fc-agenda-view .fc-axis {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* don't display the horizontal lines */
|
||||
.fc-slats,
|
||||
.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */
|
||||
display: none !important; /* important overrides inline declaration */
|
||||
}
|
||||
|
||||
/* let the container that holds the events be naturally positioned and create real height */
|
||||
.fc-time-grid .fc-content-skeleton {
|
||||
position: static;
|
||||
}
|
||||
|
||||
/* in case there are no events, we still want some height */
|
||||
.fc-time-grid .fc-content-skeleton table {
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
/* kill the horizontal spacing made by the event container. event margins will be done below */
|
||||
.fc-time-grid .fc-event-container {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
/* TimeGrid *Event* Restyling
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* naturally position events, vertically stacking them */
|
||||
.fc-time-grid .fc-event {
|
||||
position: static !important;
|
||||
margin: 3px 2px !important;
|
||||
}
|
||||
|
||||
/* for events that continue to a future day, give the bottom border back */
|
||||
.fc-time-grid .fc-event.fc-not-end {
|
||||
border-bottom-width: 1px !important;
|
||||
}
|
||||
|
||||
/* indicate the event continues via "..." text */
|
||||
.fc-time-grid .fc-event.fc-not-end:after {
|
||||
content: "...";
|
||||
}
|
||||
|
||||
/* for events that are continuations from previous days, give the top border back */
|
||||
.fc-time-grid .fc-event.fc-not-start {
|
||||
border-top-width: 1px !important;
|
||||
}
|
||||
|
||||
/* indicate the event is a continuation via "..." text */
|
||||
.fc-time-grid .fc-event.fc-not-start:before {
|
||||
content: "...";
|
||||
}
|
||||
|
||||
/* time */
|
||||
|
||||
/* undo a previous declaration and let the time text span to a second line */
|
||||
.fc-time-grid .fc-event .fc-time {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
/* hide the the time that is normally displayed... */
|
||||
.fc-time-grid .fc-event .fc-time span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */
|
||||
.fc-time-grid .fc-event .fc-time:after {
|
||||
content: attr(data-full);
|
||||
}
|
||||
|
||||
|
||||
/* Vertical Scroller & Containers
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* kill the scrollbars and allow natural height */
|
||||
.fc-scroller,
|
||||
.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */
|
||||
.fc-time-grid-container { /* */
|
||||
overflow: visible !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/* kill the horizontal border/padding used to compensate for scrollbars */
|
||||
.fc-row {
|
||||
border: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
/* Button Controls
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-button-group,
|
||||
.fc button {
|
||||
display: none; /* don't display any button-related controls */
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user