Compare commits

..

114 Commits

Author SHA1 Message Date
043c352fba Rip out some more useless stuff 2020-05-27 18:35:40 -04:00
99255e0e93 Flip Django localization back on
Plz don't explode...
2020-05-27 18:28:01 -04:00
258aa81778 Maybe this one... 2020-05-27 18:18:44 -04:00
5a101e554c ?! 2020-05-27 18:09:34 -04:00
dd654cff22 Another attempt at forcing the right locale on CI 2020-05-27 18:01:55 -04:00
e1fd466f85 Print statements it is... 2020-05-27 16:38:48 -04:00
edab48cab1 Try to set Travis's TZ to London, instead 2020-05-27 16:31:29 -04:00
5c633c7fff Force 12hr time on CI
Hmm...
2020-05-27 15:51:11 -04:00
26f48c7ef4 Cleanup testing 2020-05-27 15:48:56 -04:00
f69365a0fb Add 12hour time support to time/datetime POM regions
>_>
2020-05-27 15:46:04 -04:00
c065148979 TEST: Disable L10N flag 2020-05-27 13:40:40 -04:00
af5e5ee83f Explicitly set default format on time widgets, python side 2020-05-27 19:31:50 +02:00
207cf0aefe Make asset create test scroll the page
For a more useful screenshot...
2020-05-27 19:00:07 +02:00
0fd2fae8c3 Explicitly set step param on time fields 2020-05-27 18:59:45 +02:00
50d0d746f4 Revert changes 2020-05-27 18:52:03 +02:00
7cd4b46c92 Try overriding localization at template level 2020-05-27 18:29:12 +02:00
4c403d67b3 TEST: Disallow local datetime input formats 2020-05-27 18:15:04 +02:00
c1d13d2802 TEST: Disallow %H:%M:%S input format 2020-05-27 17:57:31 +02:00
Matthew Smith
4eab50f4f3 Forgot to add "sys" to imports 2020-05-27 02:59:53 +01:00
Matthew Smith
ce69727f7b Maybe this'll make it appear? 2020-05-27 02:54:14 +01:00
Matthew Smith
c616c8dbe5 Okay so what locale is the CI server on 2020-05-27 02:47:12 +01:00
Matthew Smith
a0f571c783 Added colon to input 2020-05-27 02:16:02 +01:00
Matthew Smith
170c2f6d8c Fixed Pep8 - I promise I'll make a pre-commit hook sometime! 2020-05-27 01:59:18 +01:00
Matthew Smith
f37228e058 Fixed RIGS tests not being run 2020-05-27 01:53:14 +01:00
Matthew Smith
6d47be72fe Added screenshot recording of test failures 2020-05-27 01:47:10 +01:00
6ded711dd5 Try disabling chrome cache 2020-05-26 15:26:03 +01:00
920ea0d058 What about if I did this 2020-05-26 15:10:27 +01:00
e45e58321c Revert to old submit wait behaviour 2020-05-26 15:02:44 +01:00
3f78b9f05f That was even less clever of me 2020-05-26 14:39:12 +01:00
0bbc23853d Well that wasn't clever of me 2020-05-26 13:42:37 +01:00
7356d020b2 Minor test futzing 2020-05-26 13:35:50 +01:00
08f40bce9e Update python version
Trying to get CI to match my local environment as much as possible...
2020-05-26 01:39:15 +01:00
1d0e8e14e5 Of course | is part of YAML syntax, of course...
Maybe this works.
2020-05-26 01:23:38 +01:00
b52709f412 Make CI use latest (stable) chromedriver rather than some ancient one
Since Travis uses the latest stable chrome, should always match. Bash oneliner \o/
2020-05-26 00:52:42 +01:00
c4c4291050 Fix asset creation test + actually verify its results 2020-05-26 00:22:06 +01:00
07641a2520 Ensure submit button is scrolled to in tests 2020-05-26 00:17:02 +01:00
fd6aee83cf Fix the remaining tests 2020-05-25 22:49:07 +01:00
177c37ffbc Dedupe generic search logic 2020-05-25 22:47:25 +01:00
d070f97696 Fully replace test_functional 2020-05-25 16:16:38 +01:00
2f7389d8bb Really ought to get a pre-commit hook for pep8... 2020-05-25 13:40:53 +01:00
18fde7c16a FIX: Don't show asset buttons/history for basic users 2020-05-25 13:36:42 +01:00
f06dc56b40 Refactor calendar tests 2020-05-25 13:32:09 +01:00
e6d06db2a1 FIX: Prevent setting access time after start time
Cherry pick of d274ea4606. Will close #405.
2020-05-25 01:16:26 +01:00
544b6df35c Fix some tests
And some things that were actually borked
2020-05-24 22:16:50 +01:00
c9ea1bb75d pep8 2020-05-24 19:24:51 +01:00
928d5cd8e6 Deduplication of testing code 2020-05-24 19:16:09 +01:00
cc225b2eb7 Refactor date validation test
So close to killing test_functional.EventTest!
2020-05-24 00:27:41 +01:00
3c0005ddb0 Initial port of duplicate testing
Needs the latter half rewriting once we have an EventDetail POM
2020-05-23 23:48:11 +01:00
Matthew Smith
8ffb5ab23e Updated bootstrap-select for BS4 2020-05-22 00:38:04 +01:00
34bf49876b Upgrade bootstrap-select 2020-05-21 22:57:28 +01:00
813db2c474 Initial refactor of event item testing 2020-05-21 22:48:46 +01:00
919975e1ba Fix event item adding
Bit too heavy handed with the deduplication there Arona
2020-05-21 22:18:04 +01:00
0d0c783e07 Desaturate theme colors even more
Much closer to BS3
2020-05-21 21:26:56 +01:00
c23d18cd45 Init other tests, more rigs test faffery 2020-05-21 21:19:53 +01:00
5a3547ea74 Initial work on event create test reimpl 2020-05-21 02:40:14 +01:00
e3c1da9d13 Start on new tests 2020-05-11 23:04:13 +01:00
3f48c51aeb Fix migrations
TODO - need to ensure moved models are *moved* rather than deleted and recreated!
2020-04-15 19:42:42 +01:00
abb8dc25ec Test Refactor Part 1 - Shuffle things around 2020-04-15 19:40:49 +01:00
70995a0d0b Merge branch 'master' into bs4
# Conflicts:
#	assets/models.py
#	assets/templates/asset_update.html
#	assets/templates/partials/asset_buttons.html
#	assets/templates/partials/asset_list_table_body.html
#	assets/views.py
#	templates/base_assets.html
2020-04-15 16:28:44 +01:00
5e60675115 Python Format/import opt 2020-04-12 20:10:35 +01:00
f308a095f3 Breakout (most) user stuff to separate module
The model remains in RIGS for now, as it's pretty painful to move...
2020-04-12 19:22:03 +01:00
2bf643cd7a Use the right autocompleter.js... 2020-04-12 18:47:09 +01:00
c8d0c0d5d0 Asset form fixes 2020-04-06 15:17:14 +01:00
176324ed79 Tweak versioning templates to allow ID overrides
Asset specific templates begone. Still need to bring back the ID formatting for the Rigboard.
2020-04-06 14:59:10 +01:00
d216dd4c74 Versioning template improvements
//TODO Rather than have seperate asset templates, convert 'id' into a template variable
2020-04-06 03:15:43 +01:00
55e37d8c69 Tweak asset list to be more in line with other lists 2020-04-06 02:50:36 +01:00
7aa19cc7ab Asset list table improvements 2020-04-06 02:43:20 +01:00
4c40226bcf Fixed fullcalendar print css not included 2020-04-06 02:43:20 +01:00
d351d9eb7b Item table fixes 2020-04-06 02:43:19 +01:00
de210caa36 First pass at mobile-ising the generic list 2020-03-25 00:51:03 +00:00
0271840f4d Improvements to event table mobile 2020-03-24 22:54:35 +00:00
7a08f2d889 Tweak some colours to be a bit less OTT
I need to work out if I can seperate background and primary colours like BS3 did
2020-03-24 22:54:34 +00:00
ee7ba3ea19 Improvements to generic lists 2020-03-22 12:47:32 +00:00
41b0387e49 Reduced overall font size a touch 2020-03-22 12:15:55 +00:00
50ca782569 Start reworking invoice things 2020-03-18 18:38:37 +00:00
0144bd37fc Start to seperate versioning into its own app 2020-03-18 17:36:09 +00:00
959097286c Stop the navbar from sticking to the top of screen 2020-03-18 15:18:50 +00:00
264b306b2f Add a skip link for keyboard users
Someone might use it...
2020-03-18 15:07:29 +00:00
e42989637e Ditch custom loading in favour of BS4 inbuilt
It's less pretty/cool...I may bring it back in future but this'll do for now.
2020-03-18 01:46:13 +00:00
12d8b46f86 First pass at reworked rigboard layout on mobile 2020-03-18 01:39:21 +00:00
89ddb09459 Port (most) RIGS urls to path() 2020-03-18 00:40:45 +00:00
f4f8c6b417 Delete obsolete code 2020-03-18 00:30:23 +00:00
36556dea33 Add the usual set of fields to supplier and update form accordingly 2020-03-17 20:07:14 +00:00
b9d318e675 Whitelist ins/del tags in activity popovers 2020-03-17 18:41:26 +00:00
b8931a64c8 Remove unused dependencies 2020-03-16 23:27:25 +00:00
cfe0a264e7 Pagination fixes 2020-03-16 23:24:03 +00:00
8fbe9f9026 Bring colours back to the rigboard 2020-03-16 23:12:21 +00:00
fe0e4063d7 Work on event archive template 2020-03-16 22:47:38 +00:00
0bd4b281d1 Convert some urls to path 2020-03-16 22:47:26 +00:00
7de778a57e Work on event creation form 2020-03-16 22:35:59 +00:00
88b34740f6 Asset list/CRUD template updates 2020-03-16 21:45:04 +00:00
e0e4e8d11e pycodestyle 2020-03-16 21:19:09 +00:00
c1d277be9c Authorisation template work 2020-03-15 23:08:49 +00:00
4a71dd0d95 AssetList template update 2020-03-15 21:33:05 +00:00
2bfecb9c0f Improvements to generic list + use for supplier 2020-03-15 18:38:51 +00:00
3814f5abfc Generic list template 2020-03-15 18:25:05 +00:00
4c34e4e43e Reimpl custom css - Open Sans is back! 2020-03-15 17:14:12 +00:00
3f36f66b8a Fix up header search 2020-03-15 15:19:39 +00:00
7cef4d03c0 Contain the navbar 2020-03-15 13:48:30 +00:00
6970c5c490 Activity feed updates 2020-03-15 10:33:08 +00:00
b8ea3d3d42 Dependency update 2020-03-15 10:10:39 +00:00
366a14408b More messing with the rigboard 2020-03-15 03:10:33 +00:00
270b1fc5bb Various template changes 2020-03-15 02:53:12 +00:00
7786512dc2 Inital work on adding development livereload 2020-03-15 01:35:00 +00:00
88ac1b93ae Re-enable calendar 2020-03-10 18:03:05 +00:00
4d845309c9 btn-default -> btn-secondary 2020-03-10 17:20:13 +00:00
3b8789e49e More refactoring of rigboard 2020-03-10 16:46:53 +00:00
1526a2f22b Start to move search bar into header
Broken because my scripts are all over the place
2020-03-10 14:33:50 +00:00
fe71f7640f Migrate user urls to PyRIGS module 2020-03-10 14:08:47 +00:00
b57716f7fc Port from glyphicons to Font Awesome
Mmmm pretty
2020-03-09 23:18:56 +00:00
320c43e472 Initial refactor of rigboard template 2020-03-09 22:44:54 +00:00
1e5fcbdba0 Mostly reimplement rigboard navbar 2020-03-09 22:36:22 +00:00
1df1784d02 Move rigboard templates up a level
Also:
- Find and replace panel -> card
- Some base template work
- gulpfile things, added dep on django-gulp
2020-03-09 21:44:09 +00:00
6c72f070f2 Begin moving to NPM/Gulp based build system
There are too many options.
2020-03-07 23:00:45 +00:00
396 changed files with 26738 additions and 36160 deletions

