Compare commits
239 Commits
bugfix-dat
...
f35ce88acc
| Author | SHA1 | Date | |
|---|---|---|---|
|
f35ce88acc
|
|||
|
ee5468fdd7
|
|||
|
1ce6ec3284
|
|||
|
677f352524
|
|||
|
8b3102b136
|
|||
|
e2b1dc1d05
|
|||
|
c9ba228bd2
|
|||
|
9f4cd41d23
|
|||
|
2049d0f76d
|
|||
|
29db3b5a0c
|
|||
|
53b09e47b8
|
|||
|
097e7c2481
|
|||
| 16874073e9 | |||
|
|
d03a4e115f | ||
| e1b87b412a | |||
| 54b44404ba | |||
|
|
26942b80dd | ||
|
|
888300490c | ||
| 9201f9d896 | |||
| 9fae129e26 | |||
|
|
8d45e260dd | ||
| 7d8dddb952 | |||
| 1104f10c91 | |||
| 3d5efba0af | |||
| 9a44aaf557 | |||
| 550eff83ee | |||
| bf3da5ae25 | |||
| 87aa87bc0f | |||
| 01a0b8f831 | |||
|
724762a1e8
|
|||
|
6ea5dc9698
|
|||
|
|
eb45db8950 | ||
|
b1a2859f1b
|
|||
|
dc71c2de62
|
|||
| 8863d86ed0 | |||
|
|
6550ed2318 | ||
|
|
c9759a6339 | ||
|
|
b637c4e452 | ||
|
|
8986b94b07 | ||
| 86c033ba97 | |||
| 52fd662340 | |||
| 9818ed995f | |||
| 5178614d71 | |||
| a7bf990666 | |||
|
a4a28a6130
|
|||
|
e3d8cf8978
|
|||
|
626779ef25
|
|||
| fa1dc31639 | |||
| d69543e309 | |||
|
|
a24e6d4495 | ||
|
|
fa5792914a | ||
| 0117091f3e | |||
|
37101d3340
|
|||
| de4bed92a4 | |||
|
|
3767923175 | ||
|
|
1d77cf95d3 | ||
|
|
1f21d0b265 | ||
| 7846a6d31e | |||
| d28b73a0b8 | |||
|
|
5c2e8b391c | ||
|
|
548bc1df81 | ||
|
|
c1d2bce8fb | ||
|
c71beab278
|
|||
|
259932a548
|
|||
|
7526485837
|
|||
| 39ed5aefb4 | |||
|
e7e760de2e
|
|||
| 9091197639 | |||
|
4f4baa62c1
|
|||
|
b9f8621e1a
|
|||
|
4b1dc37a7f
|
|||
|
|
9273ca35cf | ||
|
|
4a4b7fa30d | ||
| a44a532c7d | |||
| 3a2e5c943b | |||
| 426a9088cc | |||
|
|
1369a2f978 | ||
|
38eafbced3
|
|||
|
900002bf71
|
|||
|
2869c9fcc3
|
|||
|
00eb4e0e27
|
|||
|
23e17b0e34
|
|||
|
bf268a4566
|
|||
|
dedb8d81fe
|
|||
|
7d785f4f1b
|
|||
|
5eb113156b
|
|||
|
ab03ad081a
|
|||
| cd5889f60e | |||
| f18bf3b077 | |||
|
3d36d986a4
|
|||
|
41f5a23ef0
|
|||
|
09f48f740d
|
|||
|
805d77af20
|
|||
|
fabab87e23
|
|||
|
a95779e04e
|
|||
|
24e6ba540d
|
|||
|
14d3522b81
|
|||
|
e4cfaba57d
|
|||
|
d9664422c5
|
|||
|
27bb3f1d8e
|
|||
|
151ac8b3bd
|
|||
|
c2dcd86d5d
|
|||
|
6c14b30c13
|
|||
|
5215af349a
|
|||
|
a5e888fef5
|
|||
|
|
2ae4e4142c | ||
|
8799f822bb
|
|||
|
2dd3d306b4
|
|||
|
042004e1ae
|
|||
|
733ea69cc5
|
|||
|
bbea47e8ec
|
|||
|
c4aafbd7e5
|
|||
|
ccdc13df93
|
|||
|
aa19ceaf18
|
|||
|
05d280172d
|
|||
|
2f51b7b1d3
|
|||
|
8d1edb54ea
|
|||
| 54c90a7be4 | |||
|
3e1e0079d8
|
|||
|
b6952aeb52
|
|||
|
d33a4231fb
|
|||
|
8dea6aeab0
|
|||
|
34c03e379d
|
|||
|
988fb78b45
|
|||
|
eda314c092
|
|||
|
8ef520619a
|
|||
|
95931f86b4
|
|||
|
cc2cb5c4d1
|
|||
|
3ae507b469
|
|||
|
33754eed60
|
|||
|
15ab626593
|
|||
|
7bc47b446c
|
|||
|
83b287a418
|
|||
|
3b9848d457
|
|||
|
308d0c697e
|
|||
|
f243a589fa
|
|||
|
79c90ac92c
|
|||
|
8244287a64
|
|||
|
da4d62729b
|
|||
|
f8a48798de
|
|||
|
fc817fa9b5
|
|||
|
b04a168f01
|
|||
|
cc6992976e
|
|||
|
a556b17d2d
|
|||
|
f9e38338dc
|
|||
|
ce83ae6dd1
|
|||
|
9e1d54dc02
|
|||
| 375b0af2fd | |||
|
|
0354662864 | ||
|
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 |
@@ -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]
|
[run]
|
||||||
source =
|
omit = */migrations/*
|
||||||
./
|
*/tests/*
|
||||||
|
*/site-packages/*
|
||||||
omit =
|
*/distutils/*
|
||||||
*/migrations/*
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
151
.github/workflows/combine-prs.yml
vendored
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
name: 'Combine PRs'
|
||||||
|
|
||||||
|
# Controls when the action will run - in this case triggered manually
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
branchPrefix:
|
||||||
|
description: 'Branch prefix to find combinable PRs based on'
|
||||||
|
required: true
|
||||||
|
default: 'dependabot'
|
||||||
|
mustBeGreen:
|
||||||
|
description: 'Only combine PRs that are green (status is success)'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
combineBranchName:
|
||||||
|
description: 'Name of the branch to combine PRs into'
|
||||||
|
required: true
|
||||||
|
default: 'combine-prs-branch'
|
||||||
|
ignoreLabel:
|
||||||
|
description: 'Exclude PRs with this label'
|
||||||
|
required: true
|
||||||
|
default: 'nocombine'
|
||||||
|
|
||||||
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
|
jobs:
|
||||||
|
# This workflow contains a single job called "combine-prs"
|
||||||
|
combine-prs:
|
||||||
|
# The type of runner that the job will run on
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
|
steps:
|
||||||
|
- uses: actions/github-script@v6
|
||||||
|
id: create-combined-pr
|
||||||
|
name: Create Combined PR
|
||||||
|
with:
|
||||||
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
script: |
|
||||||
|
const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo
|
||||||
|
});
|
||||||
|
let branchesAndPRStrings = [];
|
||||||
|
let baseBranch = null;
|
||||||
|
let baseBranchSHA = null;
|
||||||
|
for (const pull of pulls) {
|
||||||
|
const branch = pull['head']['ref'];
|
||||||
|
console.log('Pull for branch: ' + branch);
|
||||||
|
if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) {
|
||||||
|
console.log('Branch matched prefix: ' + branch);
|
||||||
|
let statusOK = true;
|
||||||
|
if(${{ github.event.inputs.mustBeGreen }}) {
|
||||||
|
console.log('Checking green status: ' + branch);
|
||||||
|
const stateQuery = `query($owner: String!, $repo: String!, $pull_number: Int!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
pullRequest(number:$pull_number) {
|
||||||
|
commits(last: 1) {
|
||||||
|
nodes {
|
||||||
|
commit {
|
||||||
|
statusCheckRollup {
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
const vars = {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: pull['number']
|
||||||
|
};
|
||||||
|
const result = await github.graphql(stateQuery, vars);
|
||||||
|
const [{ commit }] = result.repository.pullRequest.commits.nodes;
|
||||||
|
const state = commit.statusCheckRollup.state
|
||||||
|
console.log('Validating status: ' + state);
|
||||||
|
if(state != 'SUCCESS') {
|
||||||
|
console.log('Discarding ' + branch + ' with status ' + state);
|
||||||
|
statusOK = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Checking labels: ' + branch);
|
||||||
|
const labels = pull['labels'];
|
||||||
|
for(const label of labels) {
|
||||||
|
const labelName = label['name'];
|
||||||
|
console.log('Checking label: ' + labelName);
|
||||||
|
if(labelName == '${{ github.event.inputs.ignoreLabel }}') {
|
||||||
|
console.log('Discarding ' + branch + ' with label ' + labelName);
|
||||||
|
statusOK = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (statusOK) {
|
||||||
|
console.log('Adding branch to array: ' + branch);
|
||||||
|
const prString = '#' + pull['number'] + ' ' + pull['title'];
|
||||||
|
branchesAndPRStrings.push({ branch, prString });
|
||||||
|
baseBranch = pull['base']['ref'];
|
||||||
|
baseBranchSHA = pull['base']['sha'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (branchesAndPRStrings.length == 0) {
|
||||||
|
core.setFailed('No PRs/branches matched criteria');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await github.rest.git.createRef({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}',
|
||||||
|
sha: baseBranchSHA
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let combinedPRs = [];
|
||||||
|
let mergeFailedPRs = [];
|
||||||
|
for(const { branch, prString } of branchesAndPRStrings) {
|
||||||
|
try {
|
||||||
|
await github.rest.repos.merge({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
base: '${{ github.event.inputs.combineBranchName }}',
|
||||||
|
head: branch,
|
||||||
|
});
|
||||||
|
console.log('Merged branch ' + branch);
|
||||||
|
combinedPRs.push(prString);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to merge branch ' + branch);
|
||||||
|
mergeFailedPRs.push(prString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Creating combined PR');
|
||||||
|
const combinedPRsString = combinedPRs.join('\n');
|
||||||
|
let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString;
|
||||||
|
if(mergeFailedPRs.length > 0) {
|
||||||
|
const mergeFailedPRsString = mergeFailedPRs.join('\n');
|
||||||
|
body += '\n\n⚠️ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString
|
||||||
|
}
|
||||||
|
await github.rest.pulls.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title: 'Combined PR',
|
||||||
|
head: '${{ github.event.inputs.combineBranchName }}',
|
||||||
|
base: baseBranch,
|
||||||
|
body: body
|
||||||
|
});
|
||||||
14
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
name: Manual Deploy
|
||||||
|
|
||||||
|
on: workflow_dispatch
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: akhileshns/heroku-deploy@v3.12.12 # This is the action
|
||||||
|
with:
|
||||||
|
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
|
||||||
|
heroku_app_name: "pyrigs" #Must be unique in Heroku
|
||||||
|
heroku_email: "aj@aronajones.com"
|
||||||
53
.github/workflows/django.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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 }}
|
||||||
|
PYTHONDONTWRITEBYTECODE: 1
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
cache: 'pipenv'
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
python3 -m pip install --upgrade pip pipenv
|
||||||
|
pipenv install -d
|
||||||
|
# if: steps.pcache.outputs.cache-hit != 'true'
|
||||||
|
- name: Cache Static Files
|
||||||
|
id: static-cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
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 python3 manage.py check
|
||||||
|
pipenv run python3 manage.py makemigrations --check --dry-run
|
||||||
|
pipenv run python3 manage.py collectstatic --noinput
|
||||||
|
- name: Run Tests
|
||||||
|
run: pipenv run pytest -n auto --cov
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: failure-screenshots ${{ matrix.test-group }}
|
||||||
|
path: screenshots/
|
||||||
|
retention-days: 5
|
||||||
|
- name: Coveralls
|
||||||
|
run: pipenv run coveralls --service=github
|
||||||
18
.gitignore
vendored
@@ -26,6 +26,7 @@ var/
|
|||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
node_modules/
|
node_modules/
|
||||||
|
data/
|
||||||
|
|
||||||
# Continer extras
|
# Continer extras
|
||||||
.vagrant
|
.vagrant
|
||||||
@@ -68,19 +69,9 @@ target/
|
|||||||
|
|
||||||
## Directory-based project format:
|
## Directory-based project format:
|
||||||
.idea/
|
.idea/
|
||||||
# if you remove the above rule, at least ignore the following:
|
|
||||||
|
|
||||||
# User-specific stuff:
|
#Built dependencies
|
||||||
# .idea/workspace.xml
|
pipeline/built_assets
|
||||||
# .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
|
|
||||||
|
|
||||||
# Gradle:
|
# Gradle:
|
||||||
# .idea/gradle.xml
|
# .idea/gradle.xml
|
||||||
@@ -109,5 +100,4 @@ com_crashlytics_export_strings.xml
|
|||||||
crashlytics.properties
|
crashlytics.properties
|
||||||
crashlytics-build.properties
|
crashlytics-build.properties
|
||||||
.vscode/
|
.vscode/
|
||||||
/package-lock.json
|
screenshots/
|
||||||
screenshots/
|
|
||||||
|
|||||||
1
.idea/.name
generated
@@ -1 +0,0 @@
|
|||||||
PyRIGS
|
|
||||||
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
@@ -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
@@ -1,5 +0,0 @@
|
|||||||
<component name="DependencyValidationManager">
|
|
||||||
<state>
|
|
||||||
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
|
|
||||||
</state>
|
|
||||||
</component>
|
|
||||||
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
@@ -1,7 +1,6 @@
|
|||||||
*.sqlite3
|
*.sqlite3
|
||||||
*.scss
|
|
||||||
*.md
|
*.md
|
||||||
*.rb
|
**/tests
|
||||||
Vagrantfile
|
conftest.py
|
||||||
config/vagrant/*
|
pytest.ini
|
||||||
config/vagrant.yml
|
Dockerfile
|
||||||
|
|||||||
37
.travis.yml
@@ -1,37 +0,0 @@
|
|||||||
language: python
|
|
||||||
python:
|
|
||||||
"3.8"
|
|
||||||
cache: pip
|
|
||||||
|
|
||||||
addons:
|
|
||||||
chrome: stable
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- export LANGUAGE=en_GB.UTF-8
|
|
||||||
|
|
||||||
install:
|
|
||||||
- |
|
|
||||||
latest=$(wget -qO- https://chromedriver.storage.googleapis.com/LATEST_RELEASE)
|
|
||||||
wget https://chromedriver.storage.googleapis.com/$latest/chromedriver_linux64.zip
|
|
||||||
- unzip chromedriver_linux64.zip
|
|
||||||
- export PATH=$PATH:$(pwd)
|
|
||||||
- chmod +x chromedriver
|
|
||||||
- pip install -r requirements.txt
|
|
||||||
- pip install coveralls codeclimate-test-reporter pycodestyle
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- export PATH=$PATH:/usr/lib/chromium-browser/
|
|
||||||
- python manage.py collectstatic --noinput
|
|
||||||
|
|
||||||
script:
|
|
||||||
- pycodestyle . --exclude=migrations,importer*
|
|
||||||
- python manage.py check
|
|
||||||
- python manage.py makemigrations --check --dry-run
|
|
||||||
- coverage run manage.py test --verbosity=2
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- coveralls
|
|
||||||
- codeclimate-test-reporter
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
webhooks: https://fathomless-fjord-24024.herokuapp.com/notify
|
|
||||||
12
Dockerfile
@@ -1,12 +0,0 @@
|
|||||||
FROM python:3.6
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ADD . /app
|
|
||||||
|
|
||||||
RUN pip install -r requirements.txt && \
|
|
||||||
python manage.py collectstatic --noinput
|
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
|
||||||
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"
|
||||||
|
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 = "~=4.0.0"
|
||||||
|
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-widget-tweaks = "~=1.4.8"
|
||||||
|
django-htmlmin = "~=0.11.0"
|
||||||
|
envparse = "*"
|
||||||
|
gunicorn = "~=20.0.4"
|
||||||
|
icalendar = "~=4.0.7"
|
||||||
|
idna = "~=2.10"
|
||||||
|
Markdown = "~=3.3.3"
|
||||||
|
msgpack = "~=1.0.2"
|
||||||
|
pep517 = "~=0.9.1"
|
||||||
|
Pillow = "~=9.3.0"
|
||||||
|
premailer = "~=3.7.0"
|
||||||
|
progress = "~=1.5"
|
||||||
|
psutil = "~=5.8.0"
|
||||||
|
psycopg2 = "~=2.8.6"
|
||||||
|
Pygments = "~=2.7.4"
|
||||||
|
pyparsing = "~=2.4.7"
|
||||||
|
PyPDF2 = "~=1.27.5"
|
||||||
|
PyPOM = "~=2.2.4"
|
||||||
|
python-dateutil = "~=2.8.1"
|
||||||
|
pytoml = "~=0.1.21"
|
||||||
|
pytz = "~=2020.5"
|
||||||
|
reportlab = "*"
|
||||||
|
requests = "~=2.31.0"
|
||||||
|
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.3"
|
||||||
|
urllib3 = "~=1.26.5"
|
||||||
|
whitenoise = "~=5.2.0"
|
||||||
|
yolk = "~=0.4.3"
|
||||||
|
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 = "*"
|
||||||
|
"z3c.rml" = "*"
|
||||||
|
pikepdf = "*"
|
||||||
|
django-queryable-properties = "*"
|
||||||
|
django-mass-edit = "*"
|
||||||
|
selenium = "~=4.9.1"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
pycodestyle = "~=2.9.1"
|
||||||
|
coveralls = "*"
|
||||||
|
django-coverage-plugin = "*"
|
||||||
|
pytest-cov = "*"
|
||||||
|
pytest-django = "*"
|
||||||
|
pluggy = "*"
|
||||||
|
pytest-splinter = "*"
|
||||||
|
pytest = "*"
|
||||||
|
pytest-reverse = "*"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.10"
|
||||||
|
|
||||||
|
[dev-packages.pytest-xdist]
|
||||||
|
extras = [ "psutil",]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
|
[dev-packages.PyPOM]
|
||||||
|
extras = [ "splinter",]
|
||||||
|
version = "*"
|
||||||
2078
Pipfile.lock
generated
Normal file
@@ -1,6 +1,7 @@
|
|||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
from django.shortcuts import render
|
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.shortcuts import render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
@@ -8,18 +9,13 @@ from RIGS import models
|
|||||||
|
|
||||||
def get_oembed(login_url, request, oembed_view, kwargs):
|
def get_oembed(login_url, request, oembed_view, kwargs):
|
||||||
context = {}
|
context = {}
|
||||||
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'],
|
context['oembed_url'] = f"{request.scheme}://{request.META['HTTP_HOST']}{reverse(oembed_view, kwargs=kwargs)}"
|
||||||
reverse(oembed_view, kwargs=kwargs))
|
context['login_url'] = f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}"
|
||||||
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
|
||||||
resp = render(request, 'login_redirect.html', context=context)
|
resp = render(request, 'login_redirect.html', context=context)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
def has_oembed(oembed_view, login_url=None):
|
def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
|
||||||
if not login_url:
|
|
||||||
from django.conf import settings
|
|
||||||
login_url = settings.LOGIN_URL
|
|
||||||
|
|
||||||
def _dec(view_func):
|
def _dec(view_func):
|
||||||
def _checklogin(request, *args, **kwargs):
|
def _checklogin(request, *args, **kwargs):
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
@@ -28,7 +24,7 @@ def has_oembed(oembed_view, login_url=None):
|
|||||||
if oembed_view is not None:
|
if oembed_view is not None:
|
||||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||||
else:
|
else:
|
||||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
|
||||||
|
|
||||||
_checklogin.__doc__ = view_func.__doc__
|
_checklogin.__doc__ = view_func.__doc__
|
||||||
_checklogin.__dict__ = view_func.__dict__
|
_checklogin.__dict__ = view_func.__dict__
|
||||||
@@ -58,7 +54,7 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
|
|||||||
if oembed_view is not None:
|
if oembed_view is not None:
|
||||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||||
else:
|
else:
|
||||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
|
||||||
else:
|
else:
|
||||||
resp = render(request, '403.html')
|
resp = render(request, '403.html')
|
||||||
resp.status_code = 403
|
resp.status_code = 403
|
||||||
|
|||||||
0
PyRIGS/management/__init__.py
Normal file
0
PyRIGS/management/commands/__init__.py
Normal file
@@ -8,25 +8,23 @@ For the full list of settings and their values, see
|
|||||||
https://docs.djangoproject.com/en/1.7/ref/settings/
|
https://docs.djangoproject.com/en/1.7/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
|
||||||
import os
|
|
||||||
import raven
|
|
||||||
import secrets
|
|
||||||
import datetime
|
import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
import secrets
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
import sentry_sdk
|
||||||
|
from sentry_sdk.integrations.django import DjangoIntegration
|
||||||
|
from envparse import env
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
|
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get(
|
SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e')
|
||||||
'SECRET_KEY') else 'gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e'
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# 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
|
DEBUG = env('DEBUG', cast=bool, default=True)
|
||||||
|
STAGING = env('STAGING', cast=bool, default=False)
|
||||||
STAGING = bool(int(os.environ.get('STAGING'))) if os.environ.get('STAGING') else False
|
CI = env('CI', cast=bool, default=False)
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
|
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
|
||||||
|
|
||||||
@@ -44,13 +42,15 @@ if not DEBUG:
|
|||||||
|
|
||||||
INTERNAL_IPS = ['127.0.0.1']
|
INTERNAL_IPS = ['127.0.0.1']
|
||||||
|
|
||||||
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
|
DOMAIN = env('DOMAIN', default='example.com')
|
||||||
('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
|
|
||||||
|
ADMINS = [('IT Manager', f'it@{DOMAIN}'), ('Arona Jones', f'arona.jones@{DOMAIN}')]
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
|
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
|
'whitenoise.runserver_nostatic',
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
@@ -62,19 +62,20 @@ INSTALLED_APPS = (
|
|||||||
'users',
|
'users',
|
||||||
'RIGS',
|
'RIGS',
|
||||||
'assets',
|
'assets',
|
||||||
|
'training',
|
||||||
|
|
||||||
'debug_toolbar',
|
# 'debug_toolbar',
|
||||||
'registration',
|
'registration',
|
||||||
'reversion',
|
'reversion',
|
||||||
'captcha',
|
|
||||||
'widget_tweaks',
|
'widget_tweaks',
|
||||||
'raven.contrib.django.raven_compat',
|
'hcaptcha',
|
||||||
|
'massadmin',
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE = (
|
MIDDLEWARE = (
|
||||||
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||||
|
# 'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||||
'reversion.middleware.RevisionMiddleware',
|
'reversion.middleware.RevisionMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
@@ -82,6 +83,8 @@ MIDDLEWARE = (
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'htmlmin.middleware.HtmlMinifyMiddleware',
|
||||||
|
'htmlmin.middleware.MarkRequestMiddleware',
|
||||||
)
|
)
|
||||||
|
|
||||||
ROOT_URLCONF = 'PyRIGS.urls'
|
ROOT_URLCONF = 'PyRIGS.urls'
|
||||||
@@ -89,11 +92,10 @@ ROOT_URLCONF = 'PyRIGS.urls'
|
|||||||
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
'NAME': str(BASE_DIR / 'db.sqlite3'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,12 +152,33 @@ LOGGING = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RAVEN_CONFIG = {
|
# Tests lock up SQLite otherwise
|
||||||
'dsn': os.environ.get('RAVEN_DSN'),
|
if STAGING or CI:
|
||||||
# If you are using git, you can also automatically configure the
|
CACHES = {
|
||||||
# release based on the git info.
|
'default': {
|
||||||
# 'release': raven.fetch_git_sha(os.path.dirname(os.path.dirname(__file__))),
|
'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',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Error/performance monitoring
|
||||||
|
sentry_sdk.init(
|
||||||
|
dsn=env('SENTRY_DSN', default=""),
|
||||||
|
integrations=[DjangoIntegration()],
|
||||||
|
traces_sample_rate=1.0,
|
||||||
|
)
|
||||||
|
|
||||||
# User system
|
# User system
|
||||||
AUTH_USER_MODEL = 'RIGS.Profile'
|
AUTH_USER_MODEL = 'RIGS.Profile'
|
||||||
@@ -166,26 +189,21 @@ LOGOUT_URL = '/user/logout/'
|
|||||||
|
|
||||||
ACCOUNT_ACTIVATION_DAYS = 7
|
ACCOUNT_ACTIVATION_DAYS = 7
|
||||||
|
|
||||||
# reCAPTCHA settings
|
# CAPTCHA settings
|
||||||
RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY',
|
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001')
|
||||||
"6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key
|
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000')
|
||||||
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY',
|
|
||||||
"6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
|
|
||||||
NOCAPTCHA = True
|
|
||||||
|
|
||||||
SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error']
|
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
EMAILER_TEST = False
|
EMAILER_TEST = False
|
||||||
if not DEBUG or EMAILER_TEST:
|
if not DEBUG or EMAILER_TEST:
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
EMAIL_HOST = os.environ.get('EMAIL_HOST')
|
EMAIL_HOST = env('EMAIL_HOST')
|
||||||
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 25))
|
EMAIL_PORT = env('EMAIL_PORT', cast=int, default=25)
|
||||||
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
|
EMAIL_HOST_USER = env('EMAIL_HOST_USER')
|
||||||
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
|
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
|
||||||
EMAIL_USE_TLS = bool(int(os.environ.get('EMAIL_USE_TLS', 0)))
|
EMAIL_USE_TLS = env('EMAIL_USE_TLS', cast=bool, default=False)
|
||||||
EMAIL_USE_SSL = bool(int(os.environ.get('EMAIL_USE_SSL', 0)))
|
EMAIL_USE_SSL = env('EMAIL_USE_SSL', cast=bool, default=False)
|
||||||
DEFAULT_FROM_EMAIL = os.environ.get('EMAIL_FROM')
|
DEFAULT_FROM_EMAIL = env('EMAIL_FROM')
|
||||||
else:
|
else:
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
|
||||||
@@ -200,8 +218,6 @@ TIME_ZONE = 'Europe/London'
|
|||||||
|
|
||||||
FORMAT_MODULE_PATH = 'PyRIGS.formats'
|
FORMAT_MODULE_PATH = 'PyRIGS.formats'
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_L10N = True
|
USE_L10N = True
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
@@ -210,18 +226,18 @@ USE_TZ = True
|
|||||||
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
|
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
|
STATIC_ROOT = str(BASE_DIR / 'static/')
|
||||||
STATIC_DIRS = (
|
STATICFILES_DIRS = [
|
||||||
os.path.join(BASE_DIR, 'static/')
|
str(BASE_DIR / 'pipeline/built_assets'),
|
||||||
)
|
]
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [
|
'DIRS': [
|
||||||
os.path.join(BASE_DIR, 'templates')
|
BASE_DIR / 'templates'
|
||||||
],
|
],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
@@ -244,10 +260,12 @@ USE_GRAVATAR = True
|
|||||||
|
|
||||||
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
||||||
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
|
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
|
||||||
RISK_ASSESSMENT_URL = os.environ.get('RISK_ASSESSMENT_URL') if os.environ.get(
|
|
||||||
'RISK_ASSESSMENT_URL') else "http://example.com"
|
|
||||||
RISK_ASSESSMENT_SECRET = os.environ.get('RISK_ASSESSMENT_SECRET') if os.environ.get(
|
|
||||||
'RISK_ASSESSMENT_SECRET') else secrets.token_hex(15)
|
|
||||||
|
|
||||||
IMGUR_UPLOAD_CLIENT_ID = os.environ.get('IMGUR_UPLOAD_CLIENT_ID', '')
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||||
IMGUR_UPLOAD_CLIENT_SECRET = os.environ.get('IMGUR_UPLOAD_CLIENT_SECRET', '')
|
|
||||||
|
SECURE_HSTS_SECONDS = 3600
|
||||||
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||||
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
|
SESSION_COOKIE_SECURE = env('SESSION_COOKIE_SECURE_ENABLED', True)
|
||||||
|
CSRF_COOKIE_SECURE = env('CSRF_COOKIE_SECURE_ENABLED', True)
|
||||||
|
SECURE_HSTS_PRELOAD = True
|
||||||
|
|||||||
0
PyRIGS/tests/__init__.py
Normal file
@@ -1,33 +1,30 @@
|
|||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from django.conf import settings
|
||||||
from django.test import LiveServerTestCase
|
from django.test import LiveServerTestCase
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.support.wait import WebDriverWait
|
||||||
|
|
||||||
from RIGS import models as rigsmodels
|
from RIGS import models as rigsmodels
|
||||||
from . import pages
|
from . import pages
|
||||||
import os
|
|
||||||
import pytz
|
from pytest_django.asserts import assertContains
|
||||||
from datetime import date, time, datetime, timedelta
|
|
||||||
from django.conf import settings
|
|
||||||
import imgurpython
|
|
||||||
import PyRIGS.settings
|
|
||||||
import sys
|
|
||||||
import pathlib
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
|
|
||||||
def create_datetime(year, month, day, hour, min):
|
def create_datetime(year, month, day, hour, minute):
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
return tz.localize(datetime(year, month, day, hour, min)).astimezone(pytz.utc)
|
return tz.localize(datetime(year, month, day, hour, minute)).astimezone(tz)
|
||||||
|
|
||||||
|
|
||||||
def create_browser():
|
def create_browser():
|
||||||
options = webdriver.ChromeOptions()
|
options = webdriver.ChromeOptions()
|
||||||
options.add_argument("--window-size=1920,1080")
|
options.add_argument("--window-size=1920,1080")
|
||||||
# No caching, please and thank you
|
options.add_argument("--headless")
|
||||||
options.add_argument("--aggressive-cache-discard")
|
if settings.CI:
|
||||||
options.add_argument("--disk-cache-size=0")
|
|
||||||
# God Save The Queen
|
|
||||||
options.add_argument("--lang=en_GB")
|
|
||||||
if os.environ.get('CI', False):
|
|
||||||
options.add_argument("--headless")
|
|
||||||
options.add_argument("--no-sandbox")
|
options.add_argument("--no-sandbox")
|
||||||
driver = webdriver.Chrome(options=options)
|
driver = webdriver.Chrome(options=options)
|
||||||
return driver
|
return driver
|
||||||
@@ -37,6 +34,7 @@ class BaseTest(LiveServerTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
self.driver = create_browser()
|
self.driver = create_browser()
|
||||||
|
self.wait = WebDriverWait(self.driver, 15)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
@@ -50,10 +48,11 @@ class AutoLoginTest(BaseTest):
|
|||||||
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
|
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
|
||||||
self.profile.set_password("EventTestPassword")
|
self.profile.set_password("EventTestPassword")
|
||||||
self.profile.save()
|
self.profile.save()
|
||||||
loginPage = pages.LoginPage(self.driver, self.live_server_url).open()
|
login_page = pages.LoginPage(self.driver, self.live_server_url).open()
|
||||||
loginPage.login("EventTest", "EventTestPassword")
|
login_page.login("EventTest", "EventTestPassword")
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME Refactor as a pytest fixture
|
||||||
def screenshot_failure(func):
|
def screenshot_failure(func):
|
||||||
def wrapper_func(self, *args, **kwargs):
|
def wrapper_func(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
@@ -64,20 +63,9 @@ def screenshot_failure(func):
|
|||||||
if not pathlib.Path("screenshots").is_dir():
|
if not pathlib.Path("screenshots").is_dir():
|
||||||
os.mkdir("screenshots")
|
os.mkdir("screenshots")
|
||||||
self.driver.save_screenshot(screenshot_file)
|
self.driver.save_screenshot(screenshot_file)
|
||||||
|
print(f"Error in test {screenshot_name} is at path {screenshot_file}", file=sys.stderr)
|
||||||
if settings.IMGUR_UPLOAD_CLIENT_ID != "":
|
|
||||||
config = {
|
|
||||||
'album': None,
|
|
||||||
'name': screenshot_name,
|
|
||||||
'title': screenshot_name,
|
|
||||||
'description': ""
|
|
||||||
}
|
|
||||||
client = imgurpython.ImgurClient(settings.IMGUR_UPLOAD_CLIENT_ID, settings.IMGUR_UPLOAD_CLIENT_SECRET)
|
|
||||||
image = client.upload_from_path(screenshot_file, config=config)
|
|
||||||
print("Error in test {} is at url {}".format(screenshot_name, image['link']), file=sys.stderr)
|
|
||||||
else:
|
|
||||||
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
|
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
return wrapper_func
|
return wrapper_func
|
||||||
|
|
||||||
|
|
||||||
@@ -88,15 +76,30 @@ def screenshot_failure_cls(cls):
|
|||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
# Checks if animation is done
|
def assert_times_almost_equal(first_time, second_time):
|
||||||
class animation_is_finished(object):
|
assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0)
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __call__(self, driver):
|
|
||||||
numberAnimating = driver.execute_script('return $(":animated").length')
|
def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url):
|
||||||
finished = numberAnimating == 0
|
# Test the meta tag is in place
|
||||||
if finished:
|
response = client.get(event_url, follow=True, HTTP_HOST='example.com')
|
||||||
import time
|
assertContains(response, 'application/json+oembed')
|
||||||
time.sleep(0.1)
|
assertContains(response, oembed_url)
|
||||||
return finished
|
# 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
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from pypom import Page, Region
|
from pypom import Page
|
||||||
|
from selenium.common.exceptions import NoSuchElementException
|
||||||
from selenium.webdriver.common.action_chains import ActionChains
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver import Chrome
|
|
||||||
from selenium.common.exceptions import NoSuchElementException
|
|
||||||
from PyRIGS.tests import regions
|
from PyRIGS.tests import regions
|
||||||
|
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ class BasePage(Page):
|
|||||||
|
|
||||||
class FormPage(BasePage):
|
class FormPage(BasePage):
|
||||||
_errors_selector = (By.CLASS_NAME, "alert-danger")
|
_errors_selector = (By.CLASS_NAME, "alert-danger")
|
||||||
|
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
|
||||||
|
|
||||||
def remove_all_required(self):
|
def remove_all_required(self):
|
||||||
self.driver.execute_script(
|
self.driver.execute_script(
|
||||||
@@ -43,6 +44,7 @@ class FormPage(BasePage):
|
|||||||
submit = self.find_element(*self._submit_locator)
|
submit = self.find_element(*self._submit_locator)
|
||||||
ActionChains(self.driver).move_to_element(submit).perform()
|
ActionChains(self.driver).move_to_element(submit).perform()
|
||||||
submit.click()
|
submit.click()
|
||||||
|
self.wait.until(animation_is_finished())
|
||||||
self.wait.until(lambda x: self.errors != previous_errors or self.success)
|
self.wait.until(lambda x: self.errors != previous_errors or self.success)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -72,3 +74,13 @@ class LoginPage(BasePage):
|
|||||||
password_element.send_keys(password)
|
password_element.send_keys(password)
|
||||||
|
|
||||||
self.find_element(*self._submit_locator).click()
|
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
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
from pypom import Region
|
|
||||||
from django.utils import timezone
|
|
||||||
from selenium.webdriver.common.by import By
|
|
||||||
from selenium.webdriver.support import expected_conditions
|
|
||||||
from selenium.webdriver.remote.webelement import WebElement
|
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
|
||||||
from selenium.webdriver.support.select import Select
|
|
||||||
from selenium.webdriver.common.keys import Keys
|
|
||||||
import datetime
|
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):
|
def parse_bool_from_string(string):
|
||||||
# Used to convert from attribute strings to boolean values, written after I found this:
|
# Used to convert from attribute strings to boolean values, written after I found this:
|
||||||
@@ -18,18 +18,22 @@ def parse_bool_from_string(string):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 12-Hour vs 24-Hour Time. Affects widget display
|
|
||||||
|
|
||||||
|
|
||||||
def get_time_format():
|
def get_time_format():
|
||||||
# Default
|
# Default
|
||||||
time_format = "%H:%M"
|
time_format = "%H%M"
|
||||||
# If system is 12hr
|
if settings.CI: # The CI is American
|
||||||
if timezone.now().strftime("%p"):
|
time_format = "%I%M%p"
|
||||||
time_format = "%I:%M %p"
|
|
||||||
return time_format
|
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):
|
class BootstrapSelectElement(Region):
|
||||||
_main_button_locator = (By.CSS_SELECTOR, 'button.dropdown-toggle')
|
_main_button_locator = (By.CSS_SELECTOR, 'button.dropdown-toggle')
|
||||||
_option_box_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu')
|
_option_box_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu')
|
||||||
@@ -67,11 +71,11 @@ class BootstrapSelectElement(Region):
|
|||||||
self.find_element(*self._deselect_all_locator).click()
|
self.find_element(*self._deselect_all_locator).click()
|
||||||
|
|
||||||
def search(self, query):
|
def search(self, query):
|
||||||
|
# self.wait.until(expected_conditions.visibility_of_element_located(self._status_locator))
|
||||||
search_box = self.find_element(*self._search_locator)
|
search_box = self.find_element(*self._search_locator)
|
||||||
self.open()
|
self.open()
|
||||||
search_box.clear()
|
search_box.clear()
|
||||||
search_box.send_keys(query)
|
search_box.send_keys(query)
|
||||||
status_text = self.find_element(*self._status_locator)
|
|
||||||
self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator))
|
self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -80,7 +84,7 @@ class BootstrapSelectElement(Region):
|
|||||||
return [self.BootstrapSelectOption(self, i) for i in options]
|
return [self.BootstrapSelectOption(self, i) for i in options]
|
||||||
|
|
||||||
def set_option(self, name, selected):
|
def set_option(self, name, selected):
|
||||||
options = list((x for x in self.options if x.name == name))
|
options = [x for x in self.options if x.name == name]
|
||||||
assert len(options) == 1
|
assert len(options) == 1
|
||||||
options[0].set_selected(selected)
|
options[0].set_selected(selected)
|
||||||
|
|
||||||
@@ -113,6 +117,15 @@ class TextBox(Region):
|
|||||||
self.root.send_keys(value)
|
self.root.send_keys(value)
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleMDETextArea(Region):
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self.driver.execute_script("return document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().getValue();", self.root.get_attribute("id"))
|
||||||
|
|
||||||
|
def set_value(self, value):
|
||||||
|
self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
|
||||||
|
|
||||||
|
|
||||||
class CheckBox(Region):
|
class CheckBox(Region):
|
||||||
def toggle(self):
|
def toggle(self):
|
||||||
self.root.click()
|
self.root.click()
|
||||||
@@ -126,6 +139,22 @@ class CheckBox(Region):
|
|||||||
self.toggle()
|
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, f"//label[@for='{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):
|
class DatePicker(Region):
|
||||||
@property
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
@@ -133,13 +162,13 @@ class DatePicker(Region):
|
|||||||
|
|
||||||
def set_value(self, value):
|
def set_value(self, value):
|
||||||
self.root.clear()
|
self.root.clear()
|
||||||
self.root.send_keys(value.strftime("%d%m%Y"))
|
self.root.send_keys(value.strftime(get_date_format()))
|
||||||
|
|
||||||
|
|
||||||
class TimePicker(Region):
|
class TimePicker(Region):
|
||||||
@property
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
return datetime.datetime.strptime(self.root.get_attribute("value"), get_time_format())
|
return datetime.datetime.strptime(self.root.get_attribute("value"), "%H:%M")
|
||||||
|
|
||||||
def set_value(self, value):
|
def set_value(self, value):
|
||||||
self.root.clear()
|
self.root.clear()
|
||||||
@@ -149,12 +178,12 @@ class TimePicker(Region):
|
|||||||
class DateTimePicker(Region):
|
class DateTimePicker(Region):
|
||||||
@property
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d " + get_time_format())
|
return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
def set_value(self, value):
|
def set_value(self, value):
|
||||||
self.root.clear()
|
self.root.clear()
|
||||||
|
|
||||||
date = value.date().strftime("%d%m%Y")
|
date = value.date().strftime(get_date_format())
|
||||||
time = value.time().strftime(get_time_format())
|
time = value.time().strftime(get_time_format())
|
||||||
|
|
||||||
self.root.send_keys(date)
|
self.root.send_keys(date)
|
||||||
@@ -199,7 +228,7 @@ class ErrorPage(Region):
|
|||||||
|
|
||||||
class Modal(Region):
|
class Modal(Region):
|
||||||
_submit_locator = (By.CSS_SELECTOR, '.btn-primary')
|
_submit_locator = (By.CSS_SELECTOR, '.btn-primary')
|
||||||
_header_selector = (By.TAG_NAME, 'h3')
|
_header_selector = (By.TAG_NAME, 'h4')
|
||||||
|
|
||||||
form_items = {
|
form_items = {
|
||||||
'name': (TextBox, (By.ID, 'id_name'))
|
'name': (TextBox, (By.ID, 'id_name'))
|
||||||
|
|||||||
146
PyRIGS/tests/test_unit.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
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 training.tests.test_unit import get_response
|
||||||
|
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 not Asset.objects.all().exists()
|
||||||
|
assert not Event.objects.all().exists()
|
||||||
|
|
||||||
|
|
||||||
|
@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 = f"{reverse('login_embed')}?next={request_url}"
|
||||||
|
else:
|
||||||
|
expected_url = f"{reverse('login')}?next={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')
|
||||||
|
|
||||||
|
|
||||||
|
def test_search(admin_client, admin_user):
|
||||||
|
url = reverse('search')
|
||||||
|
response = admin_client.get(url, {'q': "Definetelynothingfoundifwesearchthis"})
|
||||||
|
assertContains(response, "No results found")
|
||||||
|
response = admin_client.get(url, {'q': admin_user.first_name})
|
||||||
|
assertContains(response, admin_user.first_name)
|
||||||
@@ -1,21 +1,20 @@
|
|||||||
from django.urls import path
|
|
||||||
from django.conf.urls import include, url
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
from django.conf.urls import include
|
||||||
from django.contrib.auth.views import LoginView
|
from django.contrib import admin
|
||||||
from registration.backends.default.views import RegistrationView
|
from django.contrib.auth.decorators import login_required
|
||||||
from PyRIGS.decorators import permission_required_with_403
|
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||||
import RIGS
|
from django.urls import path
|
||||||
import users
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from PyRIGS import views
|
from PyRIGS import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include('users.urls')),
|
path('', include('versioning.urls')),
|
||||||
path('', include('RIGS.urls')),
|
path('', include('RIGS.urls')),
|
||||||
path('assets/', include('assets.urls')),
|
path('assets/', include('assets.urls')),
|
||||||
|
path('training/', include('training.urls')),
|
||||||
|
|
||||||
|
path('', login_required(views.Index.as_view()), name='index'),
|
||||||
|
|
||||||
# API
|
# API
|
||||||
path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()),
|
path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()),
|
||||||
@@ -23,13 +22,22 @@ urlpatterns = [
|
|||||||
path('api/<str:model>/<int:pk>/', login_required(views.SecureAPIRequest.as_view()),
|
path('api/<str:model>/<int:pk>/', login_required(views.SecureAPIRequest.as_view()),
|
||||||
name="api_secure"),
|
name="api_secure"),
|
||||||
|
|
||||||
|
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
|
||||||
|
path('search/', login_required(views.Search.as_view()), name='search'),
|
||||||
|
path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'),
|
||||||
|
|
||||||
|
path('', include('users.urls')),
|
||||||
|
|
||||||
|
path('admin/', include('massadmin.urls')),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += staticfiles_urlpatterns()
|
urlpatterns += staticfiles_urlpatterns()
|
||||||
|
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
urlpatterns = [
|
urlpatterns += [
|
||||||
url(r'^__debug__/', include(debug_toolbar.urls)),
|
path('__debug__/', include(debug_toolbar.urls)),
|
||||||
] + urlpatterns
|
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
|
||||||
|
]
|
||||||
|
|||||||
300
PyRIGS/views.py
@@ -1,24 +1,55 @@
|
|||||||
|
import datetime
|
||||||
|
import operator
|
||||||
|
import re
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from functools import reduce
|
||||||
|
from itertools import chain
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from PyPDF2 import PdfFileMerger, PdfFileReader
|
||||||
|
from z3c.rml import rml2pdf
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.core import serializers
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http.response import HttpResponseRedirect
|
from django.db.models import Q
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.contrib.auth.views import LoginView
|
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||||
from django.db.models import Q
|
from django.template.loader import get_template
|
||||||
from django.shortcuts import get_object_or_404
|
from django.utils import timezone
|
||||||
from django.core import serializers
|
|
||||||
from django.conf import settings
|
|
||||||
import simplejson
|
|
||||||
from django.contrib import messages
|
|
||||||
import datetime
|
|
||||||
import pytz
|
|
||||||
import operator
|
|
||||||
from registration.views import RegistrationView
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
|
|
||||||
from RIGS import models, forms
|
from RIGS import models
|
||||||
from assets import models as asset_models
|
from assets import models as asset_models
|
||||||
from functools import reduce
|
from training import models as training_models
|
||||||
|
|
||||||
|
|
||||||
|
def is_ajax(request):
|
||||||
|
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
|
||||||
|
|
||||||
|
|
||||||
|
def get_related(form, context): # Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
||||||
|
for field, model in form.related_models.items():
|
||||||
|
value = form[field].value()
|
||||||
|
if value is not None and value != '':
|
||||||
|
context[field] = model.objects.get(pk=value)
|
||||||
|
|
||||||
|
|
||||||
|
class Index(generic.TemplateView): # Displays the current rig count along with a few other bits and pieces
|
||||||
|
template_name = 'index.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['rig_count'] = models.Event.objects.rig_count()
|
||||||
|
context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now()).exclude(dry_hire=True).exclude(status=models.Event.CANCELLED)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class SecureAPIRequest(generic.View):
|
class SecureAPIRequest(generic.View):
|
||||||
@@ -28,7 +59,9 @@ class SecureAPIRequest(generic.View):
|
|||||||
'organisation': models.Organisation,
|
'organisation': models.Organisation,
|
||||||
'profile': models.Profile,
|
'profile': models.Profile,
|
||||||
'event': models.Event,
|
'event': models.Event,
|
||||||
'supplier': asset_models.Supplier
|
'asset': asset_models.Asset,
|
||||||
|
'supplier': asset_models.Supplier,
|
||||||
|
'training_item': training_models.TrainingItem,
|
||||||
}
|
}
|
||||||
|
|
||||||
perms = {
|
perms = {
|
||||||
@@ -37,7 +70,9 @@ class SecureAPIRequest(generic.View):
|
|||||||
'organisation': 'RIGS.view_organisation',
|
'organisation': 'RIGS.view_organisation',
|
||||||
'profile': 'RIGS.view_profile',
|
'profile': 'RIGS.view_profile',
|
||||||
'event': None,
|
'event': None,
|
||||||
'supplier': None
|
'asset': None,
|
||||||
|
'supplier': None,
|
||||||
|
'training_item': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@@ -65,6 +100,9 @@ class SecureAPIRequest(generic.View):
|
|||||||
fields = request.GET.get('fields', None)
|
fields = request.GET.get('fields', None)
|
||||||
if fields:
|
if fields:
|
||||||
fields = fields.split(",")
|
fields = fields.split(",")
|
||||||
|
filters = request.GET.get('filters', [])
|
||||||
|
if filters:
|
||||||
|
filters = filters.split(",")
|
||||||
|
|
||||||
# Supply data for one record
|
# Supply data for one record
|
||||||
if pk:
|
if pk:
|
||||||
@@ -85,27 +123,35 @@ class SecureAPIRequest(generic.View):
|
|||||||
for field in fields:
|
for field in fields:
|
||||||
q = Q(**{field + "__icontains": part})
|
q = Q(**{field + "__icontains": part})
|
||||||
qs.append(q)
|
qs.append(q)
|
||||||
|
|
||||||
queries.append(reduce(operator.or_, qs))
|
queries.append(reduce(operator.or_, qs))
|
||||||
|
|
||||||
|
for f in filters:
|
||||||
|
q = Q(**{f: True})
|
||||||
|
queries.append(q)
|
||||||
|
|
||||||
# Build the data response list
|
# Build the data response list
|
||||||
results = []
|
results = []
|
||||||
query = reduce(operator.and_, queries)
|
query = reduce(operator.and_, queries)
|
||||||
objects = self.models[model].objects.filter(query)
|
objects = self.models[model].objects.filter(query)
|
||||||
|
# Returning unactivated or unapproved users when they are elsewhere filtered out of the default queryset leads to some *very* unexpected results
|
||||||
|
if model == "profile":
|
||||||
|
objects = objects.filter(is_active=True, is_approved=True)
|
||||||
for o in objects:
|
for o in objects:
|
||||||
|
name = o.display_name if hasattr(o, 'display_name') else o.name
|
||||||
data = {
|
data = {
|
||||||
'pk': o.pk,
|
'pk': o.pk,
|
||||||
'value': o.pk,
|
'value': o.pk,
|
||||||
'text': o.name,
|
'text': name,
|
||||||
}
|
}
|
||||||
try: # See if there is a valid update URL
|
try: # See if there is a valid update URL
|
||||||
data['update'] = reverse("%s_update" % model, kwargs={'pk': o.pk})
|
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
pass
|
pass
|
||||||
results.append(data)
|
results.append(data)
|
||||||
|
|
||||||
# return a data response
|
# return a data response
|
||||||
json = simplejson.dumps(results)
|
return JsonResponse(results, safe=False)
|
||||||
return HttpResponse(json, content_type="application/json") # Always json
|
|
||||||
|
|
||||||
start = request.GET.get('start', None)
|
start = request.GET.get('start', None)
|
||||||
end = request.GET.get('end', None)
|
end = request.GET.get('end', None)
|
||||||
@@ -130,32 +176,210 @@ class SecureAPIRequest(generic.View):
|
|||||||
}
|
}
|
||||||
|
|
||||||
results.append(data)
|
results.append(data)
|
||||||
json = simplejson.dumps(results)
|
return JsonResponse(results, safe=False)
|
||||||
return HttpResponse(json, content_type="application/json") # Always json
|
|
||||||
|
|
||||||
return HttpResponse(model)
|
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, f"modalobject[0]['update_url']='{update_url}'")
|
||||||
|
else:
|
||||||
|
url = reverse_lazy(detail, kwargs={
|
||||||
|
'pk': self.object.pk,
|
||||||
|
})
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
class GenericListView(generic.ListView):
|
class GenericListView(generic.ListView):
|
||||||
|
template_name = 'generic_list.html'
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = self.model.__name__ + "s"
|
||||||
|
if is_ajax(self.request):
|
||||||
|
context['override'] = "base_ajax.html"
|
||||||
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
q = self.request.GET.get('q', "")
|
object_list = self.model.objects.search(query=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")
|
orderBy = self.request.GET.get('orderBy', "name")
|
||||||
if orderBy != "":
|
if orderBy != "":
|
||||||
object_list = object_list.order_by(orderBy)
|
object_list = object_list.order_by(orderBy)
|
||||||
return object_list
|
return object_list
|
||||||
|
|
||||||
|
|
||||||
|
class GenericDetailView(generic.DetailView):
|
||||||
|
template_name = "generic_detail.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = f"{self.model.__name__} | {self.object.name}"
|
||||||
|
if is_ajax(self.request):
|
||||||
|
context['override'] = "base_ajax.html"
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class GenericUpdateView(generic.UpdateView):
|
||||||
|
template_name = "generic_form.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = f"Edit {self.model.__name__}"
|
||||||
|
if is_ajax(self.request):
|
||||||
|
context['override'] = "base_ajax.html"
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class GenericCreateView(generic.CreateView):
|
||||||
|
template_name = "generic_form.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = f"Create {self.model.__name__}"
|
||||||
|
if is_ajax(self.request):
|
||||||
|
context['override'] = "base_ajax.html"
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class Search(generic.ListView):
|
||||||
|
template_name = 'search_results.html'
|
||||||
|
paginate_by = 20
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
context['count'] = self.count or 0
|
||||||
|
context['query'] = self.request.GET.get('q')
|
||||||
|
context['page_title'] = f"{context['count']} search results for <b>{context['query']}</b>"
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
request = self.request
|
||||||
|
query = request.GET.get('q', None)
|
||||||
|
|
||||||
|
if query is not None:
|
||||||
|
event_results = models.Event.objects.search(query)
|
||||||
|
person_results = models.Person.objects.search(query)
|
||||||
|
organisation_results = models.Organisation.objects.search(query)
|
||||||
|
venue_results = models.Venue.objects.search(query)
|
||||||
|
invoice_results = models.Invoice.objects.search(query)
|
||||||
|
asset_results = asset_models.Asset.objects.search(query)
|
||||||
|
supplier_results = asset_models.Supplier.objects.search(query)
|
||||||
|
trainee_results = training_models.Trainee.objects.search(query)
|
||||||
|
training_item_results = training_models.TrainingItem.objects.search(query)
|
||||||
|
|
||||||
|
# combine querysets
|
||||||
|
queryset_chain = chain(
|
||||||
|
event_results,
|
||||||
|
person_results,
|
||||||
|
organisation_results,
|
||||||
|
venue_results,
|
||||||
|
invoice_results,
|
||||||
|
asset_results,
|
||||||
|
supplier_results,
|
||||||
|
trainee_results,
|
||||||
|
training_item_results,
|
||||||
|
)
|
||||||
|
qs = sorted(queryset_chain,
|
||||||
|
key=lambda instance: instance.pk,
|
||||||
|
reverse=True)
|
||||||
|
self.count = len(qs) # since qs is actually a list
|
||||||
|
return qs
|
||||||
|
return models.Event.objects.none() # just an empty queryset as default
|
||||||
|
|
||||||
|
|
||||||
|
class SearchHelp(generic.TemplateView):
|
||||||
|
template_name = 'search_help.html'
|
||||||
|
|
||||||
|
|
||||||
|
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 = f"{request.scheme}://{request.META['HTTP_HOST']}{embed_url}"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'html': f'<iframe src="{full_url}" frameborder="0" width="100%" height="250"></iframe>',
|
||||||
|
'version': '1.0',
|
||||||
|
'type': 'rich',
|
||||||
|
'height': '250'
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonResponse(data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_info_string(user):
|
||||||
|
user_str = f"by {user.name} " if user else ""
|
||||||
|
time = timezone.now().strftime('%d/%m/%Y %H:%I')
|
||||||
|
return f"[Paperwork generated {user_str}on {time}"
|
||||||
|
|
||||||
|
|
||||||
|
def render_pdf_response(template, context, append_terms):
|
||||||
|
merger = PdfFileMerger()
|
||||||
|
rml = template.render(context)
|
||||||
|
buffer = rml2pdf.parseString(rml)
|
||||||
|
merger.append(PdfFileReader(buffer))
|
||||||
|
buffer.close()
|
||||||
|
|
||||||
|
if append_terms:
|
||||||
|
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
|
||||||
|
merger.append(BytesIO(terms.read()))
|
||||||
|
|
||||||
|
merged = BytesIO()
|
||||||
|
merger.write(merged)
|
||||||
|
|
||||||
|
response = HttpResponse(content_type='application/pdf')
|
||||||
|
f = context['filename']
|
||||||
|
response['Content-Disposition'] = f'filename="{f}"'
|
||||||
|
response.write(merged.getvalue())
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class PrintView(generic.View):
|
||||||
|
append_terms = False
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
obj = get_object_or_404(self.model, pk=self.kwargs['pk'])
|
||||||
|
object_name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', obj.name)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'object': obj,
|
||||||
|
'current_user': self.request.user,
|
||||||
|
'object_name': object_name,
|
||||||
|
'info_string': get_info_string(self.request.user) + f"- {obj.current_version_id}]",
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
return render_pdf_response(get_template(self.template_name), self.get_context_data(), self.append_terms)
|
||||||
|
|
||||||
|
|
||||||
|
class PrintListView(generic.ListView):
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
context['current_user'] = self.request.user
|
||||||
|
context['info_string'] = get_info_string(self.request.user) + "]"
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
self.object_list = self.get_queryset()
|
||||||
|
return render_pdf_response(get_template(self.template_name), self.get_context_data(), False)
|
||||||
|
|||||||
114
README.md
@@ -1,111 +1,19 @@
|
|||||||
# TEC PA & Lighting - PyRIGS #
|
# TEC PA & Lighting - PyRIGS #
|
||||||
[](https://travis-ci.org/nottinghamtec/PyRIGS)
|

|
||||||
[](https://coveralls.io/github/nottinghamtec/PyRIGS)
|
[](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)
|
||||||
|
|
||||||
### What is this repository for? ###
|
# Apps
|
||||||
When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production.
|
- PyRIGS: Base app, stores 'global' information
|
||||||
|
- RIGS: Rigboard stuff - event calendar etc
|
||||||
|
- assets: Database of our kit, testing data etc
|
||||||
|
- training: Logs in-house training within various "departments" (sound, lighting etc).
|
||||||
|
- versioning: Our custom logic built on top of django-reversion. Semi-modular.
|
||||||
|
- users: Our custom logic for registration and profiles. Semi-modular.
|
||||||
|
|
||||||
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. Orginally written with the C implementation of Python 2 (CPython 2, specifically the Python 2.7 standard), the application now runs in Python 3.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Development using docker
|
|
||||||
|
|
||||||
```
|
|
||||||
docker build . -t pyrigs
|
|
||||||
docker run -it --rm -p=8000:8000 -v $(pwd):/app pyrigs
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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 |
|
|
||||||
|
|
||||||
### Testing ###
|
|
||||||
Tests are contained in 3 files. `RIGS/test_models.py` contains tests for logic within the data models. `RIGS/test_unit.py` contains "Live server" tests, using raw web requests. `RIGS/test_integration.py` contains user interface tests which take control of a web browser. For automated Travis tests, we use [Sauce Labs](https://saucelabs.com). When debugging locally, ensure that you have the latest version of Google Chrome installed, then install [chromedriver](https://sites.google.com/a/chromium.org/chromedriver/) and ensure it is on the `PATH`.
|
|
||||||
|
|
||||||
You can run the entire test suite, or you can run specific sections individually. For example, in order of specificity:
|
|
||||||
|
|
||||||
```
|
|
||||||
python manage.py test
|
|
||||||
python manage.py test RIGS.test_models
|
|
||||||
python manage.py test RIGS.test_models.EventTestCase
|
|
||||||
python manage.py test RIGS.test_models.EventTestCase.test_current_events
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
[](https://forthebadge.com) [](https://forthebadge.com)
|
[](https://forthebadge.com) [](https://forthebadge.com)
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
default_app_config = 'RIGS.apps.RIGSAppConfig'
|
|
||||||
|
|||||||
198
RIGS/admin.py
@@ -1,69 +1,120 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from RIGS import models, forms
|
|
||||||
from users import forms as user_forms
|
|
||||||
from django.contrib.auth.admin import UserAdmin
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from reversion.admin import VersionAdmin
|
|
||||||
|
|
||||||
from django.contrib.admin import helpers
|
|
||||||
from django.template.response import TemplateResponse
|
|
||||||
from django.contrib import messages
|
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.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.db import IntegrityError
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
|
from reversion.admin import VersionAdmin
|
||||||
|
|
||||||
|
from RIGS import models
|
||||||
|
from users import forms as user_forms
|
||||||
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
admin.site.register(models.VatRate, VersionAdmin)
|
admin.site.register(models.VatRate, VersionAdmin)
|
||||||
admin.site.register(models.Event, VersionAdmin)
|
admin.site.register(models.Event, VersionAdmin)
|
||||||
admin.site.register(models.EventItem, VersionAdmin)
|
admin.site.register(models.EventItem, VersionAdmin)
|
||||||
admin.site.register(models.Invoice)
|
admin.site.register(models.Invoice, VersionAdmin)
|
||||||
admin.site.register(models.Payment)
|
admin.site.register(models.EventCheckIn)
|
||||||
|
|
||||||
|
|
||||||
def approve_user(modeladmin, request, queryset):
|
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
|
||||||
queryset.update(is_approved=True)
|
def merge_model_instances(primary_object, alias_objects):
|
||||||
|
"""
|
||||||
|
Merge several model instances into one, the `primary_object`.
|
||||||
|
Use this function to merge model objects and migrate all of the related
|
||||||
|
fields from the alias objects the primary object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# get related fields
|
||||||
|
related_fields = list(filter(
|
||||||
|
lambda x: x.is_relation is True,
|
||||||
|
primary_object._meta.get_fields()))
|
||||||
|
|
||||||
approve_user.short_description = "Approve selected users"
|
many_to_many_fields = list(filter(
|
||||||
|
lambda x: x.many_to_many is True, related_fields))
|
||||||
|
|
||||||
|
related_fields = list(filter(
|
||||||
|
lambda x: x.many_to_many is False, related_fields))
|
||||||
|
|
||||||
@admin.register(models.Profile)
|
# Loop through all alias objects and migrate their references to the
|
||||||
class ProfileAdmin(UserAdmin):
|
# primary object
|
||||||
# Don't know how to add 'is_approved' whilst preserving the default list...
|
deleted_objects = []
|
||||||
list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
|
deleted_objects_count = 0
|
||||||
fieldsets = (
|
for alias_object in alias_objects:
|
||||||
(None, {'fields': ('username', 'password')}),
|
# Migrate all foreign key references from alias object to primary
|
||||||
(_('Personal info'), {
|
# object.
|
||||||
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
|
for many_to_many_field in many_to_many_fields:
|
||||||
(_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
|
alias_varname = many_to_many_field.name
|
||||||
'groups', 'user_permissions')}),
|
related_objects = getattr(alias_object, alias_varname)
|
||||||
(_('Important dates'), {
|
for obj in related_objects.all():
|
||||||
'fields': ('last_login', 'date_joined')}),
|
try:
|
||||||
)
|
# Handle regular M2M relationships.
|
||||||
add_fieldsets = (
|
getattr(alias_object, alias_varname).remove(obj)
|
||||||
(None, {
|
getattr(primary_object, alias_varname).add(obj)
|
||||||
'classes': ('wide',),
|
except AttributeError:
|
||||||
'fields': ('username', 'password1', 'password2'),
|
# Handle M2M relationships with a 'through' model.
|
||||||
}),
|
# This does not delete the 'through model.
|
||||||
)
|
# TODO: Allow the user to delete a duplicate 'through' model.
|
||||||
form = user_forms.ProfileChangeForm
|
through_model = getattr(alias_object, alias_varname).through
|
||||||
add_form = user_forms.ProfileCreationForm
|
kwargs = {
|
||||||
actions = [approve_user]
|
many_to_many_field.m2m_reverse_field_name(): obj,
|
||||||
|
many_to_many_field.m2m_field_name(): alias_object,
|
||||||
|
}
|
||||||
|
through_model_instances = through_model.objects.filter(**kwargs)
|
||||||
|
for instance in through_model_instances:
|
||||||
|
# Re-attach the through model to the primary_object
|
||||||
|
setattr(
|
||||||
|
instance,
|
||||||
|
many_to_many_field.m2m_field_name(),
|
||||||
|
primary_object)
|
||||||
|
instance.save()
|
||||||
|
# TODO: Here, try to delete duplicate instances that are
|
||||||
|
# disallowed by a unique_together constraint
|
||||||
|
|
||||||
|
for related_field in related_fields:
|
||||||
|
if related_field.one_to_many:
|
||||||
|
with transaction.atomic():
|
||||||
|
try:
|
||||||
|
alias_varname = related_field.get_accessor_name()
|
||||||
|
related_objects = getattr(alias_object, alias_varname)
|
||||||
|
for obj in related_objects.all():
|
||||||
|
field_name = related_field.field.name
|
||||||
|
setattr(obj, field_name, primary_object)
|
||||||
|
obj.save()
|
||||||
|
except IntegrityError:
|
||||||
|
pass # Skip to avoid integrity error from unique_together
|
||||||
|
elif related_field.one_to_one or related_field.many_to_one:
|
||||||
|
alias_varname = related_field.name
|
||||||
|
if hasattr(alias_object, alias_varname):
|
||||||
|
related_object = getattr(alias_object, alias_varname)
|
||||||
|
primary_related_object = getattr(primary_object, alias_varname)
|
||||||
|
if primary_related_object is None:
|
||||||
|
setattr(primary_object, alias_varname, related_object)
|
||||||
|
primary_object.save()
|
||||||
|
elif related_field.one_to_one:
|
||||||
|
related_object.delete()
|
||||||
|
|
||||||
|
if alias_object.id:
|
||||||
|
deleted_objects += [alias_object]
|
||||||
|
alias_object.delete()
|
||||||
|
deleted_objects_count += 1
|
||||||
|
|
||||||
|
return primary_object, deleted_objects, deleted_objects_count
|
||||||
|
|
||||||
|
|
||||||
class AssociateAdmin(VersionAdmin):
|
class AssociateAdmin(VersionAdmin):
|
||||||
list_display = ('id', 'name', 'number_of_events')
|
|
||||||
search_fields = ['id', 'name']
|
search_fields = ['id', 'name']
|
||||||
list_display_links = ['id', 'name']
|
list_display_links = ['id', 'name']
|
||||||
actions = ['merge']
|
actions = ['merge']
|
||||||
|
|
||||||
merge_fields = ['name']
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return super(AssociateAdmin, self).get_queryset(request).annotate(event_count=Count('event'))
|
return super().get_queryset(request).annotate(event_count=Count('event'))
|
||||||
|
|
||||||
def number_of_events(self, obj):
|
def number_of_events(self, obj):
|
||||||
return obj.latest_events.count()
|
return obj.latest_events.count()
|
||||||
@@ -73,24 +124,16 @@ class AssociateAdmin(VersionAdmin):
|
|||||||
def merge(self, request, queryset):
|
def merge(self, request, queryset):
|
||||||
if request.POST.get('post'): # Has the user confirmed which is the master record?
|
if request.POST.get('post'): # Has the user confirmed which is the master record?
|
||||||
try:
|
try:
|
||||||
masterObjectPk = request.POST.get('master')
|
master_object_pk = request.POST.get('master')
|
||||||
masterObject = queryset.get(pk=masterObjectPk)
|
master_object = queryset.get(pk=master_object_pk)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
|
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
|
||||||
return
|
return
|
||||||
|
|
||||||
with transaction.atomic(), reversion.create_revision():
|
primary_object, deleted_objects, deleted_objects_count = merge_model_instances(master_object, queryset.exclude(pk=master_object_pk).all())
|
||||||
for obj in queryset.exclude(pk=masterObjectPk):
|
reversion.set_comment('Merging Objects')
|
||||||
events = obj.event_set.all()
|
self.message_user(request, f"Objects successfully merged. {deleted_objects_count} old objects deleted.")
|
||||||
for event in events:
|
|
||||||
masterObject.event_set.add(event)
|
|
||||||
obj.delete()
|
|
||||||
reversion.set_comment('Merging Objects')
|
|
||||||
|
|
||||||
self.message_user(request, "Objects successfully merged.")
|
|
||||||
return
|
|
||||||
else: # Present the confirmation screen
|
else: # Present the confirmation screen
|
||||||
|
|
||||||
class TempForm(ModelForm):
|
class TempForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = queryset.model
|
model = queryset.model
|
||||||
@@ -109,6 +152,36 @@ class AssociateAdmin(VersionAdmin):
|
|||||||
return TemplateResponse(request, 'admin_associate_merge.html', context)
|
return TemplateResponse(request, 'admin_associate_merge.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Profile)
|
||||||
|
class ProfileAdmin(UserAdmin, AssociateAdmin):
|
||||||
|
list_display = ('username', 'name', 'is_approved', 'is_superuser', 'is_supervisor', 'number_of_events', 'last_login')
|
||||||
|
list_display_links = ['username']
|
||||||
|
list_filter = UserAdmin.list_filter + ('is_approved',)
|
||||||
|
fieldsets = (
|
||||||
|
(None, {'fields': ('username', 'password')}),
|
||||||
|
(_('Personal info'), {
|
||||||
|
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
|
||||||
|
(_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
|
||||||
|
'groups', 'user_permissions')}),
|
||||||
|
(_('Important dates'), {
|
||||||
|
'fields': ('last_login', 'date_joined')}),
|
||||||
|
)
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('username', 'password1', 'password2'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
form = user_forms.ProfileChangeForm
|
||||||
|
add_form = user_forms.ProfileCreationForm
|
||||||
|
actions = ['approve_user', 'merge']
|
||||||
|
|
||||||
|
merge_fields = ['username', 'first_name', 'last_name', 'initials', 'email', 'phone', 'is_supervisor']
|
||||||
|
|
||||||
|
def approve_user(modeladmin, request, queryset):
|
||||||
|
queryset.update(is_approved=True)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Person)
|
@admin.register(models.Person)
|
||||||
class PersonAdmin(AssociateAdmin):
|
class PersonAdmin(AssociateAdmin):
|
||||||
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
|
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
|
||||||
@@ -125,3 +198,18 @@ class VenueAdmin(AssociateAdmin):
|
|||||||
class OrganisationAdmin(AssociateAdmin):
|
class OrganisationAdmin(AssociateAdmin):
|
||||||
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
|
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
|
||||||
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account']
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.PowerTestRecord)
|
||||||
|
class EventChecklistAdmin(VersionAdmin):
|
||||||
|
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
|
||||||
|
|||||||
118
RIGS/forms.py
@@ -1,15 +1,14 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import simplejson
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils import formats
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
from django.utils import timezone
|
||||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
from reversion import revisions as reversion
|
||||||
from registration.forms import RegistrationFormUniqueEmail
|
|
||||||
from django.contrib.auth.forms import AuthenticationForm
|
|
||||||
from captcha.fields import ReCaptchaField
|
|
||||||
import simplejson
|
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
|
from training.models import TrainingLevel
|
||||||
|
|
||||||
# Override the django form defaults to use the HTML date/time/datetime UI elements
|
# Override the django form defaults to use the HTML date/time/datetime UI elements
|
||||||
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
|
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
|
||||||
@@ -18,8 +17,6 @@ forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'
|
|||||||
|
|
||||||
|
|
||||||
# Events Shit
|
# Events Shit
|
||||||
|
|
||||||
|
|
||||||
class EventForm(forms.ModelForm):
|
class EventForm(forms.ModelForm):
|
||||||
datetime_input_formats = list(settings.DATETIME_INPUT_FORMATS)
|
datetime_input_formats = list(settings.DATETIME_INPUT_FORMATS)
|
||||||
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
|
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
|
||||||
@@ -47,7 +44,7 @@ class EventForm(forms.ModelForm):
|
|||||||
return simplejson.dumps(items)
|
return simplejson.dumps(items)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(EventForm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['items_json'].initial = self._get_items_json
|
self.fields['items_json'].initial = self._get_items_json
|
||||||
self.fields['start_date'].widget.format = '%Y-%m-%d'
|
self.fields['start_date'].widget.format = '%Y-%m-%d'
|
||||||
@@ -100,10 +97,10 @@ class EventForm(forms.ModelForm):
|
|||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
'You haven\'t provided any client contact details. Please add a person or organisation.',
|
'You haven\'t provided any client contact details. Please add a person or organisation.',
|
||||||
code='contact')
|
code='contact')
|
||||||
return super(EventForm, self).clean()
|
return super().clean()
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
m = super(EventForm, self).save(commit=False)
|
m = super().save(commit=False)
|
||||||
|
|
||||||
if (commit):
|
if (commit):
|
||||||
m.save()
|
m.save()
|
||||||
@@ -124,7 +121,7 @@ class EventForm(forms.ModelForm):
|
|||||||
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
|
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
|
||||||
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
|
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
|
||||||
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
|
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
|
||||||
'purchase_order', 'collector']
|
'purchase_order', 'collector', 'forum_url']
|
||||||
|
|
||||||
|
|
||||||
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
||||||
@@ -134,7 +131,7 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
if self.cleaned_data.get('amount') != self.instance.event.total:
|
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).')
|
self.add_error('amount', 'The amount authorised must equal the total for the event (inc VAT).')
|
||||||
return super(BaseClientEventAuthorisationForm, self).clean()
|
return super().clean()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
@@ -142,7 +139,7 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
|
|||||||
|
|
||||||
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
|
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(InternalClientEventAuthorisationForm, self).__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.fields['uni_id'].required = True
|
self.fields['uni_id'].required = True
|
||||||
self.fields['account_code'].required = True
|
self.fields['account_code'].required = True
|
||||||
|
|
||||||
@@ -153,3 +150,94 @@ class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
|
|||||||
|
|
||||||
class EventAuthorisationRequestForm(forms.Form):
|
class EventAuthorisationRequestForm(forms.Form):
|
||||||
email = forms.EmailField(required=True, label='Authoriser Email')
|
email = forms.EmailField(required=True, label='Authoriser Email')
|
||||||
|
|
||||||
|
|
||||||
|
class EventRiskAssessmentForm(forms.ModelForm):
|
||||||
|
related_models = {
|
||||||
|
'power_mic': models.Profile,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>")
|
||||||
|
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
|
||||||
|
raise forms.ValidationError(f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.", code='unusual_answers')
|
||||||
|
return super().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()
|
||||||
|
|
||||||
|
related_models = {
|
||||||
|
'venue': models.Venue,
|
||||||
|
}
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.EventChecklist
|
||||||
|
fields = '__all__'
|
||||||
|
exclude = ['reviewed_at', 'reviewed_by']
|
||||||
|
|
||||||
|
|
||||||
|
class PowerTestRecordForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
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()
|
||||||
|
|
||||||
|
related_models = {
|
||||||
|
'venue': models.Venue,
|
||||||
|
'power_mic': models.Profile,
|
||||||
|
}
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.PowerTestRecord
|
||||||
|
fields = '__all__'
|
||||||
|
exclude = ['reviewed_at', 'reviewed_by']
|
||||||
|
|
||||||
|
|
||||||
|
class EventCheckInForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['time'].initial = timezone.now()
|
||||||
|
self.fields['role'].initial = "Crew"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.EventCheckIn
|
||||||
|
fields = '__all__'
|
||||||
|
exclude = ['end_time']
|
||||||
|
|
||||||
|
|
||||||
|
class EditCheckInForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.EventCheckIn
|
||||||
|
fields = '__all__'
|
||||||
|
|||||||
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,5 +1,7 @@
|
|||||||
from django.core.management.base import BaseCommand, CommandError
|
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from RIGS import models
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@@ -7,5 +9,7 @@ class Command(BaseCommand):
|
|||||||
can_import_settings = True
|
can_import_settings = True
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
call_command('generateSampleUserData')
|
||||||
call_command('generateSampleRIGSData')
|
call_command('generateSampleRIGSData')
|
||||||
call_command('generateSampleAssetsData')
|
call_command('generateSampleAssetsData')
|
||||||
|
call_command('generateSampleTrainingData')
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from django.core.management.base import BaseCommand, CommandError
|
|
||||||
from django.contrib.auth.models import Group, Permission
|
|
||||||
from django.db import transaction
|
|
||||||
from reversion import revisions as reversion
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import random
|
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
|
from RIGS import models
|
||||||
|
|
||||||
|
|
||||||
@@ -16,12 +17,11 @@ class Command(BaseCommand):
|
|||||||
people = []
|
people = []
|
||||||
organisations = []
|
organisations = []
|
||||||
venues = []
|
venues = []
|
||||||
profiles = []
|
events = []
|
||||||
|
profiles = models.Profile.objects.all()
|
||||||
keyholder_group = None
|
|
||||||
finance_group = None
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
print("Generating rigboard data")
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
if not (settings.DEBUG or settings.STAGING):
|
if not (settings.DEBUG or settings.STAGING):
|
||||||
@@ -32,20 +32,13 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||||
|
self.setup_people()
|
||||||
|
self.setup_organisations()
|
||||||
|
self.setup_venues()
|
||||||
|
self.setup_events()
|
||||||
|
print("Done generating rigboard data")
|
||||||
|
|
||||||
self.setupGenericProfiles()
|
def setup_people(self):
|
||||||
|
|
||||||
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",
|
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe",
|
||||||
"Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore",
|
"Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore",
|
||||||
"Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan",
|
"Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan",
|
||||||
@@ -60,25 +53,25 @@ class Command(BaseCommand):
|
|||||||
"Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
|
"Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
|
||||||
for i, name in enumerate(names):
|
for i, name in enumerate(names):
|
||||||
with reversion.create_revision():
|
with reversion.create_revision():
|
||||||
reversion.set_user(random.choice(self.profiles))
|
reversion.set_user(random.choice(models.Profile.objects.all()))
|
||||||
|
person = models.Person.objects.create(name=name)
|
||||||
|
|
||||||
newPerson = models.Person.objects.create(name=name)
|
|
||||||
if i % 3 == 0:
|
if i % 3 == 0:
|
||||||
newPerson.email = "address@person.com"
|
person.email = "address@person.com"
|
||||||
|
|
||||||
if i % 5 == 0:
|
if i % 5 == 0:
|
||||||
newPerson.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
person.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||||
|
|
||||||
if i % 7 == 0:
|
if i % 7 == 0:
|
||||||
newPerson.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
person.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||||
|
|
||||||
if i % 9 == 0:
|
if i % 9 == 0:
|
||||||
newPerson.phone = "01234 567894"
|
person.phone = "01234 567894"
|
||||||
|
|
||||||
newPerson.save()
|
person.save()
|
||||||
self.people.append(newPerson)
|
self.people.append(person)
|
||||||
|
|
||||||
def setupOrganisations(self):
|
def setup_organisations(self):
|
||||||
names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars",
|
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",
|
"ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc",
|
||||||
"Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp",
|
"Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp",
|
||||||
@@ -107,27 +100,28 @@ class Command(BaseCommand):
|
|||||||
"Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
|
"Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
|
||||||
for i, name in enumerate(names):
|
for i, name in enumerate(names):
|
||||||
with reversion.create_revision():
|
with reversion.create_revision():
|
||||||
reversion.set_user(random.choice(self.profiles))
|
reversion.set_user(random.choice(models.Profile.objects.all()))
|
||||||
newOrganisation = models.Organisation.objects.create(name=name)
|
new_organisation = models.Organisation.objects.create(name=name)
|
||||||
|
|
||||||
if i % 2 == 0:
|
if i % 2 == 0:
|
||||||
newOrganisation.has_su_account = True
|
new_organisation.has_su_account = True
|
||||||
|
|
||||||
if i % 3 == 0:
|
if i % 3 == 0:
|
||||||
newOrganisation.email = "address@organisation.com"
|
new_organisation.email = "address@organisation.com"
|
||||||
|
|
||||||
if i % 5 == 0:
|
if i % 5 == 0:
|
||||||
newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
new_organisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||||
|
|
||||||
if i % 7 == 0:
|
if i % 7 == 0:
|
||||||
newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
new_organisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||||
|
|
||||||
if i % 9 == 0:
|
if i % 9 == 0:
|
||||||
newOrganisation.phone = "01234 567894"
|
new_organisation.phone = "01234 567894"
|
||||||
|
|
||||||
newOrganisation.save()
|
new_organisation.save()
|
||||||
self.organisations.append(newOrganisation)
|
self.organisations.append(new_organisation)
|
||||||
|
|
||||||
def setupVenues(self):
|
def setup_venues(self):
|
||||||
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch",
|
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch",
|
||||||
"The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands",
|
"The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands",
|
||||||
"The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins",
|
"The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins",
|
||||||
@@ -143,89 +137,27 @@ class Command(BaseCommand):
|
|||||||
for i, name in enumerate(names):
|
for i, name in enumerate(names):
|
||||||
with reversion.create_revision():
|
with reversion.create_revision():
|
||||||
reversion.set_user(random.choice(self.profiles))
|
reversion.set_user(random.choice(self.profiles))
|
||||||
newVenue = models.Venue.objects.create(name=name)
|
new_venue = models.Venue.objects.create(name=name)
|
||||||
|
|
||||||
if i % 2 == 0:
|
if i % 2 == 0:
|
||||||
newVenue.three_phase_available = True
|
new_venue.three_phase_available = True
|
||||||
|
|
||||||
if i % 3 == 0:
|
if i % 3 == 0:
|
||||||
newVenue.email = "address@venue.com"
|
new_venue.email = "address@venue.com"
|
||||||
|
|
||||||
if i % 5 == 0:
|
if i % 5 == 0:
|
||||||
newVenue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
new_venue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||||
|
|
||||||
if i % 7 == 0:
|
if i % 7 == 0:
|
||||||
newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
new_venue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||||
|
|
||||||
if i % 9 == 0:
|
if i % 9 == 0:
|
||||||
newVenue.phone = "01234 567894"
|
new_venue.phone = "01234 567894"
|
||||||
|
|
||||||
newVenue.save()
|
new_venue.save()
|
||||||
self.venues.append(newVenue)
|
self.venues.append(new_venue)
|
||||||
|
|
||||||
def setupGroups(self):
|
def setup_events(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",
|
|
||||||
"add_asset", "change_asset", "delete_asset",
|
|
||||||
"asset_finance", "view_asset", "view_supplier", "asset_finance",
|
|
||||||
"add_supplier"]
|
|
||||||
financePerms = keyholderPerms + ["add_invoice", "change_invoice", "view_invoice",
|
|
||||||
"add_payment", "change_payment", "delete_payment"]
|
|
||||||
|
|
||||||
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",
|
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",
|
"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",
|
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show",
|
||||||
@@ -236,7 +168,7 @@ class Command(BaseCommand):
|
|||||||
notes = ["The client came into the office at some point", "Who knows if this will happen",
|
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!"]
|
"Probably should check this event", "Maybe not happening", "Run away!"]
|
||||||
|
|
||||||
itemOptions = [
|
item_options = [
|
||||||
{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2,
|
{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2,
|
||||||
'cost': 200.00},
|
'cost': 200.00},
|
||||||
{'name': 'Projector',
|
{'name': 'Projector',
|
||||||
@@ -253,7 +185,7 @@ class Command(BaseCommand):
|
|||||||
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
|
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
|
||||||
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
|
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
|
||||||
|
|
||||||
dayDelta = -120 # start adding events from 4 months ago
|
day_delta = -120 # start adding events from 4 months ago
|
||||||
|
|
||||||
for i in range(150): # Let's add 100 events
|
for i in range(150): # Let's add 100 events
|
||||||
with reversion.create_revision():
|
with reversion.create_revision():
|
||||||
@@ -261,65 +193,98 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
name = names[i % len(names)]
|
name = names[i % len(names)]
|
||||||
|
|
||||||
startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
|
start_date = datetime.date.today() + datetime.timedelta(days=day_delta)
|
||||||
dayDelta = dayDelta + random.randint(0, 3)
|
day_delta = day_delta + random.randint(0, 3)
|
||||||
|
|
||||||
newEvent = models.Event.objects.create(name=name, start_date=startDate)
|
new_event = models.Event.objects.create(name=name, start_date=start_date)
|
||||||
|
|
||||||
if random.randint(0, 2) > 1: # 1 in 3 have a start time
|
if random.randint(0, 2) > 1: # 1 in 3 have a start time
|
||||||
newEvent.start_time = datetime.time(random.randint(15, 20))
|
new_event.start_time = datetime.time(random.randint(15, 20))
|
||||||
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
|
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
|
||||||
newEvent.end_time = datetime.time(random.randint(21, 23))
|
new_event.end_time = datetime.time(random.randint(21, 23))
|
||||||
elif random.randint(0, 1) > 0: # half of the others finish early the next day
|
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)
|
new_event.end_date = new_event.start_date + datetime.timedelta(days=1)
|
||||||
newEvent.end_time = datetime.time(random.randint(0, 5))
|
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
|
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
|
||||||
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1, 4))
|
new_event.end_date = new_event.start_date + datetime.timedelta(days=random.randint(1, 4))
|
||||||
|
|
||||||
if random.randint(0, 6) > 0: # 5 in 6 have MIC
|
if random.randint(0, 6) > 0: # 5 in 6 have MIC
|
||||||
newEvent.mic = random.choice(self.profiles)
|
new_event.mic = random.choice(self.profiles)
|
||||||
|
|
||||||
if random.randint(0, 6) > 0: # 5 in 6 have organisation
|
if random.randint(0, 6) > 0: # 5 in 6 have organisation
|
||||||
newEvent.organisation = random.choice(self.organisations)
|
new_event.organisation = random.choice(self.organisations)
|
||||||
|
|
||||||
if random.randint(0, 6) > 0: # 5 in 6 have person
|
if random.randint(0, 6) > 0: # 5 in 6 have person
|
||||||
newEvent.person = random.choice(self.people)
|
new_event.person = random.choice(self.people)
|
||||||
|
|
||||||
if random.randint(0, 6) > 0: # 5 in 6 have venue
|
if random.randint(0, 6) > 0: # 5 in 6 have venue
|
||||||
newEvent.venue = random.choice(self.venues)
|
new_event.venue = random.choice(self.venues)
|
||||||
|
|
||||||
# Could have any status, equally weighted
|
# Could have any status, equally weighted
|
||||||
newEvent.status = random.choice(
|
new_event.status = random.choice(
|
||||||
[models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
|
[models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
|
||||||
|
|
||||||
newEvent.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
|
new_event.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
|
||||||
|
|
||||||
if random.randint(0, 1) > 0: # 1 in 2 have description
|
if random.randint(0, 1) > 0: # 1 in 2 have description
|
||||||
newEvent.description = random.choice(descriptions)
|
new_event.description = random.choice(descriptions)
|
||||||
|
|
||||||
if random.randint(0, 1) > 0: # 1 in 2 have notes
|
if random.randint(0, 1) > 0: # 1 in 2 have notes
|
||||||
newEvent.notes = random.choice(notes)
|
new_event.notes = random.choice(notes)
|
||||||
|
|
||||||
newEvent.save()
|
new_event.save()
|
||||||
|
|
||||||
# Now add some items
|
# Now add some items
|
||||||
for j in range(random.randint(1, 5)):
|
for j in range(random.randint(1, 5)):
|
||||||
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
|
item_data = item_options[random.randint(0, len(item_options) - 1)]
|
||||||
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
|
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
|
||||||
newItem.save()
|
new_item.save()
|
||||||
|
|
||||||
while newEvent.sum_total < 0:
|
while new_event.sum_total < 0:
|
||||||
itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
|
item_data = item_options[random.randint(0, len(item_options) - 1)]
|
||||||
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
|
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
|
||||||
newItem.save()
|
new_item.save()
|
||||||
|
|
||||||
with reversion.create_revision():
|
with reversion.create_revision():
|
||||||
reversion.set_user(random.choice(self.profiles))
|
reversion.set_user(random.choice(self.profiles))
|
||||||
if newEvent.start_date < datetime.date.today(): # think about adding an invoice
|
if new_event.start_date < datetime.date.today(): # think about adding an invoice
|
||||||
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
|
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
|
||||||
newInvoice = models.Invoice.objects.create(event=newEvent)
|
new_invoice = models.Invoice.objects.create(event=new_event)
|
||||||
if newEvent.status is models.Event.CANCELLED: # void cancelled events
|
if new_event.status is models.Event.CANCELLED: # void cancelled events
|
||||||
newInvoice.void = True
|
new_invoice.void = True
|
||||||
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
|
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
|
||||||
models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance,
|
models.Payment.objects.create(invoice=new_invoice, amount=new_invoice.balance,
|
||||||
date=datetime.date.today())
|
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,
|
||||||
|
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",
|
||||||
|
date=timezone.now(), venue=random.choice(self.venues))
|
||||||
|
|||||||
38
RIGS/management/commands/send_reminders.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import premailer
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.template.loader import get_template
|
||||||
|
from django.contrib.staticfiles import finders
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from RIGS import models
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Sends email reminders as required. Triggered daily through heroku-scheduler in production.'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
events = models.Event.objects.current_events().select_related('riskassessment')
|
||||||
|
for event in events:
|
||||||
|
earliest_time = event.earliest_time if isinstance(event.earliest_time, datetime.datetime) else timezone.make_aware(datetime.datetime.combine(event.earliest_time, datetime.time(00, 00)))
|
||||||
|
# 48 hours = 172800 seconds
|
||||||
|
if event.is_rig and not event.cancelled and not event.dry_hire and (earliest_time - timezone.now()).total_seconds() <= 172800 and not hasattr(event, 'riskassessment'):
|
||||||
|
context = {
|
||||||
|
"event": event,
|
||||||
|
"url": "https://" + settings.DOMAIN + reverse('event_ra', kwargs={'pk': event.pk})
|
||||||
|
}
|
||||||
|
target = event.mic.email if event.mic else f"productions@{settings.DOMAIN}"
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
f"{event} - Risk Assessment Incomplete",
|
||||||
|
get_template("email/ra_reminder.txt").render(context),
|
||||||
|
to=[target],
|
||||||
|
reply_to=[f"h.s.manager@{settings.DOMAIN}"],
|
||||||
|
)
|
||||||
|
css = finders.find('css/email.css')
|
||||||
|
html = premailer.Premailer(get_template("email/ra_reminder.html").render(context), external_styles=css).transform()
|
||||||
|
msg.attach_alternative(html, 'text/html')
|
||||||
|
msg.send()
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import RIGS.models
|
import RIGS.models
|
||||||
|
import versioning
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -25,6 +26,6 @@ class Migration(migrations.Migration):
|
|||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import RIGS.models
|
import RIGS.models
|
||||||
|
import versioning
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -21,6 +22,6 @@ class Migration(migrations.Migration):
|
|||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import RIGS.models
|
import RIGS.models
|
||||||
|
import versioning
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -41,7 +42,7 @@ class Migration(migrations.Migration):
|
|||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='EventItem',
|
name='EventItem',
|
||||||
@@ -70,7 +71,7 @@ class Migration(migrations.Migration):
|
|||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
},
|
},
|
||||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='event',
|
model_name='event',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Generated by Django 2.0.13 on 2020-01-11 18:29
|
# Generated by Django 2.0.13 on 2020-01-11 18:29
|
||||||
# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
|
# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
def approve_legacy(apps, schema_editor):
|
def approve_legacy(apps, schema_editor):
|
||||||
@@ -15,5 +15,5 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RunPython(approve_legacy)
|
migrations.RunPython(approve_legacy, migrations.RunPython.noop)
|
||||||
]
|
]
|
||||||
|
|||||||
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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Generated by Django 3.0.3 on 2020-03-18 00:24
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0038_auto_20200306_2000'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='EventCrew',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
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)
|
||||||
|
]
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Generated by Django 3.0.3 on 2020-04-15 18:40
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0039_delete_eventcrew'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='RIGSVersion',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
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
@@ -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
@@ -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
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
RIGS/migrations/0045_alter_profile_is_approved.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.12 on 2022-10-20 23:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0044_profile_is_supervisor'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='is_approved',
|
||||||
|
field=models.BooleanField(default=False, help_text='Designates whether a staff member has approved this user.', verbose_name='Approval Status'),
|
||||||
|
),
|
||||||
|
]
|
||||||
71
RIGS/migrations/0046_create_powertests.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Generated by Django 3.2.16 on 2023-05-08 15:58
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import versioning.versioning
|
||||||
|
|
||||||
|
def migrate_old_data(apps, schema_editor):
|
||||||
|
EventChecklist = apps.get_model('RIGS', 'EventChecklist')
|
||||||
|
PowerTestRecord = apps.get_model('RIGS', 'PowerTestRecord')
|
||||||
|
for ec in EventChecklist.objects.all():
|
||||||
|
# New highscore for the most pythonic BS I've ever written.
|
||||||
|
PowerTestRecord.objects.create(event=ec.event, venue=ec.venue, reviewed_by=ec.reviewed_by, **{i.name:getattr(ec, i.attname) for i in PowerTestRecord._meta.get_fields() if not (i.is_relation or i.auto_created or i.name == "notes")})
|
||||||
|
|
||||||
|
|
||||||
|
def revert(apps, schema_editor):
|
||||||
|
apps.get_model('RIGS', 'PowerTestRecord').objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0045_alter_profile_is_approved'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PowerTestRecord',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_tests', to='RIGS.event')),
|
||||||
|
('notes', models.TextField(blank=True, default='')),
|
||||||
|
('venue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.venue')),
|
||||||
|
('reviewed_at', models.DateTimeField(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.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')),
|
||||||
|
('fd_pssc', models.IntegerField(blank=True, help_text='Prospective Short Circuit Current', null=True, verbose_name='PSCC')),
|
||||||
|
('w1_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
|
||||||
|
('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.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')),
|
||||||
|
('w2_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
|
||||||
|
('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.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')),
|
||||||
|
('w3_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
|
||||||
|
('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.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')),
|
||||||
|
('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_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
'ordering': ['event'],
|
||||||
|
'permissions': [('review_power', 'Can review Power Test Records')],
|
||||||
|
},
|
||||||
|
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_old_data, reverse_code=revert),
|
||||||
|
]
|
||||||
44
RIGS/migrations/0047_auto_20230517_0944.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-05-17 08:44
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_old_data(apps, schema_editor):
|
||||||
|
EventChecklist = apps.get_model('RIGS', 'EventChecklist')
|
||||||
|
EventCheckIn = apps.get_model('RIGS', 'EventCheckIn')
|
||||||
|
for ec in EventChecklist.objects.all():
|
||||||
|
for crew in ec.crew.all():
|
||||||
|
try:
|
||||||
|
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end, vehicle=ec.vehicles.get(driver=crew.crewmember).vehicle)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end)
|
||||||
|
|
||||||
|
|
||||||
|
def revert(apps, schema_editor):
|
||||||
|
apps.get_model('RIGS', 'EventCheckIn').objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0046_create_powertests'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventCheckIn',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('time', models.DateTimeField()),
|
||||||
|
('role', models.CharField(blank=True, max_length=50)),
|
||||||
|
('vehicle', models.CharField(blank=True, max_length=100)),
|
||||||
|
('end_time', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.event')),
|
||||||
|
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkins', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_old_data, reverse_code=revert),
|
||||||
|
]
|
||||||
156
RIGS/migrations/0048_auto_20230518_1256.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-05-18 11:56
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0047_auto_20230517_0944'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklistvehicle',
|
||||||
|
name='checklist',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklistvehicle',
|
||||||
|
name='driver',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='all_rcds_tested',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='earthing',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='fd_earth_fault',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='fd_phase_rotation',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='fd_pssc',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='fd_voltage_l1',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='fd_voltage_l2',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='fd_voltage_l3',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='labelling',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='pat',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='power_mic',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='public_sockets_tested',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='rcds',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='source_rcd',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='supply_test',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w1_description',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w1_earth_fault',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w1_polarity',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w1_voltage',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w2_description',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w2_earth_fault',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w2_polarity',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w2_voltage',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w3_description',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w3_earth_fault',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w3_polarity',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='w3_voltage',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powertestrecord',
|
||||||
|
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.AlterField(
|
||||||
|
model_name='eventchecklist',
|
||||||
|
name='reviewed_at',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powertestrecord',
|
||||||
|
name='reviewed_at',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='riskassessment',
|
||||||
|
name='reviewed_at',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='EventChecklistCrew',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='EventChecklistVehicle',
|
||||||
|
),
|
||||||
|
]
|
||||||
53
RIGS/migrations/0049_auto_20230529_1123.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-05-29 10:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0048_auto_20230518_1256'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powertestrecord',
|
||||||
|
name='fd_earth_fault',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powertestrecord',
|
||||||
|
name='fd_pssc',
|
||||||
|
field=models.IntegerField(blank=True, help_text='Prospective Short Circuit Current / A', null=True, verbose_name='PSCC'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powertestrecord',
|
||||||
|
name='w1_earth_fault',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powertestrecord',
|
||||||
|
name='w1_voltage',
|
||||||
|
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powertestrecord',
|
||||||
|
name='w2_earth_fault',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powertestrecord',
|
||||||
|
name='w2_voltage',
|
||||||
|
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powertestrecord',
|
||||||
|
name='w3_earth_fault',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powertestrecord',
|
||||||
|
name='w3_voltage',
|
||||||
|
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
19
RIGS/migrations/0050_event_forum_url.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-06-27 11:28
|
||||||
|
|
||||||
|
import RIGS.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0049_auto_20230529_1123'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='forum_url',
|
||||||
|
field=models.URLField(blank=True, default='', validators=[RIGS.models.validate_forum_url]),
|
||||||
|
),
|
||||||
|
]
|
||||||
633
RIGS/models.py
@@ -1,32 +1,48 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import datetime
|
import random
|
||||||
import pytz
|
import string
|
||||||
|
from collections import Counter
|
||||||
|
from decimal import Decimal
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django.db import models
|
import pytz
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django import forms
|
||||||
|
from django.db.models import Q, F
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
from reversion.models import Version
|
from reversion.models import Version
|
||||||
import string
|
from versioning.versioning import RevisionMixin
|
||||||
|
|
||||||
import random
|
|
||||||
from collections import Counter
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
def filter_by_pk(filt, query):
|
||||||
from django.urls import reverse_lazy
|
# try and parse an int
|
||||||
|
try:
|
||||||
|
val = int(query)
|
||||||
|
filt = filt | Q(pk=val)
|
||||||
|
except: # noqa
|
||||||
|
# not an integer
|
||||||
|
pass
|
||||||
|
return filt
|
||||||
|
|
||||||
|
|
||||||
class Profile(AbstractUser):
|
class Profile(AbstractUser):
|
||||||
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
|
initials = models.CharField(max_length=5, null=True, blank=False)
|
||||||
phone = models.CharField(max_length=13, null=True, blank=True)
|
phone = models.CharField(max_length=13, blank=True, default='')
|
||||||
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
|
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
||||||
is_approved = models.BooleanField(default=False)
|
is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.")
|
||||||
last_emailed = models.DateTimeField(blank=True,
|
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
||||||
null=True) # 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
|
@classmethod
|
||||||
def make_api_key(cls):
|
def make_api_key(cls):
|
||||||
@@ -47,12 +63,12 @@ class Profile(AbstractUser):
|
|||||||
def name(self):
|
def name(self):
|
||||||
name = self.get_full_name()
|
name = self.get_full_name()
|
||||||
if self.initials:
|
if self.initials:
|
||||||
name += ' "{}"'.format(self.initials)
|
name += f' "{self.initials}"'
|
||||||
return name
|
return name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_events(self):
|
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
|
@classmethod
|
||||||
def admins(cls):
|
def admins(cls):
|
||||||
@@ -60,48 +76,38 @@ class Profile(AbstractUser):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def users_awaiting_approval_count(cls):
|
def users_awaiting_approval_count(cls):
|
||||||
return Profile.objects.filter(models.Q(is_approved=False)).count()
|
# last_login = None ensures we only pick up genuinely new users, not those that have been deactivated for inactivity
|
||||||
|
return Profile.objects.filter(is_approved=False, last_login=None).count()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def current_event(self):
|
||||||
|
q = EventCheckIn.objects.filter(person=self, end_time=None)
|
||||||
|
return q.latest('time') if q.exists() else None
|
||||||
|
|
||||||
class RevisionMixin(object):
|
|
||||||
@property
|
|
||||||
def current_version(self):
|
|
||||||
version = Version.objects.get_for_object(self).select_related('revision').first()
|
|
||||||
return version
|
|
||||||
|
|
||||||
@property
|
class ContactableManager(models.Manager):
|
||||||
def last_edited_at(self):
|
def search(self, query=None):
|
||||||
version = self.current_version
|
qs = self.get_queryset()
|
||||||
if version is None:
|
if query is not None:
|
||||||
return None
|
or_lookup = Q(name__icontains=query) | Q(email__icontains=query) | Q(address__icontains=query) | Q(notes__icontains=query) | Q(
|
||||||
return version.revision.date_created
|
phone__startswith=query) | Q(phone__endswith=query)
|
||||||
|
|
||||||
@property
|
or_lookup = filter_by_pk(or_lookup, query)
|
||||||
def last_edited_by(self):
|
|
||||||
version = self.current_version
|
|
||||||
if version is None:
|
|
||||||
return None
|
|
||||||
return version.revision.user
|
|
||||||
|
|
||||||
@property
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
def current_version_id(self):
|
return qs
|
||||||
version = self.current_version
|
|
||||||
if version is None:
|
|
||||||
return None
|
|
||||||
return "V{0} | R{1}".format(version.pk, version.revision.pk)
|
|
||||||
|
|
||||||
|
|
||||||
class Person(models.Model, RevisionMixin):
|
class Person(models.Model, RevisionMixin):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
email = models.EmailField(blank=True, null=True)
|
email = models.EmailField(blank=True, default='')
|
||||||
|
address = models.TextField(blank=True, default='')
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
address = models.TextField(blank=True, null=True)
|
objects = ContactableManager()
|
||||||
|
|
||||||
notes = models.TextField(blank=True, null=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
string = self.name
|
string = self.name
|
||||||
@@ -127,19 +133,19 @@ class Person(models.Model, RevisionMixin):
|
|||||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('person_detail', kwargs={'pk': self.pk})
|
return reverse('person_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
class Organisation(models.Model, RevisionMixin):
|
class Organisation(models.Model, RevisionMixin):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
email = models.EmailField(blank=True, null=True)
|
email = models.EmailField(blank=True, default='')
|
||||||
|
address = models.TextField(blank=True, default='')
|
||||||
address = models.TextField(blank=True, null=True)
|
notes = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
notes = models.TextField(blank=True, null=True)
|
|
||||||
union_account = models.BooleanField(default=False)
|
union_account = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
objects = ContactableManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
string = self.name
|
string = self.name
|
||||||
if self.notes is not None:
|
if self.notes is not None:
|
||||||
@@ -164,7 +170,7 @@ class Organisation(models.Model, RevisionMixin):
|
|||||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
|
return reverse('organisation_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
class VatManager(models.Manager):
|
class VatManager(models.Manager):
|
||||||
@@ -172,7 +178,6 @@ class VatManager(models.Manager):
|
|||||||
return self.find_rate(timezone.now())
|
return self.find_rate(timezone.now())
|
||||||
|
|
||||||
def find_rate(self, date):
|
def find_rate(self, date):
|
||||||
# return self.filter(startAt__lte=date).latest()
|
|
||||||
try:
|
try:
|
||||||
return self.filter(start_at__lte=date).latest()
|
return self.filter(start_at__lte=date).latest()
|
||||||
except VatRate.DoesNotExist:
|
except VatRate.DoesNotExist:
|
||||||
@@ -189,6 +194,8 @@ class VatRate(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
objects = VatManager()
|
objects = VatManager()
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_percent(self):
|
def as_percent(self):
|
||||||
return self.rate * 100
|
return self.rate * 100
|
||||||
@@ -198,17 +205,18 @@ class VatRate(models.Model, RevisionMixin):
|
|||||||
get_latest_by = 'start_at'
|
get_latest_by = 'start_at'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.comment + " " + str(self.start_at) + " @ " + str(self.as_percent) + "%"
|
return f"{self.comment} {self.start_at} @ {self.as_percent}%"
|
||||||
|
|
||||||
|
|
||||||
class Venue(models.Model, RevisionMixin):
|
class Venue(models.Model, RevisionMixin):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
phone = models.CharField(max_length=15, blank=True, default='')
|
||||||
email = models.EmailField(blank=True, null=True)
|
email = models.EmailField(blank=True, default='')
|
||||||
three_phase_available = models.BooleanField(default=False)
|
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, default='')
|
||||||
|
|
||||||
address = models.TextField(blank=True, null=True)
|
objects = ContactableManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
string = self.name
|
string = self.name
|
||||||
@@ -221,24 +229,23 @@ class Venue(models.Model, RevisionMixin):
|
|||||||
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
|
return reverse('venue_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
class EventManager(models.Manager):
|
class EventManager(models.Manager):
|
||||||
def current_events(self):
|
def current_events(self):
|
||||||
events = self.filter(
|
events = self.filter(
|
||||||
(models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q(
|
||||||
status=Event.CANCELLED)) | # Starts after with no end
|
status=Event.CANCELLED)) | # Starts after with no end
|
||||||
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
|
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
|
||||||
status=Event.CANCELLED)) | # Ends after
|
status=Event.CANCELLED)) | # Ends after
|
||||||
(models.Q(dry_hire=True, start_date__gte=timezone.now().date()) & ~models.Q(
|
(models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(
|
||||||
status=Event.CANCELLED)) | # Active dry hire
|
status=Event.CANCELLED)) | # Active dry hire
|
||||||
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
|
(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.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
|
||||||
models.Q(status=Event.CANCELLED, start_date__gte=timezone.now().date()) # Canceled but not started
|
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',
|
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
'organisation',
|
|
||||||
'venue', 'mic')
|
|
||||||
return events
|
return events
|
||||||
|
|
||||||
def events_in_bounds(self, start, end):
|
def events_in_bounds(self, start, end):
|
||||||
@@ -261,18 +268,54 @@ class EventManager(models.Manager):
|
|||||||
|
|
||||||
def rig_count(self):
|
def rig_count(self):
|
||||||
event_count = self.filter(
|
event_count = self.filter(
|
||||||
(models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False,
|
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
|
||||||
is_rig=True) & ~models.Q(
|
is_rig=True) & ~models.Q(
|
||||||
status=Event.CANCELLED)) | # Starts after with no end
|
status=Event.CANCELLED)) | # Starts after with no end
|
||||||
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False, is_rig=True) & ~models.Q(
|
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q(
|
||||||
status=Event.CANCELLED)) | # Ends after
|
status=Event.CANCELLED)) | # Ends after
|
||||||
(models.Q(dry_hire=True, start_date__gte=timezone.now().date(), is_rig=True) & ~models.Q(
|
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q(
|
||||||
status=Event.CANCELLED)) | # Active dry hire
|
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
|
|
||||||
).count()
|
).count()
|
||||||
return event_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
|
||||||
|
|
||||||
|
def search(self, query=None):
|
||||||
|
qs = self.get_queryset()
|
||||||
|
if query is not None:
|
||||||
|
or_lookup = Q(name__icontains=query) | Q(description__icontains=query) | Q(notes__icontains=query)
|
||||||
|
|
||||||
|
or_lookup = filter_by_pk(or_lookup, query)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if query[0] == "N":
|
||||||
|
val = int(query[1:])
|
||||||
|
or_lookup = Q(pk=val) # If string is N###### then do a simple PK filter
|
||||||
|
except: # noqa
|
||||||
|
pass
|
||||||
|
|
||||||
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
def validate_forum_url(value):
|
||||||
|
if not value:
|
||||||
|
return # Required error is done the field
|
||||||
|
obj = urlparse(value)
|
||||||
|
if obj.hostname not in ('forum.nottinghamtec.co.uk'):
|
||||||
|
raise ValidationError('URL must point to a location on the TEC Forum')
|
||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['items'])
|
@reversion.register(follow=['items'])
|
||||||
class Event(models.Model, RevisionMixin):
|
class Event(models.Model, RevisionMixin):
|
||||||
@@ -292,8 +335,8 @@ class Event(models.Model, RevisionMixin):
|
|||||||
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
|
||||||
organisation = models.ForeignKey('Organisation', blank=True, null=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)
|
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, default='')
|
||||||
notes = models.TextField(blank=True, null=True)
|
notes = models.TextField(blank=True, default='')
|
||||||
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
|
||||||
dry_hire = models.BooleanField(default=False)
|
dry_hire = models.BooleanField(default=False)
|
||||||
is_rig = models.BooleanField(default=True)
|
is_rig = models.BooleanField(default=True)
|
||||||
@@ -307,7 +350,6 @@ class Event(models.Model, RevisionMixin):
|
|||||||
end_time = models.TimeField(blank=True, null=True)
|
end_time = models.TimeField(blank=True, null=True)
|
||||||
access_at = models.DateTimeField(blank=True, null=True)
|
access_at = models.DateTimeField(blank=True, null=True)
|
||||||
meet_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
|
# 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,
|
||||||
@@ -316,18 +358,23 @@ class Event(models.Model, RevisionMixin):
|
|||||||
verbose_name="MIC", on_delete=models.CASCADE)
|
verbose_name="MIC", on_delete=models.CASCADE)
|
||||||
|
|
||||||
# Monies
|
# Monies
|
||||||
payment_method = models.CharField(max_length=255, blank=True, null=True)
|
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
|
||||||
payment_received = models.CharField(max_length=255, blank=True, null=True)
|
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
|
||||||
purchase_order = models.CharField(max_length=255, blank=True, null=True, verbose_name='PO')
|
|
||||||
collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='collected by')
|
|
||||||
|
|
||||||
# Authorisation request details
|
# Authorisation request details
|
||||||
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
|
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_at = models.DateTimeField(null=True, blank=True)
|
||||||
auth_request_to = models.EmailField(null=True, blank=True)
|
auth_request_to = models.EmailField(blank=True, default='')
|
||||||
|
|
||||||
# Risk assessment info
|
forum_url = models.URLField(default='', blank=True, validators=[validate_forum_url])
|
||||||
risk_assessment_edit_url = models.CharField(verbose_name="risk assessment", max_length=255, blank=True, null=True)
|
|
||||||
|
@property
|
||||||
|
def display_id(self):
|
||||||
|
if self.pk:
|
||||||
|
if self.is_rig:
|
||||||
|
return f"N{self.pk:05d}"
|
||||||
|
return self.pk
|
||||||
|
return "????"
|
||||||
|
|
||||||
# Calculated values
|
# Calculated values
|
||||||
"""
|
"""
|
||||||
@@ -336,18 +383,7 @@ class Event(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def sum_total(self):
|
def sum_total(self):
|
||||||
# Manual querying is required for efficiency whilst maintaining floating point arithmetic
|
total = self.items.aggregate(
|
||||||
# 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(
|
|
||||||
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
|
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
|
||||||
output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
output_field=models.DecimalField(max_digits=10, decimal_places=2))
|
||||||
)['sum_total']
|
)['sum_total']
|
||||||
@@ -361,6 +397,9 @@ class Event(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def vat(self):
|
def vat(self):
|
||||||
|
# No VAT is owed on internal transfers
|
||||||
|
if self.internal:
|
||||||
|
return 0
|
||||||
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
|
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -380,8 +419,16 @@ class Event(models.Model, RevisionMixin):
|
|||||||
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def authorised(self):
|
def hs_done(self):
|
||||||
return not self.internal and self.purchase_order or self.authorisation.amount == self.total
|
return self.riskassessment is not None and self.has_checklist and self.has_power
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_checklist(self):
|
||||||
|
return self.checklists.exists()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_power(self):
|
||||||
|
return self.power_tests.exists()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_start_time(self):
|
def has_start_time(self):
|
||||||
@@ -445,30 +492,50 @@ class Event(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def internal(self):
|
def internal(self):
|
||||||
return self.organisation and self.organisation.union_account
|
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)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_check_in(self):
|
||||||
|
earliest = self.earliest_time
|
||||||
|
if isinstance(self.earliest_time, datetime.date):
|
||||||
|
earliest = datetime.datetime.combine(self.start_date, datetime.time(00, 00))
|
||||||
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
|
earliest = tz.localize(earliest)
|
||||||
|
return not self.dry_hire and not self.status == Event.CANCELLED and earliest <= timezone.now()
|
||||||
|
|
||||||
objects = EventManager()
|
objects = EventManager()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
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):
|
def __str__(self):
|
||||||
return str(self.pk) + ": " + self.name
|
return f"{self.display_id} | {self.name}"
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
errdict = {}
|
||||||
if self.end_date and self.start_date > self.end_date:
|
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
|
startEndSameDay = not self.end_date or self.end_date == self.start_date
|
||||||
hasStartAndEnd = self.has_start_time and self.has_end_time
|
hasStartAndEnd = self.has_start_time and self.has_end_time
|
||||||
if startEndSameDay and hasStartAndEnd and self.start_time > self.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 is not None:
|
||||||
if self.access_at.date() > self.start_date:
|
if self.access_at.date() > self.start_date:
|
||||||
raise ValidationError('Regardless of what some clients might think, access time cannot be after the event has started.')
|
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:
|
elif self.start_time is not None and self.start_date == self.access_at.date() and self.access_at.time() > self.start_time:
|
||||||
raise ValidationError('Regardless of what some clients might think, access time cannot be after the event has started.')
|
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):
|
def save(self, *args, **kwargs):
|
||||||
"""Call :meth:`full_clean` before saving."""
|
"""Call :meth:`full_clean` before saving."""
|
||||||
@@ -476,14 +543,17 @@ class Event(models.Model, RevisionMixin):
|
|||||||
super(Event, self).save(*args, **kwargs)
|
super(Event, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class EventItem(models.Model):
|
@reversion.register
|
||||||
|
class EventItem(models.Model, RevisionMixin):
|
||||||
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, default='')
|
||||||
quantity = models.IntegerField()
|
quantity = models.IntegerField()
|
||||||
cost = models.DecimalField(max_digits=10, decimal_places=2)
|
cost = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
order = models.IntegerField()
|
order = models.IntegerField()
|
||||||
|
|
||||||
|
reversion_hide = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_cost(self):
|
def total_cost(self):
|
||||||
return self.cost * self.quantity
|
return self.cost * self.quantity
|
||||||
@@ -492,7 +562,11 @@ class EventItem(models.Model):
|
|||||||
ordering = ['order']
|
ordering = ['order']
|
||||||
|
|
||||||
def __str__(self):
|
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}"
|
||||||
|
|
||||||
|
|
||||||
@reversion.register
|
@reversion.register
|
||||||
@@ -500,25 +574,74 @@ class EventAuthorisation(models.Model, RevisionMixin):
|
|||||||
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
|
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
|
||||||
email = models.EmailField()
|
email = models.EmailField()
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID")
|
uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID")
|
||||||
account_code = models.CharField(max_length=50, blank=True, null=True)
|
account_code = models.CharField(max_length=50, default='', blank=True)
|
||||||
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
|
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
|
||||||
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
|
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
|
return reverse('event_detail', kwargs={'pk': self.event_id})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def activity_feed_string(self):
|
def activity_feed_string(self):
|
||||||
return str("N%05d" % self.event.pk + ' (requested by ' + self.sent_by.initials + ')')
|
return f"{self.event.display_id} (requested by {self.sent_by.initials})"
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceManager(models.Manager):
|
||||||
|
def outstanding_invoices(self):
|
||||||
|
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||||
|
sql = "SELECT * FROM " \
|
||||||
|
"(SELECT " \
|
||||||
|
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
||||||
|
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
||||||
|
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
||||||
|
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
||||||
|
"AS sub " \
|
||||||
|
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
||||||
|
"ORDER BY invoice_date"
|
||||||
|
|
||||||
|
query = self.raw(sql)
|
||||||
|
return query
|
||||||
|
|
||||||
|
def search(self, query=None):
|
||||||
|
qs = self.get_queryset()
|
||||||
|
if query is not None:
|
||||||
|
or_lookup = Q(event__name__icontains=query)
|
||||||
|
|
||||||
|
or_lookup = filter_by_pk(or_lookup, query)
|
||||||
|
|
||||||
|
# try and parse an int
|
||||||
|
try:
|
||||||
|
val = int(query)
|
||||||
|
or_lookup = or_lookup | Q(event__pk=val)
|
||||||
|
except: # noqa
|
||||||
|
# not an integer
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if query[0] == "N":
|
||||||
|
val = int(query[1:])
|
||||||
|
or_lookup = Q(event__pk=val) # If string is Nxxxxx then filter by event number
|
||||||
|
elif query[0] == "#":
|
||||||
|
val = int(query[1:])
|
||||||
|
or_lookup = Q(pk=val) # If string is #xxxxx then filter by invoice number
|
||||||
|
except: # noqa
|
||||||
|
pass
|
||||||
|
|
||||||
|
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
@reversion.register(follow=['payment_set'])
|
@reversion.register(follow=['payment_set'])
|
||||||
class Invoice(models.Model):
|
class Invoice(models.Model, RevisionMixin):
|
||||||
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
event = models.OneToOneField('Event', on_delete=models.CASCADE)
|
||||||
invoice_date = models.DateField(auto_now_add=True)
|
invoice_date = models.DateField(auto_now_add=True)
|
||||||
void = models.BooleanField(default=False)
|
void = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
reversion_perm = 'RIGS.view_invoice'
|
||||||
|
|
||||||
|
objects = InvoiceManager()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sum_total(self):
|
def sum_total(self):
|
||||||
return self.event.sum_total
|
return self.event.sum_total
|
||||||
@@ -542,14 +665,26 @@ class Invoice(models.Model):
|
|||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
return self.balance == 0 or self.void
|
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 f"{self.display_id} for Event {self.event.display_id}"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
|
return f"{self.display_id}: {self.event} (£{self.balance:.2f})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_id(self):
|
||||||
|
return f"#{self.pk:05d}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-invoice_date']
|
ordering = ['-invoice_date']
|
||||||
|
|
||||||
|
|
||||||
class Payment(models.Model):
|
@reversion.register
|
||||||
|
class Payment(models.Model, RevisionMixin):
|
||||||
CASH = 'C'
|
CASH = 'C'
|
||||||
INTERNAL = 'I'
|
INTERNAL = 'I'
|
||||||
EXTERNAL = 'E'
|
EXTERNAL = 'E'
|
||||||
@@ -566,7 +701,271 @@ class Payment(models.Model):
|
|||||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
|
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
|
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):
|
def __str__(self):
|
||||||
return "%s: %d" % (self.get_method_display(), self.amount)
|
return f"{self.get_method_display()}: {self.amount}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return f"payment of £{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')
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewableModel(models.Model):
|
||||||
|
reviewed_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||||
|
verbose_name="Reviewer", on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@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]
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class RiskAssessment(ReviewableModel, 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
|
||||||
|
|
||||||
|
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 Doesn't require checking with a super either way
|
||||||
|
'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')
|
||||||
|
]
|
||||||
|
|
||||||
|
@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]
|
||||||
|
|
||||||
|
def get_event_size_display(self):
|
||||||
|
return self.SIZES[self.event_size][1] + " Event"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.pk} | {self.event}"
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('ra_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return str(self.event)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class EventChecklist(ReviewableModel, RevisionMixin):
|
||||||
|
event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
# General
|
||||||
|
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")
|
||||||
|
|
||||||
|
inverted_fields = []
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['event']
|
||||||
|
permissions = [
|
||||||
|
('review_eventchecklist', 'Can review Event Checklists')
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.pk} - {self.event}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return str(self.event)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('ec_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
@reversion.register
|
||||||
|
class PowerTestRecord(ReviewableModel, RevisionMixin):
|
||||||
|
earth_fault_text = "Earth Fault Loop Impedance (Z<small>S</small>) / Ω"
|
||||||
|
pssc_text = "Prospective Short Circuit Current / A"
|
||||||
|
|
||||||
|
event = models.ForeignKey('Event', related_name='power_tests', on_delete=models.CASCADE)
|
||||||
|
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)
|
||||||
|
notes = models.TextField(blank=True, default='')
|
||||||
|
# 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=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
|
||||||
|
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text=pssc_text)
|
||||||
|
# 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 / V")
|
||||||
|
w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
|
||||||
|
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 / V")
|
||||||
|
w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
|
||||||
|
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 / V")
|
||||||
|
w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
|
||||||
|
|
||||||
|
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>")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['event']
|
||||||
|
permissions = [
|
||||||
|
('review_power', 'Can review Power Test Records')
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.pk} - {self.event}"
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('pt_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity_feed_string(self):
|
||||||
|
return str(self.event)
|
||||||
|
|
||||||
|
|
||||||
|
class EventCheckIn(models.Model):
|
||||||
|
event = models.ForeignKey('Event', related_name='crew', on_delete=models.CASCADE)
|
||||||
|
person = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='checkins', on_delete=models.CASCADE)
|
||||||
|
time = models.DateTimeField()
|
||||||
|
role = models.CharField(max_length=50, blank=True)
|
||||||
|
vehicle = models.CharField(max_length=100, blank=True)
|
||||||
|
end_time = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.person} on {self.event}"
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
sass = " Please invent time travel and retry."
|
||||||
|
if self.time > timezone.now():
|
||||||
|
raise ValidationError("May not check in in the future." + sass)
|
||||||
|
if self.end_time and self.end_time < self.time:
|
||||||
|
raise ValidationError("May not check out before you've checked in." + sass)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('event_detail', kwargs={'pk': self.event_id})
|
||||||
|
|
||||||
|
def active(self):
|
||||||
|
return end_time is not None
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import datetime
|
|
||||||
import re
|
import re
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from django.db.models.signals import post_save
|
|
||||||
from PyPDF2 import PdfFileReader, PdfFileMerger
|
from PyPDF2 import PdfFileReader, PdfFileMerger
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
from django.contrib.staticfiles import finders
|
||||||
|
from django.core.cache import cache
|
||||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
||||||
|
from django.db.models.signals import post_save
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from registration.signals import user_activated
|
|
||||||
from premailer import Premailer
|
from premailer import Premailer
|
||||||
|
from registration.signals import user_activated
|
||||||
|
from reversion import revisions as reversion
|
||||||
from z3c.rml import rml2pdf
|
from z3c.rml import rml2pdf
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
@@ -24,12 +25,6 @@ def send_eventauthorisation_success_email(instance):
|
|||||||
# Generate PDF first to prevent context conflicts
|
# Generate PDF first to prevent context conflicts
|
||||||
context = {
|
context = {
|
||||||
'object': instance.event,
|
'object': instance.event,
|
||||||
'fonts': {
|
|
||||||
'opensans': {
|
|
||||||
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
|
|
||||||
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'receipt': True,
|
'receipt': True,
|
||||||
'current_user': False,
|
'current_user': False,
|
||||||
}
|
}
|
||||||
@@ -59,23 +54,23 @@ def send_eventauthorisation_success_email(instance):
|
|||||||
elif instance.event.organisation is not None and instance.email == instance.event.organisation.email:
|
elif instance.event.organisation is not None and instance.email == instance.event.organisation.email:
|
||||||
context['to_name'] = instance.event.organisation.name
|
context['to_name'] = instance.event.organisation.name
|
||||||
|
|
||||||
subject = "N%05d | %s - Event Authorised" % (instance.event.pk, instance.event.name)
|
subject = f"{instance.event.display_id} | {instance.event.name} - Event Authorised"
|
||||||
|
|
||||||
client_email = EmailMultiAlternatives(
|
client_email = EmailMultiAlternatives(
|
||||||
subject,
|
subject,
|
||||||
get_template("eventauthorisation_client_success.txt").render(context),
|
get_template("email/eventauthorisation_client_success.txt").render(context),
|
||||||
to=[instance.email],
|
to=[instance.email],
|
||||||
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
|
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
|
||||||
)
|
)
|
||||||
|
|
||||||
css = staticfiles_storage.path('css/email.css')
|
css = finders.find('css/email.css')
|
||||||
html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
|
html = Premailer(get_template("email/eventauthorisation_client_success.html").render(context),
|
||||||
external_styles=css).transform()
|
external_styles=css).transform()
|
||||||
client_email.attach_alternative(html, 'text/html')
|
client_email.attach_alternative(html, 'text/html')
|
||||||
|
|
||||||
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
|
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
|
||||||
|
|
||||||
client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
|
client_email.attach(f'{instance.event.display_id} - {escapedEventName} - CONFIRMATION.pdf',
|
||||||
merged.getvalue(),
|
merged.getvalue(),
|
||||||
'application/pdf'
|
'application/pdf'
|
||||||
)
|
)
|
||||||
@@ -87,7 +82,7 @@ def send_eventauthorisation_success_email(instance):
|
|||||||
|
|
||||||
mic_email = EmailMessage(
|
mic_email = EmailMessage(
|
||||||
subject,
|
subject,
|
||||||
get_template("eventauthorisation_mic_success.txt").render(context),
|
get_template("email/eventauthorisation_mic_success.txt").render(context),
|
||||||
to=[mic_email_address]
|
to=[mic_email_address]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,13 +116,13 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
|
|||||||
}
|
}
|
||||||
|
|
||||||
email = EmailMultiAlternatives(
|
email = EmailMultiAlternatives(
|
||||||
"%s new users awaiting approval on RIGS" % (context['number_of_users']),
|
f"{context['number_of_users']} new users awaiting approval on RIGS",
|
||||||
get_template("admin_awaiting_approval.txt").render(context),
|
get_template("email/admin_awaiting_approval.txt").render(context),
|
||||||
to=[admin.email],
|
to=[admin.email],
|
||||||
reply_to=[user.email],
|
reply_to=[user.email],
|
||||||
)
|
)
|
||||||
css = staticfiles_storage.path('css/email.css')
|
css = finders.find('css/email.css')
|
||||||
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
|
html = Premailer(get_template("email/admin_awaiting_approval.html").render(context),
|
||||||
external_styles=css).transform()
|
external_styles=css).transform()
|
||||||
email.attach_alternative(html, 'text/html')
|
email.attach_alternative(html, 'text/html')
|
||||||
email.send()
|
email.send()
|
||||||
@@ -138,3 +133,11 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
user_activated.connect(send_admin_awaiting_approval_email)
|
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,28 +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.4.5
|
|
||||||
* @author Adam Heim - https://github.com/truckingsim
|
|
||||||
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
|
|
||||||
* @copyright 2019 Adam Heim
|
|
||||||
* @license Released under the MIT license.
|
|
||||||
*
|
|
||||||
* Contributors:
|
|
||||||
* Mark Carver - https://github.com/markcarver
|
|
||||||
*
|
|
||||||
* Last build: 2019-04-23 12:18:56 PM EDT
|
|
||||||
*/
|
|
||||||
.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; }
|
|
||||||
|
|
||||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7OztFQWVFO0FBQ0Y7RUFDRSxtQkFBbUI7RUFDbkIsV0FBVztFQUNYLFdBQVc7RUFDWCxlQUFlO0VBQ2Ysa0JBQWtCO0VBQ2xCLGdCQUFnQjtFQUNoQixjQUFjO0VBQ2QsbUJBQW1CO0VBQ25CLGtCQUFrQixFQUFBIiwiZmlsZSI6ImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyIvKiFcbiAqIEFqYXggQm9vdHN0cmFwIFNlbGVjdFxuICpcbiAqIEV4dGVuZHMgZXhpc3RpbmcgW0Jvb3RzdHJhcCBTZWxlY3RdIGltcGxlbWVudGF0aW9ucyBieSBhZGRpbmcgdGhlIGFiaWxpdHkgdG8gc2VhcmNoIHZpYSBBSkFYIHJlcXVlc3RzIGFzIHlvdSB0eXBlLiBPcmlnaW5hbGx5IGZvciBDUk9TQ09OLlxuICpcbiAqIEB2ZXJzaW9uIDEuNC41XG4gKiBAYXV0aG9yIEFkYW0gSGVpbSAtIGh0dHBzOi8vZ2l0aHViLmNvbS90cnVja2luZ3NpbVxuICogQGxpbmsgaHR0cHM6Ly9naXRodWIuY29tL3RydWNraW5nc2ltL0FqYXgtQm9vdHN0cmFwLVNlbGVjdFxuICogQGNvcHlyaWdodCAyMDE5IEFkYW0gSGVpbVxuICogQGxpY2Vuc2UgUmVsZWFzZWQgdW5kZXIgdGhlIE1JVCBsaWNlbnNlLlxuICpcbiAqIENvbnRyaWJ1dG9yczpcbiAqICAgTWFyayBDYXJ2ZXIgLSBodHRwczovL2dpdGh1Yi5jb20vbWFya2NhcnZlclxuICpcbiAqIExhc3QgYnVpbGQ6IDIwMTktMDQtMjMgMTI6MTg6NTYgUE0gRURUXG4gKi9cbi5ib290c3RyYXAtc2VsZWN0IC5zdGF0dXMge1xuICBiYWNrZ3JvdW5kOiAjZjBmMGYwO1xuICBjbGVhcjogYm90aDtcbiAgY29sb3I6ICM5OTk7XG4gIGZvbnQtc2l6ZTogMTFweDtcbiAgZm9udC1zdHlsZTogaXRhbGljO1xuICBmb250LXdlaWdodDogNTAwO1xuICBsaW5lLWhlaWdodDogMTtcbiAgbWFyZ2luLWJvdHRvbTogLTVweDtcbiAgcGFkZGluZzogMTBweCAyMHB4O1xufVxuIl19 */
|
|
||||||
28
RIGS/static/css/ajax-bootstrap-select.min.css
vendored
@@ -1,28 +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.4.5
|
|
||||||
* @author Adam Heim - https://github.com/truckingsim
|
|
||||||
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
|
|
||||||
* @copyright 2019 Adam Heim
|
|
||||||
* @license Released under the MIT license.
|
|
||||||
*
|
|
||||||
* Contributors:
|
|
||||||
* Mark Carver - https://github.com/markcarver
|
|
||||||
*
|
|
||||||
* Last build: 2019-04-23 12:18:56 PM EDT
|
|
||||||
*/
|
|
||||||
.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; }
|
|
||||||
|
|
||||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5taW4uY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7RUFlRTtBQUFDO0VBQTBCLG1CQUFrQjtFQUFDLFdBQVU7RUFBQyxXQUFVO0VBQUMsZUFBYztFQUFDLGtCQUFpQjtFQUFDLGdCQUFlO0VBQUMsY0FBYTtFQUFDLG1CQUFrQjtFQUFDLGtCQUFpQixFQUFBIiwiZmlsZSI6ImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5taW4uY3NzIiwic291cmNlc0NvbnRlbnQiOlsiLyohXG4gKiBBamF4IEJvb3RzdHJhcCBTZWxlY3RcbiAqXG4gKiBFeHRlbmRzIGV4aXN0aW5nIFtCb290c3RyYXAgU2VsZWN0XSBpbXBsZW1lbnRhdGlvbnMgYnkgYWRkaW5nIHRoZSBhYmlsaXR5IHRvIHNlYXJjaCB2aWEgQUpBWCByZXF1ZXN0cyBhcyB5b3UgdHlwZS4gT3JpZ2luYWxseSBmb3IgQ1JPU0NPTi5cbiAqXG4gKiBAdmVyc2lvbiAxLjQuNVxuICogQGF1dGhvciBBZGFtIEhlaW0gLSBodHRwczovL2dpdGh1Yi5jb20vdHJ1Y2tpbmdzaW1cbiAqIEBsaW5rIGh0dHBzOi8vZ2l0aHViLmNvbS90cnVja2luZ3NpbS9BamF4LUJvb3RzdHJhcC1TZWxlY3RcbiAqIEBjb3B5cmlnaHQgMjAxOSBBZGFtIEhlaW1cbiAqIEBsaWNlbnNlIFJlbGVhc2VkIHVuZGVyIHRoZSBNSVQgbGljZW5zZS5cbiAqXG4gKiBDb250cmlidXRvcnM6XG4gKiAgIE1hcmsgQ2FydmVyIC0gaHR0cHM6Ly9naXRodWIuY29tL21hcmtjYXJ2ZXJcbiAqXG4gKiBMYXN0IGJ1aWxkOiAyMDE5LTA0LTIzIDEyOjE4OjU2IFBNIEVEVFxuICovLmJvb3RzdHJhcC1zZWxlY3QgLnN0YXR1c3tiYWNrZ3JvdW5kOiNmMGYwZjA7Y2xlYXI6Ym90aDtjb2xvcjojOTk5O2ZvbnQtc2l6ZToxMXB4O2ZvbnQtc3R5bGU6aXRhbGljO2ZvbnQtd2VpZ2h0OjUwMDtsaW5lLWhlaWdodDoxO21hcmdpbi1ib3R0b206LTVweDtwYWRkaW5nOjEwcHggMjBweH0iXX0= */
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
.autocomplete {
|
|
||||||
background: white;
|
|
||||||
z-index: 1000;
|
|
||||||
font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
overflow: auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 1px solid rgba(50, 50, 50, 0.6); }
|
|
||||||
|
|
||||||
.autocomplete * {
|
|
||||||
font: inherit; }
|
|
||||||
|
|
||||||
.autocomplete > div {
|
|
||||||
padding: 0 4px; }
|
|
||||||
|
|
||||||
.autocomplete .group {
|
|
||||||
background: #eee; }
|
|
||||||
|
|
||||||
.autocomplete > div:hover:not(.group),
|
|
||||||
.autocomplete > div.selected {
|
|
||||||
background: #81ca91;
|
|
||||||
cursor: pointer; }
|
|
||||||
|
|
||||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImF1dG9jb21wbGV0ZS5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0E7RUFDSSxpQkFBaUI7RUFDakIsYUFBYTtFQUNiLDRHQUE0RztFQUM1RyxjQUFjO0VBQ2Qsc0JBQXNCO0VBQ3RCLHVDQUF1QyxFQUFBOztBQUczQztFQUNJLGFBQWEsRUFBQTs7QUFHakI7RUFDSSxjQUFjLEVBQUE7O0FBR2xCO0VBQ0ksZ0JBQWdCLEVBQUE7O0FBR3BCOztFQUVJLG1CQUFtQjtFQUNuQixlQUFlLEVBQUEiLCJmaWxlIjoiYXV0b2NvbXBsZXRlLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIlxyXG4uYXV0b2NvbXBsZXRlIHtcclxuICAgIGJhY2tncm91bmQ6IHdoaXRlO1xyXG4gICAgei1pbmRleDogMTAwMDtcclxuICAgIGZvbnQ6IDE0cHgvMjJweCBcIi1hcHBsZS1zeXN0ZW1cIiwgQmxpbmtNYWNTeXN0ZW1Gb250LCBcIlNlZ29lIFVJXCIsIFJvYm90bywgXCJIZWx2ZXRpY2EgTmV1ZVwiLCBBcmlhbCwgc2Fucy1zZXJpZjtcclxuICAgIG92ZXJmbG93OiBhdXRvO1xyXG4gICAgYm94LXNpemluZzogYm9yZGVyLWJveDtcclxuICAgIGJvcmRlcjogMXB4IHNvbGlkIHJnYmEoNTAsIDUwLCA1MCwgMC42KTtcclxufVxyXG5cclxuLmF1dG9jb21wbGV0ZSAqIHtcclxuICAgIGZvbnQ6IGluaGVyaXQ7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgPiBkaXYge1xyXG4gICAgcGFkZGluZzogMCA0cHg7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgLmdyb3VwIHtcclxuICAgIGJhY2tncm91bmQ6ICNlZWU7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgPiBkaXY6aG92ZXI6bm90KC5ncm91cCksXHJcbi5hdXRvY29tcGxldGUgPiBkaXYuc2VsZWN0ZWQge1xyXG4gICAgYmFja2dyb3VuZDogIzgxY2E5MTtcclxuICAgIGN1cnNvcjogcG9pbnRlcjtcclxufVxyXG5cclxuIl19 */
|
|
||||||
248
RIGS/static/css/bootstrap-datetimepicker.min.css
vendored
455
RIGS/static/css/bootstrap-select.css
vendored
@@ -1,39 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: 0px; }
|
|
||||||
|
|
||||||
.main-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse; }
|
|
||||||
|
|
||||||
.client-header {
|
|
||||||
background-image: url("https://www.nottinghamtec.co.uk/imgs/wof2014-1.jpg");
|
|
||||||
background-color: #222;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 28px; }
|
|
||||||
.client-header .logos {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 640px; }
|
|
||||||
.client-header img {
|
|
||||||
height: 110px; }
|
|
||||||
|
|
||||||
.content-container {
|
|
||||||
width: 100%; }
|
|
||||||
.content-container .content {
|
|
||||||
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: left; }
|
|
||||||
.content-container .content .button-container {
|
|
||||||
width: 100%; }
|
|
||||||
.content-container .content .button-container .button {
|
|
||||||
padding: 6px 12px;
|
|
||||||
background-color: #357ebf;
|
|
||||||
border-radius: 4px; }
|
|
||||||
.content-container .content .button-container .button a {
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none; }
|
|
||||||
|
|
||||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImVtYWlsLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUE7RUFDRSxXQUFXLEVBQUE7O0FBR2I7RUFDRSxXQUFXO0VBQ1gseUJBQXlCLEVBQUE7O0FBSTNCO0VBQ0UsMkVBQTJFO0VBQzNFLHNCQUFzQjtFQUN0Qiw0QkFBNEI7RUFDNUIsMkJBQTJCO0VBRTNCLFdBQVc7RUFFWCxtQkFBbUIsRUFBQTtFQVJyQjtJQVdJLFdBQVc7SUFDWCxnQkFBZ0IsRUFBQTtFQVpwQjtJQWdCSSxhQUFhLEVBQUE7O0FBSWpCO0VBQ0UsV0FBVyxFQUFBO0VBRGI7SUFJSSx3RUFBd0U7SUFFeEUsV0FBVztJQUNYLGdCQUFnQjtJQUNoQixhQUFhO0lBQ2IsZ0JBQWdCLEVBQUE7SUFUcEI7TUFZTSxXQUFXLEVBQUE7TUFaakI7UUFlUSxpQkFBaUI7UUFDakIseUJBaERjO1FBaURkLGtCQUFrQixFQUFBO1FBakIxQjtVQW9CVSxXQUFXO1VBQ1gscUJBQXFCLEVBQUEiLCJmaWxlIjoiZW1haWwuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiJGJ1dHRvbl9jb2xvcjogIzM1N2ViZjtcblxuYm9keXtcbiAgbWFyZ2luOiAwcHg7XG59XG5cbi5tYWluLXRhYmxle1xuICB3aWR0aDogMTAwJTtcbiAgYm9yZGVyLWNvbGxhcHNlOiBjb2xsYXBzZTtcblxufVxuXG4uY2xpZW50LWhlYWRlciB7XG4gIGJhY2tncm91bmQtaW1hZ2U6IHVybChcImh0dHBzOi8vd3d3Lm5vdHRpbmdoYW10ZWMuY28udWsvaW1ncy93b2YyMDE0LTEuanBnXCIpO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMjIyO1xuICBiYWNrZ3JvdW5kLXJlcGVhdDogbm8tcmVwZWF0O1xuICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBjZW50ZXI7XG5cbiAgd2lkdGg6IDEwMCU7XG5cbiAgbWFyZ2luLWJvdHRvbTogMjhweDtcblxuICAubG9nb3N7XG4gICAgd2lkdGg6IDEwMCU7XG4gICAgbWF4LXdpZHRoOiA2NDBweDtcbiAgfVxuXG4gIGltZyB7XG4gICAgaGVpZ2h0OiAxMTBweDtcbiAgfVxufVxuXG4uY29udGVudC1jb250YWluZXJ7XG4gIHdpZHRoOiAxMDAlO1xuXG4gIC5jb250ZW50IHtcbiAgICBmb250LWZhbWlseTogXCJPcGVuIFNhbnNcIiwgXCJIZWx2ZXRpY2EgTmV1ZVwiLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmO1xuXG4gICAgd2lkdGg6IDEwMCU7XG4gICAgbWF4LXdpZHRoOiA2MDBweDtcbiAgICBwYWRkaW5nOiAxMHB4O1xuICAgIHRleHQtYWxpZ246IGxlZnQ7XG5cbiAgICAuYnV0dG9uLWNvbnRhaW5lcntcbiAgICAgIHdpZHRoOiAxMDAlO1xuXG4gICAgICAuYnV0dG9uIHtcbiAgICAgICAgcGFkZGluZzogNnB4IDEycHg7XG4gICAgICAgIGJhY2tncm91bmQtY29sb3I6ICRidXR0b25fY29sb3I7XG4gICAgICAgIGJvcmRlci1yYWRpdXM6IDRweDtcblxuICAgICAgICBhIHtcbiAgICAgICAgICBjb2xvcjogI2ZmZjtcbiAgICAgICAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7XG4gICAgICAgIH1cblxuICAgICAgfVxuXG4gICAgfVxuXG4gIH1cbn1cblxuIl19 */
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJpZS5jc3MiLCJzb3VyY2VzQ29udGVudCI6W119 */
|
|
||||||
BIN
RIGS/static/imgs/assets.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
RIGS/static/imgs/logo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
RIGS/static/imgs/logo.webp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 63 KiB |
BIN
RIGS/static/imgs/rigs.jpg
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
RIGS/static/imgs/tappytaptap.gif
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
RIGS/static/imgs/training.jpg
Normal file
|
After Width: | Height: | Size: 852 KiB |
BIN
RIGS/static/imgs/wof2014-1-small.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
@@ -1,6 +0,0 @@
|
|||||||
/*!
|
|
||||||
* Bootstrap alert.js v4.4.1 (https://getbootstrap.com/)
|
|
||||||
* Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
|
||||||
*/
|
|
||||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("jquery"),require("./util.js")):"function"==typeof define&&define.amd?define(["jquery","./util.js"],t):(e=e||self).Alert=t(e.jQuery,e.Util)}(this,(function(e,t){"use strict";function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}e=e&&e.hasOwnProperty("default")?e.default:e,t=t&&t.hasOwnProperty("default")?t.default:t;var r=e.fn.alert,o={CLOSE:"close.bs.alert",CLOSED:"closed.bs.alert",CLICK_DATA_API:"click.bs.alert.data-api"},i="alert",l="fade",s="show",a=function(){function r(e){this._element=e}var a,u,f,c=r.prototype;return c.close=function(e){var t=this._element;e&&(t=this._getRootElement(e)),this._triggerCloseEvent(t).isDefaultPrevented()||this._removeElement(t)},c.dispose=function(){e.removeData(this._element,"bs.alert"),this._element=null},c._getRootElement=function(n){var r=t.getSelectorFromElement(n),o=!1;return r&&(o=document.querySelector(r)),o||(o=e(n).closest("."+i)[0]),o},c._triggerCloseEvent=function(t){var n=e.Event(o.CLOSE);return e(t).trigger(n),n},c._removeElement=function(n){var r=this;if(e(n).removeClass(s),e(n).hasClass(l)){var o=t.getTransitionDurationFromElement(n);e(n).one(t.TRANSITION_END,(function(e){return r._destroyElement(n,e)})).emulateTransitionEnd(o)}else this._destroyElement(n)},c._destroyElement=function(t){e(t).detach().trigger(o.CLOSED).remove()},r._jQueryInterface=function(t){return this.each((function(){var n=e(this),o=n.data("bs.alert");o||(o=new r(this),n.data("bs.alert",o)),"close"===t&&o[t](this)}))},r._handleDismiss=function(e){return function(t){t&&t.preventDefault(),e.close(this)}},a=r,f=[{key:"VERSION",get:function(){return"4.4.1"}}],(u=null)&&n(a.prototype,u),f&&n(a,f),r}();return e(document).on(o.CLICK_DATA_API,'[data-dismiss="alert"]',a._handleDismiss(new a)),e.fn.alert=a._jQueryInterface,e.fn.alert.Constructor=a,e.fn.alert.noConflict=function(){return e.fn.alert=r,a._jQueryInterface},a}));
|
|
||||||
1
RIGS/static/js/asteroids.min.js
vendored
@@ -1 +0,0 @@
|
|||||||
$(document).ready((function(){function e(e){targetObject=$("#"+e.attr("id")+"-update"),update_url=$("option:selected",e).data("update_url"),""==update_url?targetObject.attr("disabled",!0):(targetObject.attr("href",update_url),targetObject.attr("disabled",!1))}clearSelectionLabel="(no selection)",$(".selectpicker").each((function(){var t={ajax:{url:$(this).data("sourceurl"),type:"GET",dataType:"json",data:{term:"{{{q}}}"}},locale:{emptyTitle:""},clearOnEmpty:!1,preprocessData:function(e){var t,a=e.length,l=[];if(l.push({text:clearSelectionLabel,value:"",data:{update_url:"",subtext:""}}),a)for(t=0;t<a;t++)l.push($.extend(!0,e[t],{text:e[t].label,value:e[t].pk,data:{update_url:e[t].update,subtext:""}}));return l}};$(this).prepend($("<option></option>").attr("value","").text(clearSelectionLabel).data("update_url","")),$(this).selectpicker().ajaxSelectPicker(t),$(this).change((function(){e($(this))})),e($(this))})),$("#modal").on("hide.bs.modal",(function(e){null!=modaltarget&&""!=modalobject&&function(e,t,a,l){e.find("option").remove(),e.append($("<option></option>").attr("value",t).text(a).data("update_url",l)),e.selectpicker("render"),e.selectpicker("refresh"),e.selectpicker("val",t),e.change()}($(modaltarget),modalobject[0].pk,modalobject[0].fields.name,modalobject[0].update_url)}))}));
|
|
||||||
1
RIGS/static/js/bootstrap-datetimepicker.js
vendored
7
RIGS/static/js/bootstrap-select.js
vendored
@@ -1 +0,0 @@
|
|||||||
function setupItemTable(t){objectitems=JSON.parse(t),$.each(objectitems,(function(t,e){objectitems[t]=JSON.parse(e)})),newitem=-1}function nl2br(t,e){return(t+"").replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g,"$1"+(e||void 0===e?"<br />":"<br>")+"$2")}function escapeHtml(t){return $("<div/>").text(t).html()}function updatePrices(){var t=0;for(var e in objectitems){var i=objectitems[e].fields,a=i.cost*i.quantity;$("#item-"+e+" .sub-total").html(parseFloat(a).toFixed(2)).data("subtotal",a),t+=Number(a)}$("#sumtotal").text(parseFloat(t).toFixed(2));var o=t*Number($("#vat-rate").data("rate"));$("#vat").text(parseFloat(o).toFixed(2)),$("#total").text(parseFloat(t+o).toFixed(2))}$("#item-table").on("click",".item-delete",(function(){delete objectitems[$(this).data("pk")],$("#item-"+$(this).data("pk")).remove(),updatePrices()})),$("#item-table").on("click",".item-add",(function(){$("#item-form").data("pk",newitem),$("#item_name").val(""),$("#item_description").val(""),$("#item_quantity").val(""),$("#item_cost").val(""),$($(this).data("target")).modal("show")})),$("#item-table").on("click",".item-edit",(function(){var t=$(this).data("pk");$("#item-form").data("pk",t);var e=objectitems[t].fields;$("#item_name").val(e.name),$("#item_description").val(e.description),$("#item_quantity").val(e.quantity),$("#item_cost").val(e.cost),$($(this).data("target")).modal("show")})),$("body").on("submit","#item-form",(function(t){t.preventDefault();var e,i=$(this).data("pk");if($("#itemModal").modal("hide"),i==newitem--){(e=new Object).name=$("#item_name").val(),e.description=$("#item_description").val(),e.cost=$("#item_cost").val(),e.quantity=$("#item_quantity").val();var a=0;for(item in objectitems)a++;e.order=a,objectitems[i]=new Object,objectitems[i].fields=e,$("#new-item-row").clone().attr("id","item-"+i).data("pk",i).appendTo("#item-table-body"),$("#item-"+i+" .item-delete, #item-"+i+" .item-edit").data("pk",i)}else(e=objectitems[i].fields).name=$("#item_name").val(),e.description=$("#item_description").val(),e.cost=$("#item_cost").val(),e.quantity=$("#item_quantity").val(),objectitems[i].fields=e;$row=$("#item-"+i),$row.find(".name").html(escapeHtml(e.name)),$row.find(".description").html(nl2br(escapeHtml(e.description))),$row.find(".cost").html(parseFloat(e.cost).toFixed(2)),$row.find(".quantity").html(e.quantity),updatePrices()})),$("body").on("submit",".itemised_form",(function(t){$("#id_items_json").val(JSON.stringify(objectitems))}));var fixHelper=function(t,e){return e.children().each((function(){$(this).width($(this).width())})),e};$("#item-table tbody").sortable({helper:fixHelper,update:function(t,e){info=$(this).sortable("toArray"),itemorder=new Array,$.each(info,(function(t,e){pk=$("#"+e).data("pk"),objectitems[pk].fields.order=t}))}});
|
|
||||||
186
RIGS/static/js/jquery-ui.js
vendored
25
RIGS/static/js/jquery.js
vendored
@@ -1 +0,0 @@
|
|||||||
var Konami=function(t){var e={addEvent:function(t,e,n,o){t.addEventListener?t.addEventListener(e,n,!1):t.attachEvent&&(t["e"+e+n]=n,t[e+n]=function(){t["e"+e+n](window.event,o)},t.attachEvent("on"+e,t[e+n]))},removeEvent:function(t,e,n){t.removeEventListener?t.removeEventListener(e,n):t.attachEvent&&t.detachEvent(e)},input:"",pattern:"38384040373937396665",keydownHandler:function(t,n){if(n&&(e=n),e.input+=t?t.keyCode:event.keyCode,e.input.length>e.pattern.length&&(e.input=e.input.substr(e.input.length-e.pattern.length)),e.input===e.pattern)return e.code(e._currentLink),e.input="",t.preventDefault(),!1},load:function(t){this._currentLink=t,this.addEvent(document,"keydown",this.keydownHandler,this),this.iphone.load(t)},unload:function(){this.removeEvent(document,"keydown",this.keydownHandler),this.iphone.unload()},code:function(t){window.location=t},iphone:{start_x:0,start_y:0,stop_x:0,stop_y:0,tap:!1,capture:!1,orig_keys:"",keys:["UP","UP","DOWN","DOWN","LEFT","RIGHT","LEFT","RIGHT","TAP","TAP"],input:[],code:function(t){e.code(t)},touchmoveHandler:function(t){if(1===t.touches.length&&!0===e.iphone.capture){var n=t.touches[0];e.iphone.stop_x=n.pageX,e.iphone.stop_y=n.pageY,e.iphone.tap=!1,e.iphone.capture=!1,e.iphone.check_direction()}},touchendHandler:function(){if(e.iphone.input.push(e.iphone.check_direction()),e.iphone.input.length>e.iphone.keys.length&&e.iphone.input.shift(),e.iphone.input.length===e.iphone.keys.length){for(var t=!0,n=0;n<e.iphone.keys.length;n++)e.iphone.input[n]!==e.iphone.keys[n]&&(t=!1);t&&e.iphone.code(e._currentLink)}},touchstartHandler:function(t){e.iphone.start_x=t.changedTouches[0].pageX,e.iphone.start_y=t.changedTouches[0].pageY,e.iphone.tap=!0,e.iphone.capture=!0},load:function(t){this.orig_keys=this.keys,e.addEvent(document,"touchmove",this.touchmoveHandler),e.addEvent(document,"touchend",this.touchendHandler,!1),e.addEvent(document,"touchstart",this.touchstartHandler)},unload:function(){e.removeEvent(document,"touchmove",this.touchmoveHandler),e.removeEvent(document,"touchend",this.touchendHandler),e.removeEvent(document,"touchstart",this.touchstartHandler)},check_direction:function(){return x_magnitude=Math.abs(this.start_x-this.stop_x),y_magnitude=Math.abs(this.start_y-this.stop_y),x=this.start_x-this.stop_x<0?"RIGHT":"LEFT",y=this.start_y-this.stop_y<0?"DOWN":"UP",result=x_magnitude>y_magnitude?x:y,result=!0===this.tap?"TAP":result,result}}};return"string"==typeof t&&e.load(t),"function"==typeof t&&(e.code=t,e.load()),e};"undefined"!=typeof module&&void 0!==module.exports?module.exports=Konami:"function"==typeof define&&define.amd?define([],(function(){return Konami})):window.Konami=Konami;
|
|
||||||
6
RIGS/static/js/marked.min.js
vendored
Normal file
@@ -1,6 +0,0 @@
|
|||||||
/*!
|
|
||||||
* Bootstrap popover.js v4.4.1 (https://getbootstrap.com/)
|
|
||||||
* Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
|
||||||
*/
|
|
||||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("jquery"),require("./tooltip.js")):"function"==typeof define&&define.amd?define(["jquery","./tooltip.js"],t):(e=e||self).Popover=t(e.jQuery,e.Tooltip)}(this,(function(e,t){"use strict";function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?o(Object(n),!0).forEach((function(t){r(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):o(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}e=e&&e.hasOwnProperty("default")?e.default:e,t=t&&t.hasOwnProperty("default")?t.default:t;var u="popover",s=".bs.popover",c=e.fn[u],p=new RegExp("(^|\\s)bs-popover\\S+","g"),f=i({},t.Default,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'}),a=i({},t.DefaultType,{content:"(string|element|function)"}),l="fade",h="show",y=".popover-header",d=".popover-body",v={HIDE:"hide"+s,HIDDEN:"hidden"+s,SHOW:"show"+s,SHOWN:"shown"+s,INSERTED:"inserted"+s,CLICK:"click"+s,FOCUSIN:"focusin"+s,FOCUSOUT:"focusout"+s,MOUSEENTER:"mouseenter"+s,MOUSELEAVE:"mouseleave"+s},g=function(t){var r,o;function i(){return t.apply(this,arguments)||this}o=t,(r=i).prototype=Object.create(o.prototype),r.prototype.constructor=r,r.__proto__=o;var c,g,b,O=i.prototype;return O.isWithContent=function(){return this.getTitle()||this._getContent()},O.addAttachmentClass=function(t){e(this.getTipElement()).addClass("bs-popover-"+t)},O.getTipElement=function(){return this.tip=this.tip||e(this.config.template)[0],this.tip},O.setContent=function(){var t=e(this.getTipElement());this.setElementContent(t.find(y),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(t.find(d),n),t.removeClass(l+" "+h)},O._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},O._cleanTipClass=function(){var t=e(this.getTipElement()),n=t.attr("class").match(p);null!==n&&n.length>0&&t.removeClass(n.join(""))},i._jQueryInterface=function(t){return this.each((function(){var n=e(this).data("bs.popover"),r="object"==typeof t?t:null;if((n||!/dispose|hide/.test(t))&&(n||(n=new i(this,r),e(this).data("bs.popover",n)),"string"==typeof t)){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},c=i,b=[{key:"VERSION",get:function(){return"4.4.1"}},{key:"Default",get:function(){return f}},{key:"NAME",get:function(){return u}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return v}},{key:"EVENT_KEY",get:function(){return s}},{key:"DefaultType",get:function(){return a}}],(g=null)&&n(c.prototype,g),b&&n(c,b),i}(t);return e.fn[u]=g._jQueryInterface,e.fn[u].Constructor=g,e.fn[u].noConflict=function(){return e.fn[u]=c,g._jQueryInterface},g}));
|
|
||||||