32
.codeclimate.yml Normal file
View File

@@ -0,0 +1,32 @@
---
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/

View File

@@ -1,5 +1,6 @@
[run] [run]
omit = */migrations/* source =
*/tests/* ./
*/site-packages/*
*/distutils/* omit =
*/migrations/*

2
.csslintrc Normal file
View File

@@ -0,0 +1,2 @@
--exclude-exts=.min.css
--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes

1
.eslintignore Normal file
View File

@@ -0,0 +1 @@
**/*{.,-}min.js

213
.eslintrc Normal file
View File

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

View File

@@ -1,151 +0,0 @@
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
});

View File

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

View File

@@ -1,53 +0,0 @@
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
View File

@@ -26,7 +26,6 @@ var/
.installed.cfg .installed.cfg
*.egg *.egg
node_modules/ node_modules/
data/
# Continer extras # Continer extras
.vagrant .vagrant
@@ -69,9 +68,19 @@ target/
## Directory-based project format: ## Directory-based project format:
.idea/ .idea/
# if you remove the above rule, at least ignore the following:
#Built dependencies # User-specific stuff:
pipeline/built_assets # .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle: # Gradle:
# .idea/gradle.xml # .idea/gradle.xml
@@ -100,4 +109,5 @@ com_crashlytics_export_strings.xml
crashlytics.properties crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
.vscode/ .vscode/
screenshots/ /package-lock.json
screenshots/

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
PyRIGS

5
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?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 Normal file
View File

@@ -0,0 +1,5 @@
<component name="DependencyValidationManager">
<state>
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
</state>
</component>

7
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

1156
.rubocop.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
*.sqlite3 *.sqlite3
*.scss
*.md *.md
**/tests *.rb
conftest.py Vagrantfile
pytest.ini config/vagrant/*
Dockerfile config/vagrant.yml

37
.travis.yml Normal file
View File

@@ -0,0 +1,37 @@
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 Normal file
View File

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

104
Pipfile
View File

@@ -1,104 +0,0 @@
[[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

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from RIGS import models from RIGS import models
@@ -9,13 +8,18 @@ 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'] = f"{request.scheme}://{request.META['HTTP_HOST']}{reverse(oembed_view, kwargs=kwargs)}" context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'],
context['login_url'] = f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}" reverse(oembed_view, kwargs=kwargs))
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
resp = render(request, 'login_redirect.html', context=context) resp = render(request, 'login_redirect.html', context=context)
return resp return resp
def has_oembed(oembed_view, login_url=settings.LOGIN_URL): def has_oembed(oembed_view, login_url=None):
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:
@@ -24,7 +28,7 @@ def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
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(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}') return HttpResponseRedirect('%s?%s=%s' % (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__
@@ -54,7 +58,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(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}') return HttpResponseRedirect('%s?%s=%s' % (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

View File

@@ -8,23 +8,25 @@ 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/
""" """
import datetime # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
from pathlib import Path import os
import raven
import secrets import secrets
import datetime
import sentry_sdk BASE_DIR = os.path.dirname(os.path.dirname(__file__))
from sentry_sdk.integrations.django import DjangoIntegration
from envparse import env
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Quick-start development settings - unsuitable for production
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e') SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get(
'SECRET_KEY') else 'gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e'
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG', cast=bool, default=True) DEBUG = bool(int(os.environ.get('DEBUG'))) if os.environ.get('DEBUG') else True
STAGING = env('STAGING', cast=bool, default=False)
CI = env('CI', cast=bool, default=False) STAGING = bool(int(os.environ.get('STAGING'))) if os.environ.get('STAGING') else False
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com'] ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
@@ -42,15 +44,13 @@ if not DEBUG:
INTERNAL_IPS = ['127.0.0.1'] INTERNAL_IPS = ['127.0.0.1']
DOMAIN = env('DOMAIN', default='example.com') ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
('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,20 +62,19 @@ INSTALLED_APPS = (
'users', 'users',
'RIGS', 'RIGS',
'assets', 'assets',
'training',
# 'debug_toolbar', 'debug_toolbar',
'registration', 'registration',
'reversion', 'reversion',
'captcha',
'widget_tweaks', 'widget_tweaks',
'hcaptcha', 'raven.contrib.django.raven_compat',
'massadmin',
) )
MIDDLEWARE = ( MIDDLEWARE = (
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware',
# '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',
@@ -83,8 +82,6 @@ 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'
@@ -92,10 +89,11 @@ 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': str(BASE_DIR / 'db.sqlite3'), 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
} }
} }
@@ -152,33 +150,12 @@ LOGGING = {
} }
} }
# Tests lock up SQLite otherwise RAVEN_CONFIG = {
if STAGING or CI: 'dsn': os.environ.get('RAVEN_DSN'),
CACHES = { # If you are using git, you can also automatically configure the
'default': { # release based on the git info.
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache' # 'release': raven.fetch_git_sha(os.path.dirname(os.path.dirname(__file__))),
} }
}
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'
@@ -189,21 +166,26 @@ LOGOUT_URL = '/user/logout/'
ACCOUNT_ACTIVATION_DAYS = 7 ACCOUNT_ACTIVATION_DAYS = 7
# CAPTCHA settings # reCAPTCHA settings
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001') RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY',
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000') "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY',
"6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
NOCAPTCHA = True
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 = env('EMAIL_HOST') EMAIL_HOST = os.environ.get('EMAIL_HOST')
EMAIL_PORT = env('EMAIL_PORT', cast=int, default=25) EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 25))
EMAIL_HOST_USER = env('EMAIL_HOST_USER') EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD') EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
EMAIL_USE_TLS = env('EMAIL_USE_TLS', cast=bool, default=False) EMAIL_USE_TLS = bool(int(os.environ.get('EMAIL_USE_TLS', 0)))
EMAIL_USE_SSL = env('EMAIL_USE_SSL', cast=bool, default=False) EMAIL_USE_SSL = bool(int(os.environ.get('EMAIL_USE_SSL', 0)))
DEFAULT_FROM_EMAIL = env('EMAIL_FROM') DEFAULT_FROM_EMAIL = os.environ.get('EMAIL_FROM')
else: else:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
@@ -218,6 +200,8 @@ 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
@@ -226,18 +210,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 = 'whitenoise.storage.CompressedManifestStaticFilesStorage' STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = str(BASE_DIR / 'static/') STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
STATICFILES_DIRS = [ STATIC_DIRS = (
str(BASE_DIR / 'pipeline/built_assets'), os.path.join(BASE_DIR, 'static/')
] )
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ 'DIRS': [
BASE_DIR / 'templates' os.path.join(BASE_DIR, 'templates')
], ],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
@@ -260,12 +244,10 @@ 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)
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' IMGUR_UPLOAD_CLIENT_ID = os.environ.get('IMGUR_UPLOAD_CLIENT_ID', '')
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

View File

@@ -1,30 +1,33 @@
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
from pytest_django.asserts import assertContains import pytz
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, minute): def create_datetime(year, month, day, hour, min):
tz = pytz.timezone(settings.TIME_ZONE) tz = pytz.timezone(settings.TIME_ZONE)
return tz.localize(datetime(year, month, day, hour, minute)).astimezone(tz) return tz.localize(datetime(year, month, day, hour, min)).astimezone(pytz.utc)
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")
options.add_argument("--headless") # No caching, please and thank you
if settings.CI: options.add_argument("--aggressive-cache-discard")
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
@@ -34,7 +37,6 @@ 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()
@@ -48,11 +50,10 @@ 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()
login_page = pages.LoginPage(self.driver, self.live_server_url).open() loginPage = pages.LoginPage(self.driver, self.live_server_url).open()
login_page.login("EventTest", "EventTestPassword") loginPage.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:
@@ -63,9 +64,20 @@ 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)
raise e
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
return wrapper_func return wrapper_func
@@ -76,30 +88,15 @@ def screenshot_failure_cls(cls):
return cls return cls
def assert_times_almost_equal(first_time, second_time): # Checks if animation is done
assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0) class animation_is_finished(object):
def __init__(self):
pass
def __call__(self, driver):
def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url): numberAnimating = driver.execute_script('return $(":animated").length')
# Test the meta tag is in place finished = numberAnimating == 0
response = client.get(event_url, follow=True, HTTP_HOST='example.com') if finished:
assertContains(response, 'application/json+oembed') import time
assertContains(response, oembed_url) time.sleep(0.1)
# Test that the JSON exists return finished
response = client.get(oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assertContains(response, event_embed_url)
# Should also work for non-existant events
response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assertContains(response, alt_event_embed_url)
def login(client, django_user_model):
pwd = 'testuser'
usr = 'TestUser'
user = django_user_model.objects.create_user(username=usr, email="TestUser@test.com", password=pwd,
is_superuser=True,
is_active=True, is_staff=True)
assert client.login(username=usr, password=pwd)
return user

View File

@@ -1,8 +1,8 @@
from pypom import Page from pypom import Page, Region
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,7 +31,6 @@ 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(
@@ -44,7 +43,6 @@ 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
@@ -74,13 +72,3 @@ 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

View File

@@ -1,12 +1,12 @@
import datetime
from django.conf import settings
from pypom import Region from pypom import Region
from selenium.common.exceptions import NoSuchElementException from django.utils import timezone
from selenium.webdriver.common.by import By 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 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.support.select import Select
from selenium.webdriver.common.keys import Keys
import datetime
def parse_bool_from_string(string): def parse_bool_from_string(string):
@@ -18,22 +18,18 @@ 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 settings.CI: # The CI is American # If system is 12hr
time_format = "%I%M%p" if timezone.now().strftime("%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')
@@ -71,11 +67,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
@@ -84,7 +80,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 = [x for x in self.options if x.name == name] options = list((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)
@@ -117,15 +113,6 @@ 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()
@@ -139,22 +126,6 @@ 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):
@@ -162,13 +133,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(get_date_format())) self.root.send_keys(value.strftime("%d%m%Y"))
class TimePicker(Region): class TimePicker(Region):
@property @property
def value(self): def value(self):
return datetime.datetime.strptime(self.root.get_attribute("value"), "%H:%M") return datetime.datetime.strptime(self.root.get_attribute("value"), get_time_format())
def set_value(self, value): def set_value(self, value):
self.root.clear() self.root.clear()
@@ -178,12 +149,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 %H:%M") return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d " + get_time_format())
def set_value(self, value): def set_value(self, value):
self.root.clear() self.root.clear()
date = value.date().strftime(get_date_format()) date = value.date().strftime("%d%m%Y")
time = value.time().strftime(get_time_format()) time = value.time().strftime(get_time_format())
self.root.send_keys(date) self.root.send_keys(date)
@@ -228,7 +199,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, 'h4') _header_selector = (By.TAG_NAME, 'h3')
form_items = { form_items = {
'name': (TextBox, (By.ID, 'id_name')) 'name': (TextBox, (By.ID, 'id_name'))

View File

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

View File

@@ -1,20 +1,21 @@
from django.conf import settings
from django.conf.urls import include
from django.contrib import admin
from django.contrib.auth.decorators import login_required
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path from django.urls import path
from django.views.generic import TemplateView 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.views.decorators.clickjacking import xframe_options_exempt
from django.contrib.auth.views import LoginView
from registration.backends.default.views import RegistrationView
from PyRIGS.decorators import permission_required_with_403
import RIGS
import users
from PyRIGS import views from PyRIGS import views
urlpatterns = [ urlpatterns = [
path('', include('versioning.urls')), path('', include('users.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()),
@@ -22,22 +23,13 @@ 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 = [
path('__debug__/', include(debug_toolbar.urls)), url(r'^__debug__/', include(debug_toolbar.urls)),
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")), ] + urlpatterns
]

View File

@@ -1,55 +1,24 @@
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.db.models import Q from django.http.response import HttpResponseRedirect
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse
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.views.decorators.clickjacking import xframe_options_exempt from django.contrib.auth.views import LoginView
from django.template.loader import get_template from django.db.models import Q
from django.utils import timezone from django.shortcuts import get_object_or_404
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 from RIGS import models, forms
from assets import models as asset_models from assets import models as asset_models
from training import models as training_models from functools import reduce
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):
@@ -59,9 +28,7 @@ class SecureAPIRequest(generic.View):
'organisation': models.Organisation, 'organisation': models.Organisation,
'profile': models.Profile, 'profile': models.Profile,
'event': models.Event, 'event': models.Event,
'asset': asset_models.Asset, 'supplier': asset_models.Supplier
'supplier': asset_models.Supplier,
'training_item': training_models.TrainingItem,
} }
perms = { perms = {
@@ -70,9 +37,7 @@ class SecureAPIRequest(generic.View):
'organisation': 'RIGS.view_organisation', 'organisation': 'RIGS.view_organisation',
'profile': 'RIGS.view_profile', 'profile': 'RIGS.view_profile',
'event': None, 'event': None,
'asset': None, 'supplier': None
'supplier': None,
'training_item': None,
} }
''' '''
@@ -100,9 +65,6 @@ 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:
@@ -123,35 +85,27 @@ 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': name, 'text': o.name,
} }
try: # See if there is a valid update URL try: # See if there is a valid update URL
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk}) data['update'] = reverse("%s_update" % model, kwargs={'pk': o.pk})
except NoReverseMatch: except NoReverseMatch:
pass pass
results.append(data) results.append(data)
# return a data response # return a data response
return JsonResponse(results, safe=False) json = simplejson.dumps(results)
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)
@@ -176,210 +130,32 @@ class SecureAPIRequest(generic.View):
} }
results.append(data) results.append(data)
return JsonResponse(results, safe=False) json = simplejson.dumps(results)
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):
object_list = self.model.objects.search(query=self.request.GET.get('q', "")) q = self.request.GET.get('q', "")
filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(
phone__startswith=q) | Q(phone__endswith=q)
# try and parse an int
try:
val = int(q)
filter = filter | Q(pk=val)
except: # noqa
# not an integer
pass
object_list = self.model.objects.filter(filter)
orderBy = self.request.GET.get('orderBy', "name") 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
View File

@@ -1,19 +1,111 @@
# TEC PA & Lighting - PyRIGS # # TEC PA & Lighting - PyRIGS #
![Build Status](https://github.com/nottinghamtec/PyRIGS/workflows/Django%20CI/badge.svg) [![Build Status](https://travis-ci.org/nottinghamtec/PyRIGS.svg)](https://travis-ci.org/nottinghamtec/PyRIGS)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS) [![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS)
[![Maintainability](https://api.codeclimate.com/v1/badges/79ca3b8106911a1d143f/maintainability)](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability)
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. 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.
For setup information and other such helpful stuff check the [Wiki](https://github.com/nottinghamtec/PyRIGS/wiki) 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.
# Apps ### What is this repository for? ###
- PyRIGS: Base app, stores 'global' information When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production.
- 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
```
[![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com)

View File

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

View File

@@ -1,120 +1,69 @@
from django.contrib import admin from django.contrib import admin
from django.contrib import messages from RIGS import models, forms
from django.contrib.admin import helpers from users import forms as user_forms
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import Count
from django.forms import ModelForm
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db import IntegrityError
from reversion import revisions as reversion
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from RIGS import models from django.contrib.admin import helpers
from users import forms as user_forms from django.template.response import TemplateResponse
from django.contrib import messages
from django.db import transaction
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count
from django.forms import ModelForm
from reversion import revisions as reversion
# Register your models here.
admin.site.register(models.VatRate, 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, VersionAdmin) admin.site.register(models.Invoice)
admin.site.register(models.EventCheckIn) admin.site.register(models.Payment)
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary. def approve_user(modeladmin, request, queryset):
def merge_model_instances(primary_object, alias_objects): queryset.update(is_approved=True)
"""
Merge several model instances into one, the `primary_object`.
Use this function to merge model objects and migrate all of the related
fields from the alias objects the primary object.
"""
# get related fields
related_fields = list(filter(
lambda x: x.is_relation is True,
primary_object._meta.get_fields()))
many_to_many_fields = list(filter( approve_user.short_description = "Approve selected users"
lambda x: x.many_to_many is True, related_fields))
related_fields = list(filter(
lambda x: x.many_to_many is False, related_fields))
# Loop through all alias objects and migrate their references to the @admin.register(models.Profile)
# primary object class ProfileAdmin(UserAdmin):
deleted_objects = [] # Don't know how to add 'is_approved' whilst preserving the default list...
deleted_objects_count = 0 list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
for alias_object in alias_objects: fieldsets = (
# Migrate all foreign key references from alias object to primary (None, {'fields': ('username', 'password')}),
# object. (_('Personal info'), {
for many_to_many_field in many_to_many_fields: 'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
alias_varname = many_to_many_field.name (_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
related_objects = getattr(alias_object, alias_varname) 'groups', 'user_permissions')}),
for obj in related_objects.all(): (_('Important dates'), {
try: 'fields': ('last_login', 'date_joined')}),
# Handle regular M2M relationships. )
getattr(alias_object, alias_varname).remove(obj) add_fieldsets = (
getattr(primary_object, alias_varname).add(obj) (None, {
except AttributeError: 'classes': ('wide',),
# Handle M2M relationships with a 'through' model. 'fields': ('username', 'password1', 'password2'),
# This does not delete the 'through model. }),
# TODO: Allow the user to delete a duplicate 'through' model. )
through_model = getattr(alias_object, alias_varname).through form = user_forms.ProfileChangeForm
kwargs = { add_form = user_forms.ProfileCreationForm
many_to_many_field.m2m_reverse_field_name(): obj, actions = [approve_user]
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().get_queryset(request).annotate(event_count=Count('event')) return super(AssociateAdmin, self).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()
@@ -124,16 +73,24 @@ 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:
master_object_pk = request.POST.get('master') masterObjectPk = request.POST.get('master')
master_object = queryset.get(pk=master_object_pk) masterObject = queryset.get(pk=masterObjectPk)
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
primary_object, deleted_objects, deleted_objects_count = merge_model_instances(master_object, queryset.exclude(pk=master_object_pk).all()) with transaction.atomic(), reversion.create_revision():
reversion.set_comment('Merging Objects') for obj in queryset.exclude(pk=masterObjectPk):
self.message_user(request, f"Objects successfully merged. {deleted_objects_count} old objects deleted.") events = obj.event_set.all()
for event in events:
masterObject.event_set.add(event)
obj.delete()
reversion.set_comment('Merging Objects')
self.message_user(request, "Objects successfully merged.")
return
else: # Present the confirmation screen else: # Present the confirmation screen
class TempForm(ModelForm): class TempForm(ModelForm):
class Meta: class Meta:
model = queryset.model model = queryset.model
@@ -152,36 +109,6 @@ 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')
@@ -198,18 +125,3 @@ 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')

View File

@@ -1,58 +1,59 @@
import datetime import datetime
import re import re
import reversion
from django import forms
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.urls import reverse_lazy
from django.db.models import Q
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse
from django.views import generic from django.views import generic
from django.db.models import Q
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
from django.db.models import Q
from RIGS import models from RIGS import models
from django import forms
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'}) forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
class InvoiceIndex(generic.ListView): class InvoiceIndex(generic.ListView):
model = models.Invoice model = models.Invoice
template_name = 'invoice_list.html' template_name = 'invoice_list_active.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(InvoiceIndex, self).get_context_data(**kwargs)
total = 0 total = 0
for i in context['object_list']: for i in context['object_list']:
total += i.balance total += i.balance
event_count = len(list(context['object_list'])) context['total'] = total
context['page_title'] = f"Outstanding Invoices ({event_count} Events, £{total:.2f})" context['count'] = len(list(context['object_list']))
context['description'] = "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger"
return context return context
def get_queryset(self): def get_queryset(self):
return self.model.objects.outstanding_invoices() # Manual query is the only way I have found to do this efficiently. Not ideal but needs must
sql = "SELECT * FROM " \
"(SELECT " \
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
"AS sub " \
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
"ORDER BY invoice_date"
query = self.model.objects.raw(sql)
return query
class InvoiceDetail(generic.DetailView): class InvoiceDetail(generic.DetailView):
model = models.Invoice model = models.Invoice
template_name = 'invoice_detail.html' template_name = 'invoice_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
invoice_date = self.object.invoice_date.strftime("%d/%m/%Y")
context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date})"
if self.object.void:
context['page_title'] += "<span class='badge badge-warning float-right'>VOID</span>"
elif self.object.is_closed:
context['page_title'] += "<span class='badge badge-success float-right'>PAID</span>"
else:
context['page_title'] += "<span class='badge badge-info float-right'>OUTSTANDING</span>"
return context
class InvoicePrint(generic.View): class InvoicePrint(generic.View):
def get(self, request, pk): def get(self, request, pk):
@@ -60,14 +61,16 @@ class InvoicePrint(generic.View):
object = invoice.event object = invoice.event
template = get_template('event_print.xml') template = get_template('event_print.xml')
name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
filename = f"Invoice {invoice.display_id} for {object.display_id} {name}.pdf"
context = { context = {
'object': object, 'object': object,
'fonts': {
'opensans': {
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'invoice': invoice, 'invoice': invoice,
'current_user': request.user, 'current_user': request.user,
'filename': filename
} }
rml = template.render(context) rml = template.render(context)
@@ -76,8 +79,11 @@ class InvoicePrint(generic.View):
pdfData = buffer.read() pdfData = buffer.read()
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
response = HttpResponse(content_type='application/pdf') response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = f'filename="{filename}"' response['Content-Disposition'] = "filename=Invoice %05d - N%05d | %s.pdf" % (
invoice.pk, invoice.event.pk, escapedEventName)
response.write(pdfData) response.write(pdfData)
return response return response
@@ -90,8 +96,8 @@ class InvoiceVoid(generic.View):
object.save() object.save()
if object.void: if object.void:
return HttpResponseRedirect(reverse('invoice_list')) return HttpResponseRedirect(reverse_lazy('invoice_list'))
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': object.pk})) return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
class InvoiceDelete(generic.DeleteView): class InvoiceDelete(generic.DeleteView):
@@ -102,14 +108,14 @@ class InvoiceDelete(generic.DeleteView):
obj = self.get_object() obj = self.get_object()
if obj.payment_set.all().count() > 0: if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.') messages.info(self.request, 'To delete an invoice, delete the payments first.')
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk})) return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
return super(InvoiceDelete, self).get(pk) return super(InvoiceDelete, self).get(pk)
def post(self, request, pk): def post(self, request, pk):
obj = self.get_object() obj = self.get_object()
if obj.payment_set.all().count() > 0: if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.') messages.info(self.request, 'To delete an invoice, delete the payments first.')
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk})) return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
return super(InvoiceDelete, self).post(pk) return super(InvoiceDelete, self).post(pk)
def get_success_url(self): def get_success_url(self):
@@ -121,14 +127,33 @@ class InvoiceArchive(generic.ListView):
template_name = 'invoice_list_archive.html' template_name = 'invoice_list_archive.html'
paginate_by = 25 paginate_by = 25
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = "Invoice Archive"
context['description'] = "This page displays all invoices: outstanding, paid, and void"
return context
def get_queryset(self): def get_queryset(self):
return self.model.objects.search(self.request.GET.get('q')).order_by('-invoice_date') q = self.request.GET.get('q', "")
filter = Q(event__name__icontains=q)
# try and parse an int
try:
val = int(q)
filter = filter | Q(pk=val)
filter = filter | Q(event__pk=val)
except: # noqa
# not an integer
pass
try:
if q[0] == "N":
val = int(q[1:])
filter = Q(event__pk=val) # If string is Nxxxxx then filter by event number
elif q[0] == "#":
val = int(q[1:])
filter = Q(pk=val) # If string is #xxxxx then filter by invoice number
except: # noqa
pass
object_list = self.model.objects.filter(filter).order_by('-invoice_date')
return object_list
class InvoiceWaiting(generic.ListView): class InvoiceWaiting(generic.ListView):
@@ -139,21 +164,35 @@ class InvoiceWaiting(generic.ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(InvoiceWaiting, self).get_context_data(**kwargs) context = super(InvoiceWaiting, self).get_context_data(**kwargs)
total = 0 total = 0
objects = self.get_queryset() for obj in self.get_objects():
for obj in objects:
total += obj.sum_total total += obj.sum_total
context['page_title'] = f"Events for Invoice ({len(objects)} Events, £{total:.2f})" context['total'] = total
context['count'] = len(self.get_objects())
return context return context
def get_queryset(self): def get_queryset(self):
return self.model.objects.waiting_invoices() return self.get_objects()
def get_objects(self):
# @todo find a way to select items
events = self.model.objects.filter(
(
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
) & Q(invoice__isnull=True) & # Has not already been invoiced
Q(is_rig=True) # Is a rig (not non-rig)
).order_by('start_date') \
.select_related('person',
'organisation',
'venue', 'mic') \
.prefetch_related('items')
return events
class InvoiceEvent(generic.View): class InvoiceEvent(generic.View):
@transaction.atomic()
@reversion.create_revision()
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
reversion.set_user(self.request.user)
epk = kwargs.get('pk') epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk) event = models.Event.objects.get(pk=epk)
invoice, created = models.Invoice.objects.get_or_create(event=event) invoice, created = models.Invoice.objects.get_or_create(event=event)
@@ -162,12 +201,7 @@ class InvoiceEvent(generic.View):
invoice.invoice_date = datetime.date.today() invoice.invoice_date = datetime.date.today()
messages.success(self.request, 'Invoice created successfully') messages.success(self.request, 'Invoice created successfully')
if kwargs.get('void'): return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))
invoice.void = not invoice.void
invoice.save()
messages.warning(self.request, 'Invoice voided')
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': invoice.pk}))
class PaymentCreate(generic.CreateView): class PaymentCreate(generic.CreateView):
@@ -176,7 +210,7 @@ class PaymentCreate(generic.CreateView):
template_name = 'payment_form.html' template_name = 'payment_form.html'
def get_initial(self): def get_initial(self):
initial = super().get_initial() initial = super(generic.CreateView, self).get_initial()
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None)) invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
if invoicepk is None: if invoicepk is None:
raise Http404() raise Http404()
@@ -184,28 +218,13 @@ class PaymentCreate(generic.CreateView):
initial.update({'invoice': invoice}) initial.update({'invoice': invoice})
return initial return initial
@transaction.atomic()
@reversion.create_revision()
def form_valid(self, form, *args, **kwargs):
reversion.add_to_revision(form.cleaned_data['invoice'])
reversion.set_comment("Payment added")
return super().form_valid(form, *args, **kwargs)
def get_success_url(self): def get_success_url(self):
messages.info(self.request, "location.reload()") messages.info(self.request, "location.reload()")
return reverse('closemodal') return reverse_lazy('closemodal')
class PaymentDelete(generic.DeleteView): class PaymentDelete(generic.DeleteView):
model = models.Payment model = models.Payment
template_name = 'payment_confirm_delete.html'
@transaction.atomic()
@reversion.create_revision()
def delete(self, *args, **kwargs):
reversion.add_to_revision(self.get_object().invoice)
reversion.set_comment("Payment removed")
return super().delete(*args, **kwargs)
def get_success_url(self): def get_success_url(self):
return self.request.POST.get('next') return self.request.POST.get('next')

View File

@@ -1,14 +1,15 @@
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.utils import timezone from django.core.mail import EmailMessage, EmailMultiAlternatives
from reversion import revisions as reversion from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
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'})
@@ -17,6 +18,8 @@ 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)
@@ -44,7 +47,7 @@ class EventForm(forms.ModelForm):
return simplejson.dumps(items) return simplejson.dumps(items)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super(EventForm, self).__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'
@@ -97,10 +100,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().clean() return super(EventForm, self).clean()
def save(self, commit=True): def save(self, commit=True):
m = super().save(commit=False) m = super(EventForm, self).save(commit=False)
if (commit): if (commit):
m.save() m.save()
@@ -121,7 +124,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', 'forum_url'] 'purchase_order', 'collector']
class BaseClientEventAuthorisationForm(forms.ModelForm): class BaseClientEventAuthorisationForm(forms.ModelForm):
@@ -131,7 +134,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().clean() return super(BaseClientEventAuthorisationForm, self).clean()
class Meta: class Meta:
abstract = True abstract = True
@@ -139,7 +142,7 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm): class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super(InternalClientEventAuthorisationForm, self).__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
@@ -150,94 +153,3 @@ 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__'

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
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):
@@ -9,7 +7,5 @@ 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')

View File

@@ -1,12 +1,11 @@
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
@@ -17,11 +16,12 @@ class Command(BaseCommand):
people = [] people = []
organisations = [] organisations = []
venues = [] venues = []
events = [] profiles = []
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,13 +32,20 @@ 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")
def setup_people(self): self.setupGenericProfiles()
self.setupPeople()
self.setupOrganisations()
self.setupVenues()
self.setupGroups()
self.setupEvents()
self.setupUsefulProfiles()
def setupPeople(self):
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe", 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",
@@ -53,25 +60,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(models.Profile.objects.all())) reversion.set_user(random.choice(self.profiles))
person = models.Person.objects.create(name=name)
newPerson = models.Person.objects.create(name=name)
if i % 3 == 0: if i % 3 == 0:
person.email = "address@person.com" newPerson.email = "address@person.com"
if i % 5 == 0: if i % 5 == 0:
person.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" newPerson.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0: if i % 7 == 0:
person.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567" newPerson.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0: if i % 9 == 0:
person.phone = "01234 567894" newPerson.phone = "01234 567894"
person.save() newPerson.save()
self.people.append(person) self.people.append(newPerson)
def setup_organisations(self): def setupOrganisations(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",
@@ -100,28 +107,27 @@ 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(models.Profile.objects.all())) reversion.set_user(random.choice(self.profiles))
new_organisation = models.Organisation.objects.create(name=name) newOrganisation = models.Organisation.objects.create(name=name)
if i % 2 == 0: if i % 2 == 0:
new_organisation.has_su_account = True newOrganisation.has_su_account = True
if i % 3 == 0: if i % 3 == 0:
new_organisation.email = "address@organisation.com" newOrganisation.email = "address@organisation.com"
if i % 5 == 0: if i % 5 == 0:
new_organisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0: if i % 7 == 0:
new_organisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567" newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0: if i % 9 == 0:
new_organisation.phone = "01234 567894" newOrganisation.phone = "01234 567894"
new_organisation.save() newOrganisation.save()
self.organisations.append(new_organisation) self.organisations.append(newOrganisation)
def setup_venues(self): def setupVenues(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",
@@ -137,27 +143,89 @@ 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))
new_venue = models.Venue.objects.create(name=name) newVenue = models.Venue.objects.create(name=name)
if i % 2 == 0: if i % 2 == 0:
new_venue.three_phase_available = True newVenue.three_phase_available = True
if i % 3 == 0: if i % 3 == 0:
new_venue.email = "address@venue.com" newVenue.email = "address@venue.com"
if i % 5 == 0: if i % 5 == 0:
new_venue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" newVenue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0: if i % 7 == 0:
new_venue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567" newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0: if i % 9 == 0:
new_venue.phone = "01234 567894" newVenue.phone = "01234 567894"
new_venue.save() newVenue.save()
self.venues.append(new_venue) self.venues.append(newVenue)
def setup_events(self): def setupGroups(self):
self.keyholder_group = Group.objects.create(name='Keyholders')
self.finance_group = Group.objects.create(name='Finance')
keyholderPerms = ["add_event", "change_event", "view_event",
"add_eventitem", "change_eventitem", "delete_eventitem",
"add_organisation", "change_organisation", "view_organisation",
"add_person", "change_person", "view_person", "view_profile",
"add_venue", "change_venue", "view_venue",
"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",
@@ -168,7 +236,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!"]
item_options = [ itemOptions = [
{'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',
@@ -185,7 +253,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}]
day_delta = -120 # start adding events from 4 months ago dayDelta = -120 # start adding events from 4 months ago
for i in range(150): # Let's add 100 events for i in range(150): # Let's add 100 events
with reversion.create_revision(): with reversion.create_revision():
@@ -193,98 +261,65 @@ class Command(BaseCommand):
name = names[i % len(names)] name = names[i % len(names)]
start_date = datetime.date.today() + datetime.timedelta(days=day_delta) startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
day_delta = day_delta + random.randint(0, 3) dayDelta = dayDelta + random.randint(0, 3)
new_event = models.Event.objects.create(name=name, start_date=start_date) newEvent = models.Event.objects.create(name=name, start_date=startDate)
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
new_event.start_time = datetime.time(random.randint(15, 20)) newEvent.start_time = datetime.time(random.randint(15, 20))
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
new_event.end_time = datetime.time(random.randint(21, 23)) newEvent.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
new_event.end_date = new_event.start_date + datetime.timedelta(days=1) newEvent.end_date = newEvent.start_date + datetime.timedelta(days=1)
new_event.end_time = datetime.time(random.randint(0, 5)) newEvent.end_time = datetime.time(random.randint(0, 5))
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
new_event.end_date = new_event.start_date + datetime.timedelta(days=random.randint(1, 4)) newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1, 4))
if random.randint(0, 6) > 0: # 5 in 6 have MIC if random.randint(0, 6) > 0: # 5 in 6 have MIC
new_event.mic = random.choice(self.profiles) newEvent.mic = random.choice(self.profiles)
if random.randint(0, 6) > 0: # 5 in 6 have organisation if random.randint(0, 6) > 0: # 5 in 6 have organisation
new_event.organisation = random.choice(self.organisations) newEvent.organisation = random.choice(self.organisations)
if random.randint(0, 6) > 0: # 5 in 6 have person if random.randint(0, 6) > 0: # 5 in 6 have person
new_event.person = random.choice(self.people) newEvent.person = random.choice(self.people)
if random.randint(0, 6) > 0: # 5 in 6 have venue if random.randint(0, 6) > 0: # 5 in 6 have venue
new_event.venue = random.choice(self.venues) newEvent.venue = random.choice(self.venues)
# Could have any status, equally weighted # Could have any status, equally weighted
new_event.status = random.choice( newEvent.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])
new_event.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire newEvent.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
if random.randint(0, 1) > 0: # 1 in 2 have description if random.randint(0, 1) > 0: # 1 in 2 have description
new_event.description = random.choice(descriptions) newEvent.description = random.choice(descriptions)
if random.randint(0, 1) > 0: # 1 in 2 have notes if random.randint(0, 1) > 0: # 1 in 2 have notes
new_event.notes = random.choice(notes) newEvent.notes = random.choice(notes)
new_event.save() newEvent.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)):
item_data = item_options[random.randint(0, len(item_options) - 1)] itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data) newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
new_item.save() newItem.save()
while new_event.sum_total < 0: while newEvent.sum_total < 0:
item_data = item_options[random.randint(0, len(item_options) - 1)] itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data) newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
new_item.save() newItem.save()
with reversion.create_revision(): with reversion.create_revision():
reversion.set_user(random.choice(self.profiles)) reversion.set_user(random.choice(self.profiles))
if new_event.start_date < datetime.date.today(): # think about adding an invoice if newEvent.start_date < datetime.date.today(): # think about adding an invoice
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
new_invoice = models.Invoice.objects.create(event=new_event) newInvoice = models.Invoice.objects.create(event=newEvent)
if new_event.status is models.Event.CANCELLED: # void cancelled events if newEvent.status is models.Event.CANCELLED: # void cancelled events
new_invoice.void = True newInvoice.void = True
elif random.randint(0, 2) > 1: # 1 in 3 have been paid elif random.randint(0, 2) > 1: # 1 in 3 have been paid
models.Payment.objects.create(invoice=new_invoice, amount=new_invoice.balance, models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.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))

View File

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

View File

@@ -3,7 +3,6 @@
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):
@@ -26,6 +25,6 @@ class Migration(migrations.Migration):
], ],
options={ options={
}, },
bases=(models.Model, versioning.versioning.RevisionMixin), bases=(models.Model, RIGS.models.RevisionMixin),
), ),
] ]

View File

@@ -3,7 +3,6 @@
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):
@@ -22,6 +21,6 @@ class Migration(migrations.Migration):
], ],
options={ options={
}, },
bases=(models.Model, versioning.versioning.RevisionMixin), bases=(models.Model, RIGS.models.RevisionMixin),
), ),
] ]

View File

@@ -4,7 +4,6 @@
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):
@@ -42,7 +41,7 @@ class Migration(migrations.Migration):
], ],
options={ options={
}, },
bases=(models.Model, versioning.versioning.RevisionMixin), bases=(models.Model, RIGS.models.RevisionMixin),
), ),
migrations.CreateModel( migrations.CreateModel(
name='EventItem', name='EventItem',
@@ -71,7 +70,7 @@ class Migration(migrations.Migration):
], ],
options={ options={
}, },
bases=(models.Model, versioning.versioning.RevisionMixin), bases=(models.Model, RIGS.models.RevisionMixin),
), ),
migrations.AddField( migrations.AddField(
model_name='event', model_name='event',

View File

@@ -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.noop) migrations.RunPython(approve_legacy)
] ]

View File

@@ -1,191 +0,0 @@
# 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'),
),
]

View File

@@ -0,0 +1,16 @@
# 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',
),
]

View File

@@ -1,67 +0,0 @@
# Generated by Django 3.1.7 on 2021-03-02 11:48
from django.db import migrations
def postgres_migration_prep(apps, schema_editor):
model = apps.get_model("RIGS", "Event")
for field in ["auth_request_to", "collector", "description", "notes", "purchase_order"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "EventAuthorisation")
for field in ["account_code", "uni_id"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "EventChecklist")
for field in ["extinguishers_location", "hs_location", "w1_description", "w2_description", "w3_description"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "EventItem")
for field in ["description"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Organisation")
for field in ["address", "email", "notes", "phone"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Payment")
for field in ["method"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Person")
for field in ["address", "email", "notes", "phone"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Profile")
for field in ["phone"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "RiskAssessment")
for field in ["general_notes", "persons_responsible_structures", "power_notes", "rigging_plan", "sound_notes"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Venue")
for field in ["address", "email", "notes", "phone"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0039_auto_20210123_1910'),
]
operations = [
migrations.RunPython(postgres_migration_prep, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,16 @@
# 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',
),
]

View File

@@ -1,201 +0,0 @@
# Generated by Django 3.1.7 on 2021-03-02 12:04
import RIGS.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0040_auto_20210302_1148'),
]
operations = [
migrations.RemoveField(
model_name='event',
name='meet_info',
),
migrations.RemoveField(
model_name='event',
name='payment_method',
),
migrations.RemoveField(
model_name='event',
name='payment_received',
),
migrations.AddField(
model_name='profile',
name='dark_theme',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='event',
name='auth_request_to',
field=models.EmailField(blank=True, default='', max_length=254),
),
migrations.AlterField(
model_name='event',
name='collector',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='collected by'),
),
migrations.AlterField(
model_name='event',
name='description',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='event',
name='notes',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='event',
name='purchase_order',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='PO'),
),
migrations.AlterField(
model_name='eventauthorisation',
name='account_code',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.AlterField(
model_name='eventauthorisation',
name='uni_id',
field=models.CharField(blank=True, default='', max_length=10, verbose_name='University ID'),
),
migrations.AlterField(
model_name='eventchecklist',
name='extinguishers_location',
field=models.CharField(blank=True, default='', help_text='Location of fire extinguishers', max_length=255),
),
migrations.AlterField(
model_name='eventchecklist',
name='hs_location',
field=models.CharField(blank=True, default='', help_text='Location of Safety Bag/Box', max_length=255),
),
migrations.AlterField(
model_name='eventchecklist',
name='w1_description',
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
),
migrations.AlterField(
model_name='eventchecklist',
name='w2_description',
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
),
migrations.AlterField(
model_name='eventchecklist',
name='w3_description',
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
),
migrations.AlterField(
model_name='eventitem',
name='description',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='organisation',
name='address',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='organisation',
name='email',
field=models.EmailField(blank=True, default='', max_length=254),
),
migrations.AlterField(
model_name='organisation',
name='notes',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='organisation',
name='phone',
field=models.CharField(blank=True, default='', max_length=15),
),
migrations.AlterField(
model_name='payment',
name='method',
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('SU', 'SU Core'), ('T', 'TEC Adjustment')], default='', max_length=2),
),
migrations.AlterField(
model_name='person',
name='address',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='person',
name='email',
field=models.EmailField(blank=True, default='', max_length=254),
),
migrations.AlterField(
model_name='person',
name='notes',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='person',
name='phone',
field=models.CharField(blank=True, default='', max_length=15),
),
migrations.AlterField(
model_name='profile',
name='api_key',
field=models.CharField(blank=True, default='', editable=False, max_length=40),
),
migrations.AlterField(
model_name='profile',
name='phone',
field=models.CharField(blank=True, default='', max_length=13),
),
migrations.AlterField(
model_name='riskassessment',
name='general_notes',
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
),
migrations.AlterField(
model_name='riskassessment',
name='persons_responsible_structures',
field=models.TextField(blank=True, default='', help_text='Who are the persons on site responsible for their use?'),
),
migrations.AlterField(
model_name='riskassessment',
name='power_notes',
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
),
migrations.AlterField(
model_name='riskassessment',
name='power_plan',
field=models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[RIGS.models.validate_url]),
),
migrations.AlterField(
model_name='riskassessment',
name='rigging_plan',
field=models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[RIGS.models.validate_url]),
),
migrations.AlterField(
model_name='riskassessment',
name='sound_notes',
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
),
migrations.AlterField(
model_name='venue',
name='address',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='venue',
name='email',
field=models.EmailField(blank=True, default='', max_length=254),
),
migrations.AlterField(
model_name='venue',
name='notes',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='venue',
name='phone',
field=models.CharField(blank=True, default='', max_length=15),
),
]

View File

@@ -1,34 +0,0 @@
# Generated by Django 3.1.13 on 2021-10-07 22:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0041_auto_20210302_1204'),
]
operations = [
migrations.AlterField(
model_name='eventchecklist',
name='fd_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='eventchecklist',
name='w1_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='eventchecklist',
name='w2_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='eventchecklist',
name='w3_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
]

View File

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

View File

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

View File

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

View File

@@ -1,71 +0,0 @@
# 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),
]

View File

@@ -1,44 +0,0 @@
# 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),
]

View File

@@ -1,156 +0,0 @@
# 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',
),
]

View File

@@ -1,53 +0,0 @@
# 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),
),
]

View File

@@ -1,19 +0,0 @@
# 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]),
),
]

View File

@@ -1,48 +1,32 @@
import datetime import datetime
import hashlib import hashlib
import random import datetime
import string
from collections import Counter
from decimal import Decimal
from urllib.parse import urlparse
import pytz import pytz
from django import forms
from django.db.models import Q, F
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.contrib.auth.models import AbstractUser
from django.conf import settings
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
from versioning.versioning import RevisionMixin import string
import random
from collections import Counter
from decimal import Decimal
def filter_by_pk(filt, query): from django.core.exceptions import ValidationError
# try and parse an int from django.urls import reverse_lazy
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, null=True, blank=False) initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
phone = models.CharField(max_length=13, blank=True, default='') phone = models.CharField(max_length=13, null=True, blank=True)
api_key = models.CharField(max_length=40, blank=True, editable=False, default='') api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.") is_approved = models.BooleanField(default=False)
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that... last_emailed = models.DateTimeField(blank=True,
last_emailed = models.DateTimeField(blank=True, null=True) null=True) # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
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):
@@ -63,12 +47,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 += f' "{self.initials}"' name += ' "{}"'.format(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', 'riskassessment', 'invoice').prefetch_related('checklists') return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
@classmethod @classmethod
def admins(cls): def admins(cls):
@@ -76,38 +60,48 @@ class Profile(AbstractUser):
@classmethod @classmethod
def users_awaiting_approval_count(cls): def users_awaiting_approval_count(cls):
# last_login = None ensures we only pick up genuinely new users, not those that have been deactivated for inactivity return Profile.objects.filter(models.Q(is_approved=False)).count()
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
class ContactableManager(models.Manager): @property
def search(self, query=None): def last_edited_at(self):
qs = self.get_queryset() version = self.current_version
if query is not None: if version is None:
or_lookup = Q(name__icontains=query) | Q(email__icontains=query) | Q(address__icontains=query) | Q(notes__icontains=query) | Q( return None
phone__startswith=query) | Q(phone__endswith=query) return version.revision.date_created
or_lookup = filter_by_pk(or_lookup, query) @property
def last_edited_by(self):
version = self.current_version
if version is None:
return None
return version.revision.user
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups @property
return qs def current_version_id(self):
version = self.current_version
if version is None:
return None
return "V{0} | R{1}".format(version.pk, version.revision.pk)
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, default='') phone = models.CharField(max_length=15, blank=True, null=True)
email = models.EmailField(blank=True, default='') email = models.EmailField(blank=True, null=True)
address = models.TextField(blank=True, default='')
notes = models.TextField(blank=True, default='')
objects = ContactableManager() address = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, null=True)
def __str__(self): def __str__(self):
string = self.name string = self.name
@@ -133,18 +127,18 @@ 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('person_detail', kwargs={'pk': self.pk}) return reverse_lazy('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, default='') phone = models.CharField(max_length=15, blank=True, null=True)
email = models.EmailField(blank=True, default='') email = models.EmailField(blank=True, null=True)
address = models.TextField(blank=True, default='')
notes = models.TextField(blank=True, default='')
union_account = models.BooleanField(default=False)
objects = ContactableManager() address = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, null=True)
union_account = models.BooleanField(default=False)
def __str__(self): def __str__(self):
string = self.name string = self.name
@@ -170,7 +164,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('organisation_detail', kwargs={'pk': self.pk}) return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
class VatManager(models.Manager): class VatManager(models.Manager):
@@ -178,6 +172,7 @@ 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:
@@ -194,8 +189,6 @@ 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
@@ -205,18 +198,17 @@ class VatRate(models.Model, RevisionMixin):
get_latest_by = 'start_at' get_latest_by = 'start_at'
def __str__(self): def __str__(self):
return f"{self.comment} {self.start_at} @ {self.as_percent}%" return self.comment + " " + str(self.start_at) + " @ " + str(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, default='') phone = models.CharField(max_length=15, blank=True, null=True)
email = models.EmailField(blank=True, default='') email = models.EmailField(blank=True, null=True)
three_phase_available = models.BooleanField(default=False) three_phase_available = models.BooleanField(default=False)
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, null=True)
address = models.TextField(blank=True, default='')
objects = ContactableManager() address = models.TextField(blank=True, null=True)
def __str__(self): def __str__(self):
string = self.name string = self.name
@@ -229,23 +221,24 @@ 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('venue_detail', kwargs={'pk': self.pk}) return reverse_lazy('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(), end_date__isnull=True, dry_hire=False) & ~models.Q( (models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end 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()) & ~models.Q( (models.Q(dry_hire=True, start_date__gte=timezone.now().date()) & ~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()) # Canceled but not started models.Q(status=Event.CANCELLED, start_date__gte=timezone.now().date()) # Canceled but not started
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic') ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
'organisation',
'venue', 'mic')
return events return events
def events_in_bounds(self, start, end): def events_in_bounds(self, start, end):
@@ -268,54 +261,18 @@ 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(), end_date__isnull=True, dry_hire=False, (models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False,
is_rig=True) & ~models.Q( 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(), dry_hire=False, is_rig=True) & ~models.Q( (models.Q(end_date__gte=timezone.now().date(), dry_hire=False, is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Ends after status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q( (models.Q(dry_hire=True, start_date__gte=timezone.now().date(), is_rig=True) & ~models.Q(
status=Event.CANCELLED)) # Active dry hire 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):
@@ -335,8 +292,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, default='') description = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, null=True)
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)
@@ -350,6 +307,7 @@ 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,
@@ -358,23 +316,18 @@ class Event(models.Model, RevisionMixin):
verbose_name="MIC", on_delete=models.CASCADE) verbose_name="MIC", on_delete=models.CASCADE)
# Monies # Monies
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO') payment_method = models.CharField(max_length=255, blank=True, null=True)
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by') payment_received = models.CharField(max_length=255, blank=True, null=True)
purchase_order = models.CharField(max_length=255, blank=True, null=True, verbose_name='PO')
collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='collected by')
# 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(blank=True, default='') auth_request_to = models.EmailField(null=True, blank=True)
forum_url = models.URLField(default='', blank=True, validators=[validate_forum_url]) # Risk assessment info
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
""" """
@@ -383,7 +336,18 @@ class Event(models.Model, RevisionMixin):
@property @property
def sum_total(self): def sum_total(self):
total = self.items.aggregate( # Manual querying is required for efficiency whilst maintaining floating point arithmetic
# if connection.vendor == 'postgresql':
# sql = "SELECT SUM(quantity * cost) AS sum_total FROM \"RIGS_eventitem\" WHERE event_id=%i" % self.id
# else:
# sql = "SELECT id, SUM(quantity * cost) AS sum_total FROM RIGS_eventitem WHERE event_id=%i" % self.id
# total = self.items.raw(sql)[0]
# if total.sum_total:
# return total.sum_total
# total = 0.0
# for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"):
# total += item.sum
total = EventItem.objects.filter(event=self).aggregate(
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']
@@ -397,9 +361,6 @@ 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'))
""" """
@@ -419,16 +380,8 @@ 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 hs_done(self): def authorised(self):
return self.riskassessment is not None and self.has_checklist and self.has_power return not self.internal and self.purchase_order or self.authorisation.amount == self.total
@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):
@@ -492,50 +445,30 @@ class Event(models.Model, RevisionMixin):
@property @property
def internal(self): def internal(self):
return bool(self.organisation and self.organisation.union_account) return 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('event_detail', kwargs={'pk': self.pk}) return reverse_lazy('event_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return f"{self.display_id} | {self.name}" return str(self.pk) + ": " + 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:
errdict['end_date'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.'] raise ValidationError('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:
errdict['end_time'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.'] raise ValidationError('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:
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.'] raise ValidationError('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:
errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.'] raise ValidationError('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."""
@@ -543,17 +476,14 @@ class Event(models.Model, RevisionMixin):
super(Event, self).save(*args, **kwargs) super(Event, self).save(*args, **kwargs)
@reversion.register class EventItem(models.Model):
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, default='') description = models.TextField(blank=True, null=True)
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
@@ -562,11 +492,7 @@ class EventItem(models.Model, RevisionMixin):
ordering = ['order'] ordering = ['order']
def __str__(self): def __str__(self):
return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}" return str(self.event.pk) + "." + str(self.order) + ": " + self.event.name + " | " + self.name
@property
def activity_feed_string(self):
return f"item {self.name}"
@reversion.register @reversion.register
@@ -574,74 +500,25 @@ 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, default='', verbose_name="University ID") uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID")
account_code = models.CharField(max_length=50, default='', blank=True) account_code = models.CharField(max_length=50, blank=True, null=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('event_detail', kwargs={'pk': self.event_id}) return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return f"{self.event.display_id} (requested by {self.sent_by.initials})" return str("N%05d" % self.event.pk + ' (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, RevisionMixin): class Invoice(models.Model):
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
@@ -665,26 +542,14 @@ class Invoice(models.Model, RevisionMixin):
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 f"{self.display_id}: {self.event}{self.balance:.2f})" return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
@property
def display_id(self):
return f"#{self.pk:05d}"
class Meta: class Meta:
ordering = ['-invoice_date'] ordering = ['-invoice_date']
@reversion.register class Payment(models.Model):
class Payment(models.Model, RevisionMixin):
CASH = 'C' CASH = 'C'
INTERNAL = 'I' INTERNAL = 'I'
EXTERNAL = 'E' EXTERNAL = 'E'
@@ -701,271 +566,7 @@ class Payment(models.Model, RevisionMixin):
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, default='', blank=True) method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True)
reversion_hide = True
def __str__(self): def __str__(self):
return f"{self.get_method_display()}: {self.amount}" return "%s: %d" % (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

View File

@@ -1,35 +1,34 @@
import copy from io import BytesIO
import urllib.request
import urllib.error
import urllib.parse
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.views import generic
from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.template.loader import get_template
from django.conf import settings
from django.urls import reverse
from django.core import signing
from django.http import HttpResponse
from django.core.exceptions import SuspiciousOperation
from django.db.models import Q
from django.contrib import messages
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from z3c.rml import rml2pdf
from PyPDF2 import PdfFileMerger, PdfFileReader
import simplejson
import premailer
from RIGS import models, forms
from PyRIGS import decorators
import datetime import datetime
import re import re
import premailer import copy
import simplejson
import urllib
import hmac
import hashlib
from envparse import env
from bs4 import BeautifulSoup
from django.conf import settings
from django.contrib import messages
from django.contrib.staticfiles import finders
from django.core import signing
from django.core.exceptions import SuspiciousOperation
from django.core.mail import EmailMultiAlternatives
from django.db.models import Q
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from django.urls import reverse
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import generic
from django.views.decorators.csrf import csrf_exempt
from PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
from RIGS import models, forms
__author__ = 'ghost' __author__ = 'ghost'
@@ -39,11 +38,10 @@ class RigboardIndex(generic.TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# get super context # get super context
context = super().get_context_data(**kwargs) context = super(RigboardIndex, self).get_context_data(**kwargs)
# call out method to get current events # call out method to get current events
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists') context['events'] = models.Event.objects.current_events()
context['page_title'] = "Rigboard"
return context return context
@@ -51,37 +49,57 @@ class WebCalendar(generic.TemplateView):
template_name = 'calendar.html' template_name = 'calendar.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(WebCalendar, self).get_context_data(**kwargs)
context['view'] = kwargs.get('view', '') context['view'] = kwargs.get('view', '')
context['date'] = kwargs.get('date', '') context['date'] = kwargs.get('date', '')
# context['page_title'] = "Calendar"
return context return context
class EventDetail(generic.DetailView, ModalURLMixin): class EventDetail(generic.DetailView):
template_name = 'event_detail.html' template_name = 'event_detail.html'
model = models.Event model = models.Event
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) class EventOembed(generic.View):
title = f"{self.object.display_id} | {self.object.name}" model = models.Event
if self.object.dry_hire:
title += " <span class='badge badge-secondary'>Dry Hire</span>" def get(self, request, pk=None):
context['page_title'] = title embed_url = reverse('event_embed', args=[pk])
if is_ajax(self.request): full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
context['override'] = "base_ajax.html"
else: data = {
context['override'] = 'base_assets.html' 'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
return context 'version': '1.0',
'type': 'rich',
'height': '250'
}
json = simplejson.JSONEncoderForHTML().encode(data)
return HttpResponse(json, content_type="application/json")
class EventEmbed(EventDetail): class EventEmbed(EventDetail):
template_name = 'event_embed.html' template_name = 'event_embed.html'
class EventOEmbed(OEmbedView): class EventRA(generic.base.RedirectView):
model = models.Event permanent = False
url_name = 'event_embed'
def get_redirect_url(self, *args, **kwargs):
event = get_object_or_404(models.Event, pk=kwargs['pk'])
if event.risk_assessment_edit_url:
return event.risk_assessment_edit_url
params = {
'entry.708610078': f'N{event.pk:05}',
'entry.905899507': event.name,
'entry.139491562': event.venue.name if event.venue else '',
'entry.1689826056': event.start_date.strftime('%Y-%m-%d') + (
(' - ' + event.end_date.strftime('%Y-%m-%d')) if event.end_date else ''),
'entry.902421165': event.mic.name if event.mic else ''
}
return settings.RISK_ASSESSMENT_URL + "?" + urllib.parse.urlencode(params)
class EventCreate(generic.CreateView): class EventCreate(generic.CreateView):
@@ -90,17 +108,19 @@ class EventCreate(generic.CreateView):
template_name = 'event_form.html' template_name = 'event_form.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(EventCreate, self).get_context_data(**kwargs)
context['page_title'] = "New Event"
context['edit'] = True context['edit'] = True
context['currentVAT'] = models.VatRate.objects.current_rate() context['currentVAT'] = models.VatRate.objects.current_rate()
form = context['form'] form = context['form']
if hasattr(form, 'items_json') and re.search(r'"-\d+"', form['items_json'].value()): if re.search(r'"-\d+"', form['items_json'].value()):
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.") messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
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)
return context return context
def get_success_url(self): def get_success_url(self):
@@ -113,18 +133,21 @@ class EventUpdate(generic.UpdateView):
template_name = 'event_form.html' template_name = 'event_form.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(EventUpdate, self).get_context_data(**kwargs)
context['page_title'] = f"Event {self.object.display_id}"
context['edit'] = True context['edit'] = True
form = context['form'] form = context['form']
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)
return context return context
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
if hasattr(context, 'duplicate') and not context['duplicate']: if not hasattr(context, 'duplicate'):
# If this event has already been emailed to a client, show a warning # If this event has already been emailed to a client, show a warning
if self.object.auth_request_at is not None: if self.object.auth_request_at is not None:
messages.info(self.request, messages.info(self.request,
@@ -132,8 +155,8 @@ class EventUpdate(generic.UpdateView):
if hasattr(self.object, 'authorised'): if hasattr(self.object, 'authorised'):
messages.warning(self.request, messages.warning(self.request,
'This event has already been authorised by the client, any changes to the price will require reauthorisation.') 'This event has already been authorised by client, any changes to price will require reauthorisation.')
return super().render_to_response(context, **response_kwargs) return super(EventUpdate, self).render_to_response(context, **response_kwargs)
def get_success_url(self): def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk}) return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
@@ -141,19 +164,17 @@ class EventUpdate(generic.UpdateView):
class EventDuplicate(EventUpdate): class EventDuplicate(EventUpdate):
def get_object(self, queryset=None): def get_object(self, queryset=None):
old = super().get_object(queryset) # Get the object (the event you're duplicating) old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
new = copy.copy(old) # Make a copy of the object in memory new = copy.copy(old) # Make a copy of the object in memory
new.based_on = old # Make the new event based on the old event new.based_on = old # Make the new event based on the old event
new.purchase_order = None # Remove old PO new.purchase_order = None # Remove old PO
new.status = new.PROVISIONAL # Return status to provisional
# Clear checked in by if it's a dry hire # Clear checked in by if it's a dry hire
if new.dry_hire is True: if new.dry_hire is True:
new.checked_in_by = None new.checked_in_by = None
new.collector = None
# Remove all the authorisation information from the new event # Remove all the authorisation information from the new event
new.auth_request_to = '' new.auth_request_to = None
new.auth_request_by = None new.auth_request_by = None
new.auth_request_at = None new.auth_request_at = None
@@ -166,22 +187,48 @@ class EventDuplicate(EventUpdate):
return new return new
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(EventDuplicate, self).get_context_data(**kwargs)
context['page_title'] = f"Duplicate of Event {self.object.display_id}"
context["duplicate"] = True context["duplicate"] = True
return context return context
class EventPrint(PrintView): class EventPrint(generic.View):
model = models.Event def get(self, request, pk):
template_name = 'event_print.xml' object = get_object_or_404(models.Event, pk=pk)
append_terms = True template = get_template('event_print.xml')
def get_context_data(self, **kwargs): merger = PdfFileMerger()
context = super().get_context_data(**kwargs)
context['quote'] = True context = {
context['filename'] = f"Event_{context['object'].display_id}_{context['object_name']}_{context['object'].start_date}.pdf" 'object': object,
return context 'fonts': {
'opensans': {
'regular': 'static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'static/fonts/OPENSANS-BOLD.TTF',
}
},
'quote': True,
'current_user': request.user,
}
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(BytesIO(terms.read()))
merged = BytesIO()
merger.write(merged)
response = HttpResponse(content_type='application/pdf')
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)
response['Content-Disposition'] = "filename=N%05d | %s.pdf" % (object.pk, escapedEventName)
response.write(merged.getvalue())
return response
class EventArchive(generic.ListView): class EventArchive(generic.ListView):
@@ -190,11 +237,11 @@ class EventArchive(generic.ListView):
paginate_by = 25 paginate_by = 25
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) # get super context
context = super(EventArchive, self).get_context_data(**kwargs)
context['start'] = self.request.GET.get('start', None) context['start'] = self.request.GET.get('start', None)
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d')) context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
context['statuses'] = models.Event.EVENT_STATUS_CHOICES
context['page_title'] = 'Event Archive'
return context return context
def get_queryset(self): def get_queryset(self):
@@ -214,22 +261,32 @@ class EventArchive(generic.ListView):
filter &= Q(start_date__gte=start) filter &= Q(start_date__gte=start)
q = self.request.GET.get('q', "") q = self.request.GET.get('q', "")
objects = self.model.objects.all()
if q: if q != "":
objects = self.model.objects.search(q) qfilter = Q(name__icontains=q) | Q(description__icontains=q) | Q(notes__icontains=q)
status = self.request.GET.getlist('status', "") # try and parse an int
try:
val = int(q)
qfilter = qfilter | Q(pk=val)
except: # noqa not an integer
pass
if len(status) > 0: try:
filter &= Q(status__in=status) if q[0] == "N":
val = int(q[1:])
qfilter = Q(pk=val) # If string is N###### then do a simple PK filter
except: # noqa
pass
qs = objects.filter(filter).order_by('-start_date') filter &= qfilter
qs = self.model.objects.filter(filter).order_by('-start_date')
# Preselect related for efficiency # Preselect related for efficiency
qs.select_related('person', 'organisation', 'venue', 'mic') qs.select_related('person', 'organisation', 'venue', 'mic')
if not qs.exists(): if len(qs) == 0:
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.") messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
return qs return qs
@@ -238,7 +295,6 @@ class EventArchive(generic.ListView):
class EventAuthorise(generic.UpdateView): class EventAuthorise(generic.UpdateView):
template_name = 'eventauthorisation_form.html' template_name = 'eventauthorisation_form.html'
success_template = 'eventauthorisation_success.html' success_template = 'eventauthorisation_success.html'
preview = False
def form_valid(self, form): def form_valid(self, form):
self.object = form.save() self.object = form.save()
@@ -246,7 +302,7 @@ class EventAuthorise(generic.UpdateView):
self.template_name = self.success_template self.template_name = self.success_template
messages.add_message(self.request, messages.SUCCESS, messages.add_message(self.request, messages.SUCCESS,
'Success! Your event has been authorised. ' + 'Success! Your event has been authorised. ' +
f'You will also receive email confirmation to {self.object.email}.') 'You will also receive email confirmation to %s.' % (self.object.email))
return self.render_to_response(self.get_context_data()) return self.render_to_response(self.get_context_data())
@property @property
@@ -260,13 +316,10 @@ class EventAuthorise(generic.UpdateView):
return forms.InternalClientEventAuthorisationForm return forms.InternalClientEventAuthorisationForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(EventAuthorise, self).get_context_data(**kwargs)
context['event'] = self.event context['event'] = self.event
context['tos_url'] = settings.TERMS_OF_HIRE_URL context['tos_url'] = settings.TERMS_OF_HIRE_URL
context['page_title'] = f"{self.event.display_id}: {self.event.name}"
if self.event.dry_hire:
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
context['preview'] = self.preview
return context return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@@ -279,10 +332,10 @@ class EventAuthorise(generic.UpdateView):
messages.add_message(self.request, messages.WARNING, messages.add_message(self.request, messages.WARNING,
"This event has already been authorised, but the amount has changed. " + "This event has already been authorised, but the amount has changed. " +
"Please check the amount and reauthorise.") "Please check the amount and reauthorise.")
return super().get(request, *args, **kwargs) return super(EventAuthorise, self).get(request, *args, **kwargs)
def get_form(self, **kwargs): def get_form(self, **kwargs):
form = super().get_form(**kwargs) form = super(EventAuthorise, self).get_form(**kwargs)
form.instance.event = self.event form.instance.event = self.event
form.instance.email = self.request.email form.instance.email = self.request.email
form.instance.sent_by = self.request.sent_by form.instance.sent_by = self.request.sent_by
@@ -298,7 +351,7 @@ class EventAuthorise(generic.UpdateView):
except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist): except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist):
raise SuspiciousOperation( raise SuspiciousOperation(
"This URL is invalid. Please ask your TEC contact for a new URL") "This URL is invalid. Please ask your TEC contact for a new URL")
return super().dispatch(request, *args, **kwargs) return super(EventAuthorise, self).dispatch(request, *args, **kwargs)
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin): class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
@@ -308,14 +361,14 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
@method_decorator(decorators.nottinghamtec_address_required) @method_decorator(decorators.nottinghamtec_address_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs) return super(EventAuthorisationRequest, self).dispatch(*args, **kwargs)
@property @property
def object(self): def object(self):
return self.get_object() return self.get_object()
def get_success_url(self): def get_success_url(self):
if is_ajax(self.request): if self.request.is_ajax():
url = reverse_lazy('closemodal') url = reverse_lazy('closemodal')
messages.info(self.request, "location.reload()") messages.info(self.request, "location.reload()")
else: else:
@@ -329,7 +382,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
email = form.cleaned_data['email'] email = form.cleaned_data['email']
event = self.object event = self.object
event.auth_request_by = self.request.user event.auth_request_by = self.request.user
event.auth_request_at = timezone.now() event.auth_request_at = datetime.datetime.now()
event.auth_request_to = email event.auth_request_to = email
event.save() event.save()
@@ -348,13 +401,13 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
context['to_name'] = event.organisation.name context['to_name'] = event.organisation.name
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(
f"{self.object.display_id} | {self.object.name} - Event Authorisation Request", "N%05d | %s - Event Authorisation Request" % (self.object.pk, self.object.name),
get_template("email/eventauthorisation_client_request.txt").render(context), get_template("eventauthorisation_client_request.txt").render(context),
to=[email], to=[email],
reply_to=[self.request.user.email], reply_to=[self.request.user.email],
) )
css = finders.find('css/email.css') css = staticfiles_storage.path('css/email.css')
html = premailer.Premailer(get_template("email/eventauthorisation_client_request.html").render(context), html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
external_styles=css).transform() external_styles=css).transform()
msg.attach_alternative(html, 'text/html') msg.attach_alternative(html, 'text/html')
@@ -364,61 +417,47 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
class EventAuthoriseRequestEmailPreview(generic.DetailView): class EventAuthoriseRequestEmailPreview(generic.DetailView):
template_name = "email/eventauthorisation_client_request.html" template_name = "eventauthorisation_client_request.html"
model = models.Event model = models.Event
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
css = finders.find('css/email.css') from django.contrib.staticfiles.storage import staticfiles_storage
response = super().render_to_response(context, **response_kwargs) css = staticfiles_storage.path('css/email.css')
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
assert isinstance(response, HttpResponse) assert isinstance(response, HttpResponse)
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform() response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
return response return response
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super(EventAuthoriseRequestEmailPreview, self).get_context_data(**kwargs)
context['hmac'] = signing.dumps({ context['hmac'] = signing.dumps({
'pk': self.object.pk, 'pk': self.object.pk,
'email': self.request.GET.get('email', 'hello@world.test'), 'email': self.request.GET.get('email', 'hello@world.test'),
'sent_by': self.request.user.pk, 'sent_by': self.request.user.pk,
}) })
context['to_name'] = self.request.GET.get('to_name', None) context['to_name'] = self.request.GET.get('to_name', None)
context['target'] = 'event_authorise_form_preview'
return context return context
class CreateForumThread(generic.base.RedirectView): @method_decorator(csrf_exempt, name='dispatch')
permanent = False class LogRiskAssessment(generic.View):
http_method_names = ["post"]
def get_redirect_url(self, *args, **kwargs): def post(self, request, **kwargs):
event = get_object_or_404(models.Event, pk=kwargs['pk']) data = request.POST
shared_secret = data.get("secret")
edit_url = data.get("editUrl")
rig_number = data.get("rigNum")
if shared_secret is None or edit_url is None or rig_number is None:
return HttpResponse(status=422)
if event.forum_url: if shared_secret != settings.RISK_ASSESSMENT_SECRET:
return event.forum_url return HttpResponse(status=403)
params = { rig_number = int(re.sub("[^0-9]", "", rig_number))
'title': str(event),
'body': f'https://rigs.nottinghamtec.co.uk/event/{event.pk}',
'category': 'rig-info'
}
return f'https://forum.nottinghamtec.co.uk/new-topic?{urllib.parse.urlencode(params)}'
event = get_object_or_404(models.Event, pk=rig_number)
event.risk_assessment_edit_url = edit_url
event.save()
class RecieveForumWebhook(generic.View): return HttpResponse(status=200)
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
computed = f"sha256={hmac.new(env('FORUM_WEBHOOK_SECRET').encode(), request.body, hashlib.sha256).hexdigest()}"
if not hmac.compare_digest(request.headers.get('X-Discourse-Event-Signature'), computed):
return HttpResponseForbidden('Invalid signature header')
# Check if this is the right kind of event. The webhook filters by category on the forum side
if request.headers.get('X-Discourse-Event') == "topic_created":
body = simplejson.loads(request.body.decode('utf-8'))
event_id = int(body['topic']['title'][1:6]) # find the ID, force convert it to an int to eliminate leading zeros
event = models.Event.objects.filter(pk=event_id).first()
if event:
event.forum_url = f"https://forum.nottinghamtec.co.uk/t/{body['topic']['slug']}"
event.save()
return HttpResponse(status=202)
return HttpResponse(status=204)

View File

@@ -1,21 +1,20 @@
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 import finders from django.contrib.staticfiles.storage import staticfiles_storage
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 premailer import Premailer
from registration.signals import user_activated from registration.signals import user_activated
from reversion import revisions as reversion from premailer import Premailer
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
from RIGS import models from RIGS import models
@@ -25,6 +24,12 @@ 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,
} }
@@ -54,23 +59,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 = f"{instance.event.display_id} | {instance.event.name} - Event Authorised" subject = "N%05d | %s - Event Authorised" % (instance.event.pk, instance.event.name)
client_email = EmailMultiAlternatives( client_email = EmailMultiAlternatives(
subject, subject,
get_template("email/eventauthorisation_client_success.txt").render(context), get_template("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 = finders.find('css/email.css') css = staticfiles_storage.path('css/email.css')
html = Premailer(get_template("email/eventauthorisation_client_success.html").render(context), html = Premailer(get_template("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(f'{instance.event.display_id} - {escapedEventName} - CONFIRMATION.pdf', client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
merged.getvalue(), merged.getvalue(),
'application/pdf' 'application/pdf'
) )
@@ -82,7 +87,7 @@ def send_eventauthorisation_success_email(instance):
mic_email = EmailMessage( mic_email = EmailMessage(
subject, subject,
get_template("email/eventauthorisation_mic_success.txt").render(context), get_template("eventauthorisation_mic_success.txt").render(context),
to=[mic_email_address] to=[mic_email_address]
) )
@@ -116,13 +121,13 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
} }
email = EmailMultiAlternatives( email = EmailMultiAlternatives(
f"{context['number_of_users']} new users awaiting approval on RIGS", "%s new users awaiting approval on RIGS" % (context['number_of_users']),
get_template("email/admin_awaiting_approval.txt").render(context), get_template("admin_awaiting_approval.txt").render(context),
to=[admin.email], to=[admin.email],
reply_to=[user.email], reply_to=[user.email],
) )
css = finders.find('css/email.css') css = staticfiles_storage.path('css/email.css')
html = Premailer(get_template("email/admin_awaiting_approval.html").render(context), html = Premailer(get_template("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()
@@ -133,11 +138,3 @@ 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)

View File

@@ -0,0 +1,28 @@
/*!
* 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 */

View File

@@ -0,0 +1,28 @@
/*!
* 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= */

View File

@@ -0,0 +1,23 @@
.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 */

File diff suppressed because one or more lines are too long

455
RIGS/static/css/bootstrap-select.css vendored Normal file

File diff suppressed because one or more lines are too long

39
RIGS/static/css/email.css Normal file
View File

@@ -0,0 +1,39 @@
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 */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
RIGS/static/css/ie.css Normal file
View File

@@ -0,0 +1,2 @@
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJpZS5jc3MiLCJzb3VyY2VzQ29udGVudCI6W119 */

7198
RIGS/static/css/print.css Normal file

File diff suppressed because one or more lines are too long

7418
RIGS/static/css/screen.css Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

File diff suppressed because one or more lines are too long

6
RIGS/static/js/alert.js Normal file
View File

@@ -0,0 +1,6 @@
/*!
* 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}));

5
RIGS/static/js/all.js Normal file

File diff suppressed because one or more lines are too long

1
RIGS/static/js/asteroids.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
$(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)}))}));

File diff suppressed because one or more lines are too long

7
RIGS/static/js/bootstrap-select.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
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 Normal file

File diff suppressed because one or more lines are too long

25
RIGS/static/js/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

1
RIGS/static/js/konami.js Normal file
View File

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

File diff suppressed because one or more lines are too long

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