Compare commits
22 Commits
948a41f43a
...
assets-tes
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e114484fe | |||
|
ea696deb1e
|
|||
|
b8a4a70133
|
|||
|
3028c053d4
|
|||
|
3e16075b34
|
|||
|
762af5a1eb
|
|||
|
d312634f68
|
|||
|
ca553137b6
|
|||
|
1c577cdbee
|
|||
|
eaeb56f9f9
|
|||
|
b8d9d24e2c
|
|||
|
731f9865c6
|
|||
|
be9d4115d0
|
|||
|
c6c8501640
|
|||
|
2641e80832
|
|||
|
7c30522906
|
|||
|
b93bb29777
|
|||
|
bf6d22bc38
|
|||
|
|
5265ef835e | ||
|
39b22690f1
|
|||
|
2a99e0912b
|
|||
|
|
a25c41150e |
32
.codeclimate.yml
Normal 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/
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
[run]
|
[run]
|
||||||
omit = */migrations/*
|
source =
|
||||||
*/tests/*
|
./
|
||||||
*/site-packages/*
|
|
||||||
*/distutils/*
|
omit =
|
||||||
|
*/migrations/*
|
||||||
|
|||||||
2
.csslintrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
--exclude-exts=.min.css
|
||||||
|
--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes
|
||||||
1
.eslintignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
**/*{.,-}min.js
|
||||||
213
.eslintrc
Normal 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
|
||||||
14
.github/workflows/deploy.yml
vendored
@@ -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"
|
|
||||||
58
.github/workflows/django.yml
vendored
@@ -1,58 +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 }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: 3.9.1
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
id: pcache
|
|
||||||
with:
|
|
||||||
path: ~/.local/share/virtualenvs
|
|
||||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pipenv-
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip pipenv
|
|
||||||
pipenv install -d
|
|
||||||
# if: steps.pcache.outputs.cache-hit != 'true'
|
|
||||||
- name: Cache Static Files
|
|
||||||
id: static-cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: 'pipeline/built_assets'
|
|
||||||
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
|
||||||
- uses: bahmutov/npm-install@v1
|
|
||||||
if: steps.static-cache.outputs.cache-hit != 'true'
|
|
||||||
- run: node node_modules/gulp/bin/gulp build
|
|
||||||
if: steps.static-cache.outputs.cache-hit != 'true'
|
|
||||||
- name: Basic Checks
|
|
||||||
run: |
|
|
||||||
pipenv run pycodestyle . --exclude=migrations,node_modules
|
|
||||||
pipenv run python manage.py check
|
|
||||||
pipenv run python manage.py makemigrations --check --dry-run
|
|
||||||
pipenv run python manage.py collectstatic --noinput
|
|
||||||
- name: Run Tests
|
|
||||||
run: pipenv run pytest -n auto -vv --cov
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: failure-screenshots ${{ matrix.test-group }}
|
|
||||||
path: screenshots/
|
|
||||||
retention-days: 5
|
|
||||||
- name: Coveralls
|
|
||||||
run: pipenv run coveralls --service=github
|
|
||||||
18
.gitignore
vendored
@@ -25,8 +25,6 @@ var/
|
|||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
node_modules/
|
|
||||||
data/
|
|
||||||
|
|
||||||
# Continer extras
|
# Continer extras
|
||||||
.vagrant
|
.vagrant
|
||||||
@@ -56,6 +54,7 @@ coverage.xml
|
|||||||
|
|
||||||
# Django stuff:
|
# Django stuff:
|
||||||
*.log
|
*.log
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
# Sphinx documentation
|
# Sphinx documentation
|
||||||
docs/_build/
|
docs/_build/
|
||||||
@@ -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,3 @@ com_crashlytics_export_strings.xml
|
|||||||
crashlytics.properties
|
crashlytics.properties
|
||||||
crashlytics-build.properties
|
crashlytics-build.properties
|
||||||
.vscode/
|
.vscode/
|
||||||
screenshots/
|
|
||||||
|
|||||||
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
|||||||
|
PyRIGS
|
||||||
5
.idea/encodings.xml
generated
Normal 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
@@ -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
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="DependencyValidationManager">
|
||||||
|
<state>
|
||||||
|
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
7
.idea/vcs.xml
generated
Normal 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
@@ -1,6 +1,7 @@
|
|||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
*.scss
|
||||||
*.md
|
*.md
|
||||||
**/tests
|
*.rb
|
||||||
conftest.py
|
Vagrantfile
|
||||||
pytest.ini
|
config/vagrant/*
|
||||||
Dockerfile
|
config/vagrant.yml
|
||||||
|
|||||||
32
.travis.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
language: python
|
||||||
|
python:
|
||||||
|
"3.6"
|
||||||
|
cache: pip
|
||||||
|
|
||||||
|
addons:
|
||||||
|
chrome: stable
|
||||||
|
|
||||||
|
install:
|
||||||
|
- wget https://chromedriver.storage.googleapis.com/2.36/chromedriver_linux64.zip
|
||||||
|
- unzip chromedriver_linux64.zip
|
||||||
|
- export PATH=$PATH:$(pwd)
|
||||||
|
- chmod +x chromedriver
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
- pip install coveralls codeclimate-test-reporter pep8
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- export PATH=$PATH:/usr/lib/chromium-browser/
|
||||||
|
- python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
script:
|
||||||
|
- pep8 . --exclude=migrations,importer*
|
||||||
|
- python manage.py check
|
||||||
|
- python manage.py makemigrations --check --dry-run
|
||||||
|
- coverage run manage.py test --verbosity=2
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- coveralls
|
||||||
|
- codeclimate-test-reporter
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
webhooks: https://fathomless-fjord-24024.herokuapp.com/notify
|
||||||
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM python:3.6
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ADD . /app
|
||||||
|
|
||||||
|
RUN pip install -r requirements.txt && \
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||||
103
Pipfile
@@ -1,103 +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"
|
|
||||||
certifi = "*"
|
|
||||||
chardet = "~=4.0.0"
|
|
||||||
configparser = "~=5.0.1"
|
|
||||||
contextlib2 = "~=0.6.0.post1"
|
|
||||||
cssselect = "~=1.1.0"
|
|
||||||
cssutils = "~=1.0.2"
|
|
||||||
dj-database-url = "~=0.5.0"
|
|
||||||
dj-static = "~=0.0.6"
|
|
||||||
Django = "~=3.2"
|
|
||||||
django-debug-toolbar = "~=3.2"
|
|
||||||
django-filter = "~=2.4.0"
|
|
||||||
django-ical = "~=1.7.1"
|
|
||||||
django-recurrence = "~=1.10.3"
|
|
||||||
django-registration-redux = "~=2.9"
|
|
||||||
django-reversion = "~=3.0.9"
|
|
||||||
django-toolbelt = "~=0.0.1"
|
|
||||||
django-widget-tweaks = "~=1.4.8"
|
|
||||||
django-htmlmin = "~=0.11.0"
|
|
||||||
envparse = "~=0.2.0"
|
|
||||||
gunicorn = "~=20.0.4"
|
|
||||||
icalendar = "~=4.0.7"
|
|
||||||
idna = "~=2.10"
|
|
||||||
lxml = "*"
|
|
||||||
Markdown = "~=3.3.3"
|
|
||||||
msgpack = "~=1.0.2"
|
|
||||||
pep517 = "~=0.9.1"
|
|
||||||
Pillow = "~=9.0.0"
|
|
||||||
premailer = "~=3.7.0"
|
|
||||||
progress = "~=1.5"
|
|
||||||
psutil = "~=5.8.0"
|
|
||||||
psycopg2 = "~=2.8.6"
|
|
||||||
Pygments = "~=2.7.4"
|
|
||||||
pyparsing = "~=2.4.7"
|
|
||||||
PyPDF2 = "~=1.27.5"
|
|
||||||
python-dateutil = "~=2.8.1"
|
|
||||||
pytoml = "~=0.1.21"
|
|
||||||
pytz = "~=2020.5"
|
|
||||||
reportlab = "*"
|
|
||||||
requests = "~=2.25.1"
|
|
||||||
retrying = "~=1.3.3"
|
|
||||||
simplejson = "~=3.17.2"
|
|
||||||
six = "~=1.15.0"
|
|
||||||
soupsieve = "~=2.1"
|
|
||||||
sqlparse = "~=0.4.2"
|
|
||||||
static3 = "~=0.7.0"
|
|
||||||
svg2rlg = "~=0.3"
|
|
||||||
tini = "~=3.0.1"
|
|
||||||
tornado = "~=6.1"
|
|
||||||
urllib3 = "~=1.26.5"
|
|
||||||
whitenoise = "~=5.2.0"
|
|
||||||
yolk = "~=0.4.3"
|
|
||||||
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 = "*"
|
|
||||||
|
|
||||||
[dev-packages]
|
|
||||||
selenium = "~=3.141.0"
|
|
||||||
pycodestyle = "*"
|
|
||||||
coveralls = "*"
|
|
||||||
django-coverage-plugin = "*"
|
|
||||||
pytest-cov = "*"
|
|
||||||
pytest-django = "*"
|
|
||||||
pluggy = "*"
|
|
||||||
pytest-splinter = "*"
|
|
||||||
pytest = "*"
|
|
||||||
pytest-reverse = "*"
|
|
||||||
pytest-xdist = {extras = [ "psutil",], version = "*"}
|
|
||||||
PyPOM = {extras = [ "splinter",], version = "*"}
|
|
||||||
|
|
||||||
[requires]
|
|
||||||
python_version = "3.9"
|
|
||||||
|
|
||||||
[pipenv]
|
|
||||||
allow_prereleases = true
|
|
||||||
1609
Pipfile.lock
generated
@@ -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,17 @@ 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'], reverse(oembed_view, kwargs=kwargs))
|
||||||
context['login_url'] = f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}"
|
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
||||||
resp = render(request, 'login_redirect.html', context=context)
|
resp = render(request, 'login_redirect.html', context=context)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
def has_oembed(oembed_view, login_url=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,12 +27,10 @@ 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__
|
||||||
return _checklogin
|
return _checklogin
|
||||||
|
|
||||||
return _dec
|
return _dec
|
||||||
|
|
||||||
|
|
||||||
@@ -54,16 +55,14 @@ 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
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
_checklogin.__doc__ = view_func.__doc__
|
_checklogin.__doc__ = view_func.__doc__
|
||||||
_checklogin.__dict__ = view_func.__dict__
|
_checklogin.__dict__ = view_func.__dict__
|
||||||
return _checklogin
|
return _checklogin
|
||||||
|
|
||||||
return _dec
|
return _dec
|
||||||
|
|
||||||
|
|
||||||
@@ -81,7 +80,6 @@ def api_key_required(function):
|
|||||||
Failed users will be given a 403 error.
|
Failed users will be given a 403 error.
|
||||||
Should only be used for urls which include <api_pk> and <api_key> kwargs
|
Should only be used for urls which include <api_pk> and <api_key> kwargs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def wrap(request, *args, **kwargs):
|
def wrap(request, *args, **kwargs):
|
||||||
|
|
||||||
userid = kwargs.get('api_pk')
|
userid = kwargs.get('api_pk')
|
||||||
@@ -103,7 +101,6 @@ def api_key_required(function):
|
|||||||
if user_object.api_key != key:
|
if user_object.api_key != key:
|
||||||
return error_resp
|
return error_resp
|
||||||
return function(request, *args, **kwargs)
|
return function(request, *args, **kwargs)
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
@@ -111,13 +108,11 @@ def nottinghamtec_address_required(function):
|
|||||||
"""
|
"""
|
||||||
Checks that the current user has an email address ending @nottinghamtec.co.uk
|
Checks that the current user has an email address ending @nottinghamtec.co.uk
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def wrap(request, *args, **kwargs):
|
def wrap(request, *args, **kwargs):
|
||||||
# Fail if current user's email address isn't @nottinghamtec.co.uk
|
# Fail if current user's email address isn't @nottinghamtec.co.uk
|
||||||
if not request.user.email.endswith('@nottinghamtec.co.uk'):
|
if not request.user.email.endswith('@nottinghamtec.co.uk'):
|
||||||
error_resp = render(request, 'eventauthorisation_request_error.html')
|
error_resp = render(request, 'RIGS/eventauthorisation_request_error.html')
|
||||||
return error_resp
|
return error_resp
|
||||||
|
|
||||||
return function(request, *args, **kwargs)
|
return function(request, *args, **kwargs)
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
DATETIME_FORMAT = ('d/m/Y H:i')
|
DATETIME_FORMAT = ('d/m/Y H:i')
|
||||||
DATE_FORMAT = ('d/m/Y')
|
DATE_FORMAT = ('d/m/Y')
|
||||||
TIME_FORMAT = ('H:i')
|
TIME_FORMAT = ('H:i')
|
||||||
|
|||||||
@@ -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 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,38 +44,33 @@ if not DEBUG:
|
|||||||
|
|
||||||
INTERNAL_IPS = ['127.0.0.1']
|
INTERNAL_IPS = ['127.0.0.1']
|
||||||
|
|
||||||
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
|
ADMINS = (
|
||||||
('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
|
('Tom Price', 'tomtom5152@gmail.com')
|
||||||
if DEBUG:
|
)
|
||||||
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',
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django.contrib.humanize',
|
|
||||||
'versioning',
|
|
||||||
'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',
|
||||||
@@ -82,8 +79,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'
|
||||||
@@ -91,10 +86,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'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,33 +147,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'
|
||||||
@@ -188,26 +163,25 @@ 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', "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key
|
||||||
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000')
|
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
|
||||||
|
NOCAPTCHA = True
|
||||||
|
|
||||||
# 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'
|
||||||
|
|
||||||
EMAIL_COOLDOWN = datetime.timedelta(minutes=15)
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
||||||
|
|
||||||
@@ -223,22 +197,22 @@ USE_L10N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
# Need to allow seconds as datetime-local input type spits out a time that has seconds
|
|
||||||
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
|
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'
|
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
||||||
|
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': {
|
||||||
@@ -261,5 +235,7 @@ 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(
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
'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)
|
||||||
|
|||||||
@@ -1,32 +1,17 @@
|
|||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def create_datetime(year, month, day, hour, minute):
|
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
|
||||||
return tz.localize(datetime(year, month, day, hour, minute)).astimezone(tz)
|
|
||||||
|
|
||||||
|
|
||||||
def create_browser():
|
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")
|
if os.environ.get('CI', False):
|
||||||
if settings.CI:
|
options.add_argument("--headless")
|
||||||
options.add_argument("--no-sandbox")
|
options.add_argument("--no-sandbox")
|
||||||
driver = webdriver.Chrome(options=options)
|
driver = webdriver.Chrome(chrome_options=options)
|
||||||
return driver
|
return driver
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +19,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,58 +32,5 @@ 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 wrapper_func(self, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
func(self, *args, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
screenshot_name = func.__module__ + "." + func.__qualname__
|
|
||||||
screenshot_file = "screenshots/" + func.__qualname__ + ".png"
|
|
||||||
if not pathlib.Path("screenshots").is_dir():
|
|
||||||
os.mkdir("screenshots")
|
|
||||||
self.driver.save_screenshot(screenshot_file)
|
|
||||||
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
return wrapper_func
|
|
||||||
|
|
||||||
|
|
||||||
def screenshot_failure_cls(cls):
|
|
||||||
for attr in cls.__dict__:
|
|
||||||
if callable(getattr(cls, attr)) and attr.startswith("test"):
|
|
||||||
setattr(cls, attr, screenshot_failure(getattr(cls, attr)))
|
|
||||||
return cls
|
|
||||||
|
|
||||||
|
|
||||||
def assert_times_almost_equal(first_time, second_time):
|
|
||||||
assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0)
|
|
||||||
|
|
||||||
|
|
||||||
def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url):
|
|
||||||
# Test the meta tag is in place
|
|
||||||
response = client.get(event_url, follow=True, HTTP_HOST='example.com')
|
|
||||||
assertContains(response, 'application/json+oembed')
|
|
||||||
assertContains(response, oembed_url)
|
|
||||||
# Test that the JSON exists
|
|
||||||
response = client.get(oembed_url, follow=True, HTTP_HOST='example.com')
|
|
||||||
assert response.status_code == 200
|
|
||||||
assertContains(response, event_embed_url)
|
|
||||||
# Should also work for non-existant events
|
|
||||||
response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
|
|
||||||
assert response.status_code == 200
|
|
||||||
assertContains(response, alt_event_embed_url)
|
|
||||||
|
|
||||||
|
|
||||||
def login(client, django_user_model):
|
|
||||||
pwd = 'testuser'
|
|
||||||
usr = 'TestUser'
|
|
||||||
user = django_user_model.objects.create_user(username=usr, email="TestUser@test.com", password=pwd,
|
|
||||||
is_superuser=True,
|
|
||||||
is_active=True, is_staff=True)
|
|
||||||
assert client.login(username=usr, password=pwd)
|
|
||||||
return user
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
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.by import By
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver import Chrome
|
||||||
from PyRIGS.tests import regions
|
from selenium.common.exceptions import NoSuchElementException
|
||||||
|
|
||||||
|
|
||||||
class BasePage(Page):
|
class BasePage(Page):
|
||||||
@@ -31,30 +29,42 @@ 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("Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
||||||
"Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
self.driver.execute_script("Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
||||||
self.driver.execute_script(
|
|
||||||
"Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
|
||||||
|
|
||||||
def submit(self):
|
|
||||||
previous_errors = self.errors
|
|
||||||
submit = self.find_element(*self._submit_locator)
|
|
||||||
ActionChains(self.driver).move_to_element(submit).perform()
|
|
||||||
submit.click()
|
|
||||||
self.wait.until(animation_is_finished())
|
|
||||||
self.wait.until(lambda x: self.errors != previous_errors or self.success)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def errors(self):
|
def errors(self):
|
||||||
try:
|
try:
|
||||||
error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector))
|
error_page = self.ErrorPage(self, self.find_element(*self._errors_selector))
|
||||||
return error_page.errors
|
return error_page.errors
|
||||||
except NoSuchElementException:
|
except NoSuchElementException:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
class ErrorPage(Region):
|
||||||
|
_error_item_selector = (By.CSS_SELECTOR, "dl>span")
|
||||||
|
|
||||||
|
class ErrorItem(Region):
|
||||||
|
_field_selector = (By.CSS_SELECTOR, "dt")
|
||||||
|
_error_selector = (By.CSS_SELECTOR, "dd>ul>li")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def field_name(self):
|
||||||
|
return self.find_element(*self._field_selector).text
|
||||||
|
|
||||||
|
@property
|
||||||
|
def errors(self):
|
||||||
|
return [x.text for x in self.find_elements(*self._error_selector)]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def errors(self):
|
||||||
|
error_items = [self.ErrorItem(self, x) for x in self.find_elements(*self._error_item_selector)]
|
||||||
|
errors = {}
|
||||||
|
for error in error_items:
|
||||||
|
errors[error.field_name] = error.errors
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
class LoginPage(BasePage):
|
class LoginPage(BasePage):
|
||||||
URL_TEMPLATE = '/user/login'
|
URL_TEMPLATE = '/user/login'
|
||||||
@@ -74,13 +84,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
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import datetime
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from pypom import Region
|
from pypom import Region
|
||||||
from selenium.common.exceptions import NoSuchElementException
|
|
||||||
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
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
def parse_bool_from_string(string):
|
def parse_bool_from_string(string):
|
||||||
@@ -19,25 +17,10 @@ def parse_bool_from_string(string):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_time_format():
|
|
||||||
# Default
|
|
||||||
time_format = "%H%M"
|
|
||||||
if settings.CI: # The CI is American
|
|
||||||
time_format = "%I%M%p"
|
|
||||||
return time_format
|
|
||||||
|
|
||||||
|
|
||||||
def get_date_format():
|
|
||||||
date_format = "%d%m%Y"
|
|
||||||
if settings.CI: # And try as I might I can't stop it being so
|
|
||||||
date_format = "%m%d%Y"
|
|
||||||
return date_format
|
|
||||||
|
|
||||||
|
|
||||||
class BootstrapSelectElement(Region):
|
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')
|
||||||
_option_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu.inner>li>a.dropdown-item')
|
_option_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu.inner>li>a[role=option]')
|
||||||
_select_all_locator = (By.CLASS_NAME, 'bs-select-all')
|
_select_all_locator = (By.CLASS_NAME, 'bs-select-all')
|
||||||
_deselect_all_locator = (By.CLASS_NAME, 'bs-deselect-all')
|
_deselect_all_locator = (By.CLASS_NAME, 'bs-deselect-all')
|
||||||
_search_locator = (By.CSS_SELECTOR, '.bs-searchbox>input')
|
_search_locator = (By.CSS_SELECTOR, '.bs-searchbox>input')
|
||||||
@@ -49,12 +32,12 @@ class BootstrapSelectElement(Region):
|
|||||||
|
|
||||||
def toggle(self):
|
def toggle(self):
|
||||||
original_state = self.is_open
|
original_state = self.is_open
|
||||||
option_box = self.find_element(*self._option_box_locator)
|
|
||||||
if not original_state:
|
|
||||||
self.wait.until(expected_conditions.invisibility_of_element(option_box))
|
|
||||||
else:
|
|
||||||
self.wait.until(expected_conditions.visibility_of(option_box))
|
|
||||||
return self.find_element(*self._main_button_locator).click()
|
return self.find_element(*self._main_button_locator).click()
|
||||||
|
option_box = self.find_element(*self._option_box_locator)
|
||||||
|
if original_state:
|
||||||
|
self.wait.until(expected_conditions.invisibility_of_element_located(option_box))
|
||||||
|
else:
|
||||||
|
self.wait.until(expected_conditions.visibility_of_element_located(option_box))
|
||||||
|
|
||||||
def open(self):
|
def open(self):
|
||||||
if not self.is_open:
|
if not self.is_open:
|
||||||
@@ -71,11 +54,10 @@ 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()
|
|
||||||
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 +66,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 +99,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 +112,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,33 +119,7 @@ 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):
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
return datetime.datetime.strptime(self.root.get_attribute("value"), "%H:%M")
|
|
||||||
|
|
||||||
def set_value(self, value):
|
|
||||||
self.root.clear()
|
|
||||||
self.root.send_keys(value.strftime(get_time_format()))
|
|
||||||
|
|
||||||
|
|
||||||
class DateTimePicker(Region):
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d %H:%M")
|
|
||||||
|
|
||||||
def set_value(self, value):
|
|
||||||
self.root.clear()
|
|
||||||
|
|
||||||
date = value.date().strftime(get_date_format())
|
|
||||||
time = value.time().strftime(get_time_format())
|
|
||||||
|
|
||||||
self.root.send_keys(date)
|
|
||||||
self.root.send_keys(Keys.TAB)
|
|
||||||
self.root.send_keys(time)
|
|
||||||
|
|
||||||
|
|
||||||
class SingleSelectPicker(Region):
|
class SingleSelectPicker(Region):
|
||||||
@@ -200,63 +131,3 @@ class SingleSelectPicker(Region):
|
|||||||
def set_value(self, value):
|
def set_value(self, value):
|
||||||
picker = Select(self.root)
|
picker = Select(self.root)
|
||||||
picker.select_by_visible_text(value)
|
picker.select_by_visible_text(value)
|
||||||
|
|
||||||
|
|
||||||
class ErrorPage(Region):
|
|
||||||
_error_item_selector = (By.CSS_SELECTOR, "dl>span")
|
|
||||||
|
|
||||||
class ErrorItem(Region):
|
|
||||||
_field_selector = (By.CSS_SELECTOR, "dt")
|
|
||||||
_error_selector = (By.CSS_SELECTOR, "dd>ul>li")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def field_name(self):
|
|
||||||
return self.find_element(*self._field_selector).text
|
|
||||||
|
|
||||||
@property
|
|
||||||
def errors(self):
|
|
||||||
return [x.text for x in self.find_elements(*self._error_selector)]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def errors(self):
|
|
||||||
error_items = [self.ErrorItem(self, x) for x in self.find_elements(*self._error_item_selector)]
|
|
||||||
errors = {}
|
|
||||||
for error in error_items:
|
|
||||||
errors[error.field_name] = error.errors
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
class Modal(Region):
|
|
||||||
_submit_locator = (By.CSS_SELECTOR, '.btn-primary')
|
|
||||||
_header_selector = (By.TAG_NAME, 'h4')
|
|
||||||
|
|
||||||
form_items = {
|
|
||||||
'name': (TextBox, (By.ID, 'id_name'))
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def header(self):
|
|
||||||
return self.find_element(*self._header_selector).text
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_open(self):
|
|
||||||
return self.root.is_displayed()
|
|
||||||
|
|
||||||
def submit(self):
|
|
||||||
self.root.find_element(*self._submit_locator).click()
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
if name in self.form_items:
|
|
||||||
element = self.form_items[name]
|
|
||||||
form_element = element[0](self, self.find_element(*element[1]))
|
|
||||||
return form_element.value
|
|
||||||
else:
|
|
||||||
return super().__getattribute__(name)
|
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
|
||||||
if name in self.form_items:
|
|
||||||
element = self.form_items[name]
|
|
||||||
form_element = element[0](self, self.find_element(*element[1]))
|
|
||||||
form_element.set_value(value)
|
|
||||||
else:
|
|
||||||
self.__dict__[name] = value
|
|
||||||
|
|||||||
@@ -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 Asset.objects.all().count() == 0
|
|
||||||
assert Event.objects.all().count() == 0
|
|
||||||
|
|
||||||
|
|
||||||
@override_settings(DEBUG=True)
|
|
||||||
@pytest.mark.skip(reason="broken")
|
|
||||||
def test_unauthenticated(client): # Nothing should be available to the unauthenticated
|
|
||||||
call_command('generateSampleData')
|
|
||||||
for url in find_urls_recursive(urls.urlpatterns):
|
|
||||||
request_url = get_request_url(url)
|
|
||||||
if request_url and 'user' not in request_url: # User module is full of edge cases
|
|
||||||
response = client.get(request_url, follow=True, HTTP_HOST='example.com')
|
|
||||||
assertContains(response, 'Login')
|
|
||||||
if 'application/json+oembed' in response.content.decode():
|
|
||||||
assertTemplateUsed(response, 'login_redirect.html')
|
|
||||||
else:
|
|
||||||
if "embed" in str(url):
|
|
||||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
|
||||||
else:
|
|
||||||
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
|
|
||||||
assertRedirects(response, expected_url)
|
|
||||||
call_command('deleteSampleData')
|
|
||||||
|
|
||||||
|
|
||||||
@override_settings(DEBUG=True)
|
|
||||||
@pytest.mark.skip(reason="broken")
|
|
||||||
def test_basic_access(client):
|
|
||||||
call_command('generateSampleData')
|
|
||||||
assert client.login(username="basic", password="basic")
|
|
||||||
|
|
||||||
url = reverse('asset_list')
|
|
||||||
response = client.get(url)
|
|
||||||
# Check edit and duplicate buttons NOT shown in list
|
|
||||||
assertNotContains(response, 'Edit')
|
|
||||||
assertNotContains(response,
|
|
||||||
'Duplicate') # If this line is randomly failing, check the debug toolbar HTML hasn't crept in
|
|
||||||
|
|
||||||
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
|
|
||||||
response = client.get(url)
|
|
||||||
assertNotContains(response, 'Purchase Details')
|
|
||||||
assertNotContains(response, 'View Revision History')
|
|
||||||
|
|
||||||
urlz = {'asset_history', 'asset_update', 'asset_duplicate'}
|
|
||||||
for url_name in urlz:
|
|
||||||
request_url = reverse(url_name, kwargs={'pk': Asset.objects.first().asset_id})
|
|
||||||
response = client.get(request_url, follow=True)
|
|
||||||
assert response.status_code == 403
|
|
||||||
|
|
||||||
request_url = reverse('supplier_create')
|
|
||||||
response = client.get(request_url, follow=True)
|
|
||||||
assert response.status_code == 403
|
|
||||||
|
|
||||||
request_url = reverse('supplier_update', kwargs={'pk': 1})
|
|
||||||
response = client.get(request_url, follow=True)
|
|
||||||
assert response.status_code == 403
|
|
||||||
client.logout()
|
|
||||||
call_command('deleteSampleData')
|
|
||||||
|
|
||||||
|
|
||||||
@override_settings(DEBUG=True)
|
|
||||||
@pytest.mark.skip(reason="broken")
|
|
||||||
def test_keyholder_access(client):
|
|
||||||
call_command('generateSampleData')
|
|
||||||
assert client.login(username="keyholder", password="keyholder")
|
|
||||||
|
|
||||||
url = reverse('asset_list')
|
|
||||||
response = client.get(url)
|
|
||||||
# Check edit and duplicate buttons shown in list
|
|
||||||
assertContains(response, 'Edit')
|
|
||||||
assertContains(response, 'Duplicate')
|
|
||||||
|
|
||||||
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
|
|
||||||
response = client.get(url)
|
|
||||||
assertContains(response, 'Purchase Details')
|
|
||||||
assertContains(response, 'View Revision History')
|
|
||||||
client.logout()
|
|
||||||
call_command('deleteSampleData')
|
|
||||||
|
|
||||||
|
|
||||||
def test_search(admin_client, admin_user):
|
|
||||||
url = reverse('search')
|
|
||||||
response = admin_client.get(url, {'q': "Definetelynothingfoundifwesearchthis"})
|
|
||||||
assertContains(response, "No results found")
|
|
||||||
response = admin_client.get(url, {'q': admin_user.first_name})
|
|
||||||
assertContains(response, admin_user.first_name)
|
|
||||||
@@ -1,43 +1,30 @@
|
|||||||
from django.conf import settings
|
from django.conf.urls import include, url
|
||||||
from django.conf.urls import include
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||||
from django.urls import path
|
from django.conf import settings
|
||||||
from django.views.generic import TemplateView
|
from registration.backends.default.views import RegistrationView
|
||||||
|
import RIGS
|
||||||
from PyRIGS import views
|
from RIGS import regbackend
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include('versioning.urls')),
|
# Examples:
|
||||||
path('', include('RIGS.urls')),
|
# url(r'^$', 'PyRIGS.views.home', name='home'),
|
||||||
path('assets/', include('assets.urls')),
|
# url(r'^blog/', include('blog.urls')),
|
||||||
path('training/', include('training.urls')),
|
|
||||||
|
|
||||||
path('', login_required(views.Index.as_view()), name='index'),
|
url(r'^', include('RIGS.urls')),
|
||||||
|
url('^assets/', include('assets.urls')),
|
||||||
|
url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail),
|
||||||
|
name="registration_register"),
|
||||||
|
url('^user/', include('django.contrib.auth.urls')),
|
||||||
|
url('^user/', include('registration.backends.default.urls')),
|
||||||
|
|
||||||
# API
|
url(r'^admin/', admin.site.urls),
|
||||||
path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()),
|
|
||||||
name="api_secure"),
|
|
||||||
path('api/<str:model>/<int:pk>/', login_required(views.SecureAPIRequest.as_view()),
|
|
||||||
name="api_secure"),
|
|
||||||
|
|
||||||
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
|
|
||||||
path('search/', login_required(views.Search.as_view()), name='search'),
|
|
||||||
path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'),
|
|
||||||
|
|
||||||
path('', include('users.urls')),
|
|
||||||
|
|
||||||
path('admin/', include('massadmin.urls')),
|
|
||||||
path('admin/', admin.site.urls),
|
|
||||||
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
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
|
||||||
]
|
|
||||||
|
|||||||
365
PyRIGS/views.py
@@ -1,365 +0,0 @@
|
|||||||
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.db.models import Q
|
|
||||||
from django.http import HttpResponse, JsonResponse
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
|
||||||
from django.views import generic
|
|
||||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
|
||||||
from django.template.loader import get_template
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from RIGS import models
|
|
||||||
from assets import models as asset_models
|
|
||||||
from training import models as training_models
|
|
||||||
|
|
||||||
|
|
||||||
def is_ajax(request):
|
|
||||||
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
|
|
||||||
|
|
||||||
|
|
||||||
def get_related(form, context): # Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
|
||||||
for field, model in form.related_models.items():
|
|
||||||
value = form[field].value()
|
|
||||||
if value is not None and value != '':
|
|
||||||
context[field] = model.objects.get(pk=value)
|
|
||||||
|
|
||||||
|
|
||||||
class Index(generic.TemplateView): # Displays the current rig count along with a few other bits and pieces
|
|
||||||
template_name = 'index.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['rig_count'] = models.Event.objects.rig_count()
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class SecureAPIRequest(generic.View):
|
|
||||||
models = {
|
|
||||||
'venue': models.Venue,
|
|
||||||
'person': models.Person,
|
|
||||||
'organisation': models.Organisation,
|
|
||||||
'profile': models.Profile,
|
|
||||||
'event': models.Event,
|
|
||||||
'asset': asset_models.Asset,
|
|
||||||
'supplier': asset_models.Supplier,
|
|
||||||
'training_item': training_models.TrainingItem,
|
|
||||||
}
|
|
||||||
|
|
||||||
perms = {
|
|
||||||
'venue': 'RIGS.view_venue',
|
|
||||||
'person': 'RIGS.view_person',
|
|
||||||
'organisation': 'RIGS.view_organisation',
|
|
||||||
'profile': 'RIGS.view_profile',
|
|
||||||
'event': None,
|
|
||||||
'asset': None,
|
|
||||||
'supplier': None,
|
|
||||||
'training_item': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
'''
|
|
||||||
Validate the request is allowed based on user permissions.
|
|
||||||
Raises 403 if denied.
|
|
||||||
Potential to add API key validation at a later date.
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __validate__(self, request, key, perm):
|
|
||||||
if request.user.is_active:
|
|
||||||
if request.user.is_superuser or perm is None:
|
|
||||||
return True
|
|
||||||
elif request.user.has_perm(perm):
|
|
||||||
return True
|
|
||||||
raise PermissionDenied()
|
|
||||||
|
|
||||||
def get(self, request, model, pk=None, param=None):
|
|
||||||
# Request permission validation things
|
|
||||||
key = request.GET.get('apikey', None)
|
|
||||||
perm = self.perms[model]
|
|
||||||
self.__validate__(request, key, perm)
|
|
||||||
|
|
||||||
# Response format where applicable
|
|
||||||
format = request.GET.get('format', 'json')
|
|
||||||
fields = request.GET.get('fields', None)
|
|
||||||
if fields:
|
|
||||||
fields = fields.split(",")
|
|
||||||
filters = request.GET.get('filters', [])
|
|
||||||
if filters:
|
|
||||||
filters = filters.split(",")
|
|
||||||
|
|
||||||
# Supply data for one record
|
|
||||||
if pk:
|
|
||||||
object = get_object_or_404(self.models[model], pk=pk)
|
|
||||||
data = serializers.serialize(format, [object], fields=fields)
|
|
||||||
return HttpResponse(data, content_type="application/" + format)
|
|
||||||
|
|
||||||
# Supply data for autocomplete ajax request in json form
|
|
||||||
term = request.GET.get('q', None)
|
|
||||||
if term:
|
|
||||||
if fields is None: # Default to just name
|
|
||||||
fields = ['name']
|
|
||||||
|
|
||||||
# Build a list of Q objects for use later
|
|
||||||
queries = []
|
|
||||||
for part in term.split(" "):
|
|
||||||
qs = []
|
|
||||||
for field in fields:
|
|
||||||
q = Q(**{field + "__icontains": part})
|
|
||||||
qs.append(q)
|
|
||||||
|
|
||||||
queries.append(reduce(operator.or_, qs))
|
|
||||||
|
|
||||||
for f in filters:
|
|
||||||
q = Q(**{f: True})
|
|
||||||
queries.append(q)
|
|
||||||
|
|
||||||
# Build the data response list
|
|
||||||
results = []
|
|
||||||
query = reduce(operator.and_, queries)
|
|
||||||
objects = self.models[model].objects.filter(query)
|
|
||||||
for o in objects:
|
|
||||||
data = {
|
|
||||||
'pk': o.pk,
|
|
||||||
'value': o.pk,
|
|
||||||
'text': o.name,
|
|
||||||
}
|
|
||||||
try: # See if there is a valid update URL
|
|
||||||
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
|
|
||||||
except NoReverseMatch:
|
|
||||||
pass
|
|
||||||
results.append(data)
|
|
||||||
|
|
||||||
# return a data response
|
|
||||||
return JsonResponse(results, safe=False)
|
|
||||||
|
|
||||||
start = request.GET.get('start', None)
|
|
||||||
end = request.GET.get('end', None)
|
|
||||||
|
|
||||||
if model == "event" and start and end:
|
|
||||||
# Probably a calendar request
|
|
||||||
start_datetime = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S")
|
|
||||||
end_datetime = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S")
|
|
||||||
|
|
||||||
objects = self.models[model].objects.events_in_bounds(start_datetime, end_datetime)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for item in objects:
|
|
||||||
data = {
|
|
||||||
'pk': item.pk,
|
|
||||||
'title': item.name,
|
|
||||||
'is_rig': item.is_rig,
|
|
||||||
'status': str(item.get_status_display()),
|
|
||||||
'earliest': item.earliest_time.isoformat(),
|
|
||||||
'latest': item.latest_time.isoformat(),
|
|
||||||
'url': str(item.get_absolute_url())
|
|
||||||
}
|
|
||||||
|
|
||||||
results.append(data)
|
|
||||||
return JsonResponse(results, safe=False)
|
|
||||||
|
|
||||||
return HttpResponse(model)
|
|
||||||
|
|
||||||
|
|
||||||
class ModalURLMixin:
|
|
||||||
def get_close_url(self, update, detail):
|
|
||||||
if is_ajax(self.request):
|
|
||||||
url = reverse_lazy('closemodal')
|
|
||||||
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
|
|
||||||
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
|
|
||||||
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
|
|
||||||
else:
|
|
||||||
url = reverse_lazy(detail, kwargs={
|
|
||||||
'pk': self.object.pk,
|
|
||||||
})
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
class GenericListView(generic.ListView):
|
|
||||||
template_name = 'generic_list.html'
|
|
||||||
paginate_by = 20
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().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):
|
|
||||||
object_list = self.model.objects.search(query=self.request.GET.get('q', ""))
|
|
||||||
|
|
||||||
orderBy = self.request.GET.get('orderBy', "name")
|
|
||||||
if orderBy != "":
|
|
||||||
object_list = object_list.order_by(orderBy)
|
|
||||||
return object_list
|
|
||||||
|
|
||||||
|
|
||||||
class GenericDetailView(generic.DetailView):
|
|
||||||
template_name = "generic_detail.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().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)
|
|
||||||
|
|
||||||
|
|
||||||
class PrintView(generic.View):
|
|
||||||
append_terms = False
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
obj = get_object_or_404(self.model, pk=self.kwargs['pk'])
|
|
||||||
user_str = f"by {self.request.user.name} " if self.request.user is not None else ""
|
|
||||||
time = timezone.now().strftime('%d/%m/%Y %H:%I')
|
|
||||||
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': f"[Paperwork generated {user_str}on {time} - {obj.current_version_id}]",
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get(self, request, pk):
|
|
||||||
template = get_template(self.template_name)
|
|
||||||
|
|
||||||
merger = PdfFileMerger()
|
|
||||||
|
|
||||||
context = self.get_context_data()
|
|
||||||
|
|
||||||
rml = template.render(context)
|
|
||||||
buffer = rml2pdf.parseString(rml)
|
|
||||||
merger.append(PdfFileReader(buffer))
|
|
||||||
buffer.close()
|
|
||||||
|
|
||||||
if self.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
|
|
||||||
@@ -7,7 +7,6 @@ For more information on this file, see
|
|||||||
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
|
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "PyRIGS.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "PyRIGS.settings")
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application # noqa
|
from django.core.wsgi import get_wsgi_application # noqa
|
||||||
|
|||||||
117
README.md
@@ -1,18 +1,111 @@
|
|||||||
# TEC PA & Lighting - PyRIGS #
|
# TEC PA & Lighting - PyRIGS #
|
||||||

|
[](https://travis-ci.org/nottinghamtec/PyRIGS)
|
||||||
[](https://coveralls.io/github/nottinghamtec/PyRIGS)
|
[](https://coveralls.io/github/nottinghamtec/PyRIGS)
|
||||||
[](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability)
|
|
||||||
|
|
||||||
Welcome to TEC PA & 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
|
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.
|
||||||
- versioning: Our custom logic built on top of django-reversion. Semi-modular.
|
|
||||||
- users: Our custom logic for registration and profiles. Semi-modular.
|
### Editing ###
|
||||||
- training: SoonTM
|
It is recommended that you use the PyCharm IDE by JetBrains. Whilst other editors are available, this is the best for integration with Django as it can automatically manage all the pesky admin commands that frequently need running, as well as nice integration with git.
|
||||||
|
|
||||||
|
For the more experienced developer/somebody who doesn't want a full IDE and wants it to open in less than the age of the universe, I can strongly recommend [Sublime Text](http://www.sublimetext.com/). It has a bit of a steeper learning curve, and won't manage anything Django/git related out of the box, but once you get the hang of it is by far the fastest and most powerful editor I have used (for any type of project).
|
||||||
|
|
||||||
|
Please contact TJP for details on how to acquire these.
|
||||||
|
|
||||||
|
### Python Environment ###
|
||||||
|
Whilst the Python version used is not critical to the running of the application, using the same version usually helps avoid a lot of issues. Orginally written with the C implementation of Python 2 (CPython 2, specifically the Python 2.7 standard), the application now runs in Python 3.
|
||||||
|
|
||||||
|
Once you have your Python distribution installed, go ahead an follow the steps to set up a virtualenv, which will isolate the project from the system environment.
|
||||||
|
|
||||||
|
#### PyCharm ####
|
||||||
|
If you are using the prefered PyCharm IDE, then this should be quite easy.
|
||||||
|
|
||||||
|
1. Select "File/Settings" -> "Project Interpreter"
|
||||||
|
2. Click the small cog in the top right
|
||||||
|
3. Select "Create VirtualEnv"
|
||||||
|
4. Enter a name and a location. This doesn't matter where, just make sure it makes sense and you remember it incase you need it later (I recommend calling it "pyrigs" in "~/.virtualenvs/pyrigs")
|
||||||
|
5. Select the base interpreter to your Python 3 base interpreter (Python 2 will work, just be careful)
|
||||||
|
6. Click OK, you *don't* want to inherit global packages or make it available to all projects.
|
||||||
|
7. Open a file such as manage.py. PyCharm should winge that dependances aren't installed. This might take a while to register, but give it change. When it does, click the button to install them and let it do it's thing. If for some reason PyCharm should decide that it doesn't want to help you here, see below for the console instructions on how to do this manually.
|
||||||
|
|
||||||
|
To run the Django application follow these steps
|
||||||
|
|
||||||
|
1. Select "Run/Edit Configurations"
|
||||||
|
2. Create a new "Django server", give it a sensible name for when you need it later.
|
||||||
|
3. You might need to set the interpreter to be your virtualenv.
|
||||||
|
4. Click "OK"
|
||||||
|
5. Run the application
|
||||||
|
|
||||||
|
#### Console Based ####
|
||||||
|
If you aren't using PyCharm, or want to use a console for some reason, this is really easy, there is even [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) to help things along. Simply run
|
||||||
|
```
|
||||||
|
virtualenv <dir>
|
||||||
|
```
|
||||||
|
Where dir is the directory you wish to create the virtualenv in.
|
||||||
|
|
||||||
|
Next activate the virtualenv.
|
||||||
|
```
|
||||||
|
Windows
|
||||||
|
<virtualenv_dir>/Scripts/activate.bat
|
||||||
|
|
||||||
|
Unix
|
||||||
|
source <virtualenv_dir>/bin/activate
|
||||||
|
```
|
||||||
|
Finally install the requirements using pip
|
||||||
|
```
|
||||||
|
cd <pyrigs project directory>
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
This might take a while, but be patient and you should then be ready to go.
|
||||||
|
|
||||||
|
To run the server under normal conditions when you are already in the virtualenv (see above)
|
||||||
|
```
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
Please refer to Django documentation for a full list of options available here.
|
||||||
|
|
||||||
|
### Development using docker
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build . -t pyrigs
|
||||||
|
docker run -it --rm -p=8000:8000 -v $(pwd):/app pyrigs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Data ###
|
||||||
|
Sample data is available to aid local development and user acceptance testing. To load this data into your local database, first ensure the database is empty:
|
||||||
|
```
|
||||||
|
python manage.py flush
|
||||||
|
```
|
||||||
|
Then load the sample data using the command:
|
||||||
|
```
|
||||||
|
python manage.py generateSampleData
|
||||||
|
```
|
||||||
|
4 user accounts are created for convenience:
|
||||||
|
|
||||||
|
|Username |Password |
|
||||||
|
|---------|---------|
|
||||||
|
|superuser|superuser|
|
||||||
|
|finance |finance |
|
||||||
|
|keyholder|keyholder|
|
||||||
|
|basic |basic |
|
||||||
|
|
||||||
|
### Testing ###
|
||||||
|
Tests are contained in 3 files. `RIGS/test_models.py` contains tests for logic within the data models. `RIGS/test_unit.py` contains "Live server" tests, using raw web requests. `RIGS/test_integration.py` contains user interface tests which take control of a web browser. For automated Travis tests, we use [Sauce Labs](https://saucelabs.com). When debugging locally, ensure that you have the latest version of Google Chrome installed, then install [chromedriver](https://sites.google.com/a/chromium.org/chromedriver/) and ensure it is on the `PATH`.
|
||||||
|
|
||||||
|
You can run the entire test suite, or you can run specific sections individually. For example, in order of specificity:
|
||||||
|
|
||||||
|
```
|
||||||
|
python manage.py test
|
||||||
|
python manage.py test RIGS.test_models
|
||||||
|
python manage.py test RIGS.test_models.EventTestCase
|
||||||
|
python manage.py test RIGS.test_models.EventTestCase.test_current_events
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
[](https://forthebadge.com) [](https://forthebadge.com)
|
[](https://forthebadge.com) [](https://forthebadge.com)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'RIGS.apps.RIGSAppConfig'
|
||||||
|
|||||||
192
RIGS/admin.py
@@ -1,119 +1,58 @@
|
|||||||
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 django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.utils.translation import ugettext_lazy as _
|
||||||
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.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.Payment)
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
|
@admin.register(models.Profile)
|
||||||
def merge_model_instances(primary_object, alias_objects):
|
class ProfileAdmin(UserAdmin):
|
||||||
"""
|
fieldsets = (
|
||||||
Merge several model instances into one, the `primary_object`.
|
(None, {'fields': ('username', 'password')}),
|
||||||
Use this function to merge model objects and migrate all of the related
|
(_('Personal info'), {
|
||||||
fields from the alias objects the primary object.
|
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
|
||||||
"""
|
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
|
||||||
|
'groups', 'user_permissions')}),
|
||||||
# get related fields
|
(_('Important dates'), {
|
||||||
related_fields = list(filter(
|
'fields': ('last_login', 'date_joined')}),
|
||||||
lambda x: x.is_relation is True,
|
)
|
||||||
primary_object._meta.get_fields()))
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
many_to_many_fields = list(filter(
|
'classes': ('wide',),
|
||||||
lambda x: x.many_to_many is True, related_fields))
|
'fields': ('username', 'password1', 'password2'),
|
||||||
|
}),
|
||||||
related_fields = list(filter(
|
)
|
||||||
lambda x: x.many_to_many is False, related_fields))
|
form = forms.ProfileChangeForm
|
||||||
|
add_form = forms.ProfileCreationForm
|
||||||
# Loop through all alias objects and migrate their references to the
|
|
||||||
# primary object
|
|
||||||
deleted_objects = []
|
|
||||||
deleted_objects_count = 0
|
|
||||||
for alias_object in alias_objects:
|
|
||||||
# Migrate all foreign key references from alias object to primary
|
|
||||||
# object.
|
|
||||||
for many_to_many_field in many_to_many_fields:
|
|
||||||
alias_varname = many_to_many_field.name
|
|
||||||
related_objects = getattr(alias_object, alias_varname)
|
|
||||||
for obj in related_objects.all():
|
|
||||||
try:
|
|
||||||
# Handle regular M2M relationships.
|
|
||||||
getattr(alias_object, alias_varname).remove(obj)
|
|
||||||
getattr(primary_object, alias_varname).add(obj)
|
|
||||||
except AttributeError:
|
|
||||||
# Handle M2M relationships with a 'through' model.
|
|
||||||
# This does not delete the 'through model.
|
|
||||||
# TODO: Allow the user to delete a duplicate 'through' model.
|
|
||||||
through_model = getattr(alias_object, alias_varname).through
|
|
||||||
kwargs = {
|
|
||||||
many_to_many_field.m2m_reverse_field_name(): obj,
|
|
||||||
many_to_many_field.m2m_field_name(): alias_object,
|
|
||||||
}
|
|
||||||
through_model_instances = through_model.objects.filter(**kwargs)
|
|
||||||
for instance in through_model_instances:
|
|
||||||
# Re-attach the through model to the primary_object
|
|
||||||
setattr(
|
|
||||||
instance,
|
|
||||||
many_to_many_field.m2m_field_name(),
|
|
||||||
primary_object)
|
|
||||||
instance.save()
|
|
||||||
# TODO: Here, try to delete duplicate instances that are
|
|
||||||
# disallowed by a unique_together constraint
|
|
||||||
|
|
||||||
for related_field in related_fields:
|
|
||||||
if related_field.one_to_many:
|
|
||||||
with transaction.atomic():
|
|
||||||
try:
|
|
||||||
alias_varname = related_field.get_accessor_name()
|
|
||||||
related_objects = getattr(alias_object, alias_varname)
|
|
||||||
for obj in related_objects.all():
|
|
||||||
field_name = related_field.field.name
|
|
||||||
setattr(obj, field_name, primary_object)
|
|
||||||
obj.save()
|
|
||||||
except IntegrityError:
|
|
||||||
pass # Skip to avoid integrity error from unique_together
|
|
||||||
elif related_field.one_to_one or related_field.many_to_one:
|
|
||||||
alias_varname = related_field.name
|
|
||||||
if hasattr(alias_object, alias_varname):
|
|
||||||
related_object = getattr(alias_object, alias_varname)
|
|
||||||
primary_related_object = getattr(primary_object, alias_varname)
|
|
||||||
if primary_related_object is None:
|
|
||||||
setattr(primary_object, alias_varname, related_object)
|
|
||||||
primary_object.save()
|
|
||||||
elif related_field.one_to_one:
|
|
||||||
related_object.delete()
|
|
||||||
|
|
||||||
if alias_object.id:
|
|
||||||
deleted_objects += [alias_object]
|
|
||||||
alias_object.delete()
|
|
||||||
deleted_objects_count += 1
|
|
||||||
|
|
||||||
return primary_object, deleted_objects, deleted_objects_count
|
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
@@ -123,16 +62,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
|
||||||
@@ -148,36 +95,7 @@ class AssociateAdmin(VersionAdmin):
|
|||||||
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
||||||
'forms': forms
|
'forms': forms
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, 'admin_associate_merge.html', context)
|
return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Profile)
|
|
||||||
class ProfileAdmin(UserAdmin, AssociateAdmin):
|
|
||||||
list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events')
|
|
||||||
list_display_links = ['username']
|
|
||||||
fieldsets = (
|
|
||||||
(None, {'fields': ('username', 'password')}),
|
|
||||||
(_('Personal info'), {
|
|
||||||
'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)
|
||||||
@@ -196,13 +114,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')
|
|
||||||
|
|||||||
@@ -1,73 +1,73 @@
|
|||||||
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 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 = 'RIGS/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'
|
|
||||||
|
|
||||||
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):
|
||||||
invoice = get_object_or_404(models.Invoice, pk=pk)
|
invoice = get_object_or_404(models.Invoice, pk=pk)
|
||||||
object = invoice.event
|
object = invoice.event
|
||||||
template = get_template('event_print.xml')
|
template = get_template('RIGS/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 +76,10 @@ class InvoicePrint(generic.View):
|
|||||||
|
|
||||||
pdfData = buffer.read()
|
pdfData = buffer.read()
|
||||||
|
|
||||||
|
escapedEventName = re.sub('[^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,26 +92,25 @@ 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):
|
||||||
model = models.Invoice
|
model = models.Invoice
|
||||||
template_name = 'invoice_confirm_delete.html'
|
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(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).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):
|
||||||
@@ -118,42 +119,47 @@ class InvoiceDelete(generic.DeleteView):
|
|||||||
|
|
||||||
class InvoiceArchive(generic.ListView):
|
class InvoiceArchive(generic.ListView):
|
||||||
model = models.Invoice
|
model = models.Invoice
|
||||||
template_name = 'invoice_list_archive.html'
|
template_name = 'RIGS/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):
|
|
||||||
return self.model.objects.search(self.request.GET.get('q')).order_by('-invoice_date')
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceWaiting(generic.ListView):
|
class InvoiceWaiting(generic.ListView):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
template_name = 'invoice_list_waiting.html'
|
template_name = 'RIGS/event_invoice.html'
|
||||||
|
|
||||||
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,21 +168,15 @@ 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):
|
||||||
model = models.Payment
|
model = models.Payment
|
||||||
fields = ['invoice', 'date', 'amount', 'method']
|
fields = ['invoice', 'date', 'amount', 'method']
|
||||||
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 +184,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')
|
||||||
218
RIGS/forms.py
@@ -1,24 +1,62 @@
|
|||||||
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.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
||||||
from reversion import revisions as reversion
|
from registration.forms import RegistrationFormUniqueEmail
|
||||||
|
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'})
|
||||||
forms.TimeField.widget = forms.TimeInput(attrs={'type': 'time'}, format='%H:%M')
|
forms.TimeField.widget = forms.TextInput(attrs={'type': 'time'})
|
||||||
forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M')
|
forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'})
|
||||||
|
|
||||||
|
# Registration
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
|
||||||
|
captcha = ReCaptchaField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Profile
|
||||||
|
fields = ('username', 'email', 'first_name', 'last_name', 'initials')
|
||||||
|
|
||||||
|
def clean_initials(self):
|
||||||
|
"""
|
||||||
|
Validate that the supplied initials are unique.
|
||||||
|
"""
|
||||||
|
if models.Profile.objects.filter(initials__iexact=self.cleaned_data['initials']):
|
||||||
|
raise forms.ValidationError("These initials are already in use. Please supply different initials.")
|
||||||
|
return self.cleaned_data['initials']
|
||||||
|
|
||||||
|
|
||||||
|
# Embedded Login form - remove the autofocus
|
||||||
|
class EmbeddedAuthenticationForm(AuthenticationForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['username'].widget.attrs.pop('autofocus', None)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordReset(PasswordResetForm):
|
||||||
|
captcha = ReCaptchaField(label='Captcha')
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileCreationForm(UserCreationForm):
|
||||||
|
class Meta(UserCreationForm.Meta):
|
||||||
|
model = models.Profile
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileChangeForm(UserChangeForm):
|
||||||
|
class Meta(UserChangeForm.Meta):
|
||||||
|
model = models.Profile
|
||||||
|
|
||||||
|
|
||||||
# Events Shit
|
# Events Shit
|
||||||
class EventForm(forms.ModelForm):
|
class EventForm(forms.ModelForm):
|
||||||
datetime_input_formats = list(settings.DATETIME_INPUT_FORMATS)
|
datetime_input_formats = formats.get_format_lazy("DATETIME_INPUT_FORMATS") + list(settings.DATETIME_INPUT_FORMATS)
|
||||||
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
|
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
|
||||||
access_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
|
access_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
|
||||||
|
|
||||||
@@ -92,15 +130,12 @@ class EventForm(forms.ModelForm):
|
|||||||
return item
|
return item
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.cleaned_data.get("is_rig") and not (
|
if self.cleaned_data.get("is_rig") and not (self.cleaned_data.get('person') or self.cleaned_data.get('organisation')):
|
||||||
self.cleaned_data.get('person') or self.cleaned_data.get('organisation')):
|
raise forms.ValidationError('You haven\'t provided any client contact details. Please add a person or organisation.', code='contact')
|
||||||
raise forms.ValidationError(
|
return super(EventForm, self).clean()
|
||||||
'You haven\'t provided any client contact details. Please add a person or organisation.',
|
|
||||||
code='contact')
|
|
||||||
return super().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()
|
||||||
@@ -124,22 +159,6 @@ class EventForm(forms.ModelForm):
|
|||||||
'purchase_order', 'collector']
|
'purchase_order', 'collector']
|
||||||
|
|
||||||
|
|
||||||
class SubhireForm(forms.ModelForm):
|
|
||||||
related_models = {
|
|
||||||
'person': models.Person,
|
|
||||||
'organisation': models.Organisation,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields['start_date'].widget.format = '%Y-%m-%d'
|
|
||||||
self.fields['end_date'].widget.format = '%Y-%m-%d'
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Subhire
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
|
|
||||||
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
||||||
tos = forms.BooleanField(required=True, label="Terms of hire")
|
tos = forms.BooleanField(required=True, label="Terms of hire")
|
||||||
name = forms.CharField(label="Your Name")
|
name = forms.CharField(label="Your Name")
|
||||||
@@ -155,7 +174,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
|
||||||
|
|
||||||
@@ -166,136 +185,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(EventRiskAssessmentForm, self).clean()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.RiskAssessment
|
|
||||||
fields = '__all__'
|
|
||||||
exclude = ['reviewed_at', 'reviewed_by']
|
|
||||||
|
|
||||||
|
|
||||||
class EventChecklistForm(forms.ModelForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields['date'].widget.format = '%Y-%m-%d'
|
|
||||||
for name, field in self.fields.items():
|
|
||||||
if field.__class__ == forms.NullBooleanField:
|
|
||||||
# Only display yes/no to user, the 'none' is only ever set in the background
|
|
||||||
field.widget = forms.CheckboxInput()
|
|
||||||
# Parsed from incoming form data by clean, then saved into models when the form is saved
|
|
||||||
items = {}
|
|
||||||
|
|
||||||
related_models = {
|
|
||||||
'venue': models.Venue,
|
|
||||||
'power_mic': models.Profile,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Two possible formats
|
|
||||||
def parsedatetime(self, date_string):
|
|
||||||
try:
|
|
||||||
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S'))
|
|
||||||
except ValueError:
|
|
||||||
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M'))
|
|
||||||
|
|
||||||
# There's probably a thousand better ways to do this, but this one is mine
|
|
||||||
def clean(self):
|
|
||||||
vehicles = {key: val for key, val in self.data.items()
|
|
||||||
if key.startswith('vehicle')}
|
|
||||||
for key in vehicles:
|
|
||||||
pk = int(key.split('_')[1])
|
|
||||||
driver_key = 'driver_' + str(pk)
|
|
||||||
if(self.data[driver_key] == ''):
|
|
||||||
raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch')
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
item = models.EventChecklistVehicle.objects.get(pk=pk)
|
|
||||||
except models.EventChecklistVehicle.DoesNotExist:
|
|
||||||
item = models.EventChecklistVehicle()
|
|
||||||
|
|
||||||
item.vehicle = vehicles['vehicle_' + str(pk)]
|
|
||||||
item.driver = models.Profile.objects.get(pk=self.data[driver_key])
|
|
||||||
item.full_clean('checklist')
|
|
||||||
|
|
||||||
# item does not have a database pk yet as it isn't saved
|
|
||||||
self.items['v' + str(pk)] = item
|
|
||||||
|
|
||||||
crewmembers = {key: val for key, val in self.data.items()
|
|
||||||
if key.startswith('crewmember')}
|
|
||||||
other_fields = ['start', 'role', 'end']
|
|
||||||
for key in crewmembers:
|
|
||||||
pk = int(key.split('_')[1])
|
|
||||||
|
|
||||||
for field in other_fields:
|
|
||||||
value = self.data[f'{field}_{pk}']
|
|
||||||
if value == '':
|
|
||||||
raise forms.ValidationError(f'Add a {field} to crewmember {pk}', code=f'{field}_mismatch')
|
|
||||||
|
|
||||||
try:
|
|
||||||
item = models.EventChecklistCrew.objects.get(pk=pk)
|
|
||||||
except models.EventChecklistCrew.DoesNotExist:
|
|
||||||
item = models.EventChecklistCrew()
|
|
||||||
|
|
||||||
item.crewmember = models.Profile.objects.get(pk=self.data['crewmember_' + str(pk)])
|
|
||||||
item.start = self.parsedatetime(self.data['start_' + str(pk)])
|
|
||||||
item.role = self.data['role_' + str(pk)]
|
|
||||||
item.end = self.parsedatetime(self.data['end_' + str(pk)])
|
|
||||||
item.full_clean('checklist')
|
|
||||||
|
|
||||||
# item does not have a database pk yet as it isn't saved
|
|
||||||
self.items['c' + str(pk)] = item
|
|
||||||
|
|
||||||
return super(EventChecklistForm, self).clean()
|
|
||||||
|
|
||||||
def save(self, commit=True):
|
|
||||||
checklist = super(EventChecklistForm, self).save(commit=False)
|
|
||||||
if (commit):
|
|
||||||
# Remove all existing, to be recreated from the form
|
|
||||||
checklist.vehicles.all().delete()
|
|
||||||
checklist.crew.all().delete()
|
|
||||||
checklist.save()
|
|
||||||
|
|
||||||
for key in self.items:
|
|
||||||
item = self.items[key]
|
|
||||||
reversion.add_to_revision(item)
|
|
||||||
# finish and save new database items
|
|
||||||
item.checklist = checklist
|
|
||||||
item.full_clean()
|
|
||||||
item.save()
|
|
||||||
|
|
||||||
self.items.clear()
|
|
||||||
|
|
||||||
return checklist
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.EventChecklist
|
|
||||||
fields = '__all__'
|
|
||||||
exclude = ['reviewed_at', 'reviewed_by']
|
|
||||||
|
|||||||
@@ -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):
|
||||||
@@ -39,10 +40,8 @@ class CalendarICS(ICalFeed):
|
|||||||
return params
|
return params
|
||||||
|
|
||||||
def description(self, params):
|
def description(self, params):
|
||||||
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + (
|
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + ('Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
|
||||||
'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
|
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + ('Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
|
||||||
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
|
|
||||||
'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
|
|
||||||
|
|
||||||
return desc
|
return desc
|
||||||
|
|
||||||
@@ -73,8 +72,7 @@ class CalendarICS(ICalFeed):
|
|||||||
|
|
||||||
filter = filter & typeFilters & statusFilters
|
filter = filter & typeFilters & statusFilters
|
||||||
|
|
||||||
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
|
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||||
'venue', 'mic')
|
|
||||||
|
|
||||||
def item_title(self, item):
|
def item_title(self, item):
|
||||||
title = ''
|
title = ''
|
||||||
@@ -93,7 +91,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 +99,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,43 +113,43 @@ 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 += 'Status = ' + str(item.get_status_display()) + '\n'
|
||||||
desc += f'Status = {item.get_status_display()}\n'
|
|
||||||
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
||||||
|
|
||||||
desc += '\n'
|
desc += '\n'
|
||||||
if item.meet_at:
|
if item.meet_at:
|
||||||
desc += 'Crew Meet = ' + (
|
desc += 'Crew Meet = ' + (item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
||||||
item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
|
||||||
if item.access_at:
|
if item.access_at:
|
||||||
desc += 'Access At = ' + (
|
desc += 'Access At = ' + (item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
||||||
item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
|
||||||
if item.start_date:
|
if item.start_date:
|
||||||
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + (
|
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + ((' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
|
||||||
(' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
|
|
||||||
if item.end_date:
|
if item.end_date:
|
||||||
desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + (
|
desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + ((' ' + item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n'
|
||||||
(' ' + item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n'
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
28
RIGS/importer_tests.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
__author__ = 'ghost'
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from importer import fix_email
|
||||||
|
|
||||||
|
|
||||||
|
class EmailFixerTest(unittest.TestCase):
|
||||||
|
def test_correct(self):
|
||||||
|
e = fix_email("tom@ghost.uk.net")
|
||||||
|
self.assertEqual(e, "tom@ghost.uk.net")
|
||||||
|
|
||||||
|
def test_partial(self):
|
||||||
|
e = fix_email("psytp")
|
||||||
|
self.assertEqual(e, "psytp@nottingham.ac.uk")
|
||||||
|
|
||||||
|
def test_none(self):
|
||||||
|
old = None
|
||||||
|
new = fix_email(old)
|
||||||
|
self.assertEqual(old, new)
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
old = ""
|
||||||
|
new = fix_email(old)
|
||||||
|
self.assertEqual(old, new)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -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()
|
|
||||||
@@ -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')
|
|
||||||
|
|||||||
@@ -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,175 +16,181 @@ 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):
|
||||||
raise CommandError('You cannot run this command in production')
|
raise CommandError('You cannot run this command in production')
|
||||||
|
|
||||||
random.seed(
|
random.seed('Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests
|
||||||
'Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests
|
|
||||||
|
|
||||||
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()
|
||||||
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe",
|
|
||||||
"Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore",
|
|
||||||
"Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan",
|
|
||||||
"Nicolas Flamel", "Cornelius Fudge", "Goyle", "Gregory Goyle", "Hermione Granger", "Rubeus Hagrid",
|
|
||||||
"Igor Karkaroff", "Viktor Krum", "Bellatrix Lestrange", "Alice Longbottom", "Frank Longbottom",
|
|
||||||
"Neville Longbottom", "Luna Lovegood", "Xenophilius Lovegood", # noqa
|
|
||||||
"Remus Lupin", "Draco Malfoy", "Lucius Malfoy", "Narcissa Malfoy", "Olympe Maxime",
|
|
||||||
"Minerva McGonagall", "Mad-Eye Moody", "Peter Pettigrew", "Harry Potter", "James Potter",
|
|
||||||
"Lily Potter", "Quirinus Quirrell", "Tom Riddle", "Mary Riddle", "Lord Voldemort", "Rita Skeeter",
|
|
||||||
"Severus Snape", "Nymphadora Tonks", "Dolores Janes Umbridge", "Arthur Weasley", "Bill Weasley",
|
|
||||||
"Charlie Weasley", "Fred Weasley", "George Weasley", "Ginny Weasley", "Molly Weasley", "Percy Weasley",
|
|
||||||
"Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
|
|
||||||
for i, name in enumerate(names):
|
|
||||||
with reversion.create_revision():
|
|
||||||
reversion.set_user(random.choice(models.Profile.objects.all()))
|
|
||||||
person = models.Person.objects.create(name=name)
|
|
||||||
|
|
||||||
if i % 3 == 0:
|
self.setupPeople()
|
||||||
person.email = "address@person.com"
|
self.setupOrganisations()
|
||||||
|
self.setupVenues()
|
||||||
|
|
||||||
if i % 5 == 0:
|
self.setupGroups()
|
||||||
person.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
|
||||||
|
|
||||||
if i % 7 == 0:
|
self.setupEvents()
|
||||||
person.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
|
||||||
|
|
||||||
if i % 9 == 0:
|
self.setupUsefulProfiles()
|
||||||
person.phone = "01234 567894"
|
|
||||||
|
|
||||||
person.save()
|
def setupPeople(self):
|
||||||
self.people.append(person)
|
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe", "Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore", "Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan", "Nicolas Flamel", "Cornelius Fudge", "Goyle", "Gregory Goyle", "Hermione Granger", "Rubeus Hagrid", "Igor Karkaroff", "Viktor Krum", "Bellatrix Lestrange", "Alice Longbottom", "Frank Longbottom", "Neville Longbottom", "Luna Lovegood", "Xenophilius Lovegood", # noqa
|
||||||
|
"Remus Lupin", "Draco Malfoy", "Lucius Malfoy", "Narcissa Malfoy", "Olympe Maxime", "Minerva McGonagall", "Mad-Eye Moody", "Peter Pettigrew", "Harry Potter", "James Potter", "Lily Potter", "Quirinus Quirrell", "Tom Riddle", "Mary Riddle", "Lord Voldemort", "Rita Skeeter", "Severus Snape", "Nymphadora Tonks", "Dolores Janes Umbridge", "Arthur Weasley", "Bill Weasley", "Charlie Weasley", "Fred Weasley", "George Weasley", "Ginny Weasley", "Molly Weasley", "Percy Weasley", "Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
|
||||||
def setup_organisations(self):
|
|
||||||
names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars",
|
|
||||||
"ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc",
|
|
||||||
"Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp",
|
|
||||||
"Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp",
|
|
||||||
"North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation",
|
|
||||||
"Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp",
|
|
||||||
"Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power",
|
|
||||||
"Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank",
|
|
||||||
"Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium",
|
|
||||||
"Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors",
|
|
||||||
"Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation",
|
|
||||||
"U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation",
|
|
||||||
"Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
|
|
||||||
"Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron",
|
|
||||||
"Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry",
|
|
||||||
"Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.",
|
|
||||||
"Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer",
|
|
||||||
"Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron",
|
|
||||||
"Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company",
|
|
||||||
"Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer",
|
|
||||||
"Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp",
|
|
||||||
"The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute",
|
|
||||||
"Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King",
|
|
||||||
"Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman",
|
|
||||||
"Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande",
|
|
||||||
"Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
|
|
||||||
for i, name in enumerate(names):
|
|
||||||
with reversion.create_revision():
|
|
||||||
reversion.set_user(random.choice(models.Profile.objects.all()))
|
|
||||||
new_organisation = models.Organisation.objects.create(name=name)
|
|
||||||
|
|
||||||
if i % 2 == 0:
|
|
||||||
new_organisation.has_su_account = True
|
|
||||||
|
|
||||||
if i % 3 == 0:
|
|
||||||
new_organisation.email = "address@organisation.com"
|
|
||||||
|
|
||||||
if i % 5 == 0:
|
|
||||||
new_organisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
|
||||||
|
|
||||||
if i % 7 == 0:
|
|
||||||
new_organisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
|
||||||
|
|
||||||
if i % 9 == 0:
|
|
||||||
new_organisation.phone = "01234 567894"
|
|
||||||
|
|
||||||
new_organisation.save()
|
|
||||||
self.organisations.append(new_organisation)
|
|
||||||
|
|
||||||
def setup_venues(self):
|
|
||||||
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch",
|
|
||||||
"The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands",
|
|
||||||
"The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins",
|
|
||||||
"The Vale of Arryn", "The Whispering Wood", "White Harbor", "Winterfell", "The Arbor", "Ashemark",
|
|
||||||
"Brightwater Keep", "Casterly Rock", "Clegane's Keep", "Dragonstone", "Dorne", "God's Eye",
|
|
||||||
"The Golden Tooth", # noqa
|
|
||||||
"Harrenhal", "Highgarden", "Horn Hill", "Fingers", "King's Landing", "Lannisport", "Oldtown",
|
|
||||||
"Rainswood", "Storm's End", "Summerhall", "Sunspear", "Tarth", "Castle Black", "Craster's Keep",
|
|
||||||
"Fist of the First Men", "The Frostfangs", "The Gift", "The Skirling Pass", "The Wall", "Asshai",
|
|
||||||
"Astapor", "Braavos", "The Dothraki Sea", "Lys", "Meereen", "Myr", "Norvos", "Pentos", "Qarth",
|
|
||||||
"Qohor", "The Red Waste", "Tyrosh", "Vaes Dothrak", "Valyria", "Village of the Lhazareen", "Volantis",
|
|
||||||
"Yunkai"] # noqa
|
|
||||||
for i, name in enumerate(names):
|
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)
|
|
||||||
|
|
||||||
if i % 2 == 0:
|
|
||||||
new_venue.three_phase_available = True
|
|
||||||
|
|
||||||
|
newPerson = models.Person.objects.create(name=name)
|
||||||
if i % 3 == 0:
|
if i % 3 == 0:
|
||||||
new_venue.email = "address@venue.com"
|
newPerson.email = "address@person.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"
|
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:
|
||||||
new_venue.address = "1 Venue 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:
|
||||||
new_venue.phone = "01234 567894"
|
newPerson.phone = "01234 567894"
|
||||||
|
|
||||||
new_venue.save()
|
newPerson.save()
|
||||||
self.venues.append(new_venue)
|
self.people.append(newPerson)
|
||||||
|
|
||||||
def setup_events(self):
|
def setupOrganisations(self):
|
||||||
names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball",
|
names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
|
||||||
"Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
|
"Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
|
||||||
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show",
|
for i, name in enumerate(names):
|
||||||
"Awards Ceremony", "Debating Event", "Club Night", "DJ Evening", "Building Projection",
|
with reversion.create_revision():
|
||||||
"Choir Concert"]
|
reversion.set_user(random.choice(self.profiles))
|
||||||
descriptions = ["A brief description of the event", "This event is boring", "Probably wont happen",
|
newOrganisation = models.Organisation.objects.create(name=name)
|
||||||
"Warning: this has lots of kit"]
|
if i % 2 == 0:
|
||||||
notes = ["The client came into the office at some point", "Who knows if this will happen",
|
newOrganisation.has_su_account = True
|
||||||
"Probably should check this event", "Maybe not happening", "Run away!"]
|
|
||||||
|
|
||||||
item_options = [
|
if i % 3 == 0:
|
||||||
{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2,
|
newOrganisation.email = "address@organisation.com"
|
||||||
'cost': 200.00},
|
|
||||||
{'name': 'Projector',
|
|
||||||
'description': 'Some kind of video thinamejig, probably with unnecessary processing for free',
|
|
||||||
'quantity': 1, 'cost': 500.00},
|
|
||||||
{'name': 'Lighting Desk', 'description': 'Cannot provide guarentee that it will work', 'quantity': 1,
|
|
||||||
'cost': 200.52},
|
|
||||||
{'name': 'Moving lights', 'description': 'Flashy lights, with the copper', 'quantity': 8, 'cost': 50.00},
|
|
||||||
{'name': 'Microphones', 'description': 'Make loud noise \n you will want speakers with this', 'quantity': 5,
|
|
||||||
'cost': 0.50},
|
|
||||||
{'name': 'Sound Mixer Thing', 'description': 'Might be analogue, might be digital', 'quantity': 1,
|
|
||||||
'cost': 100.00},
|
|
||||||
{'name': 'Electricity', 'description': 'You need this', 'quantity': 1, 'cost': 200.00},
|
|
||||||
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
|
|
||||||
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
|
|
||||||
|
|
||||||
day_delta = -120 # start adding events from 4 months ago
|
if i % 5 == 0:
|
||||||
|
newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||||
|
|
||||||
|
if i % 7 == 0:
|
||||||
|
newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||||
|
|
||||||
|
if i % 9 == 0:
|
||||||
|
newOrganisation.phone = "01234 567894"
|
||||||
|
|
||||||
|
newOrganisation.save()
|
||||||
|
self.organisations.append(newOrganisation)
|
||||||
|
|
||||||
|
def setupVenues(self):
|
||||||
|
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch", "The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands", "The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins", "The Vale of Arryn", "The Whispering Wood", "White Harbor", "Winterfell", "The Arbor", "Ashemark", "Brightwater Keep", "Casterly Rock", "Clegane's Keep", "Dragonstone", "Dorne", "God's Eye", "The Golden Tooth", # noqa
|
||||||
|
"Harrenhal", "Highgarden", "Horn Hill", "Fingers", "King's Landing", "Lannisport", "Oldtown", "Rainswood", "Storm's End", "Summerhall", "Sunspear", "Tarth", "Castle Black", "Craster's Keep", "Fist of the First Men", "The Frostfangs", "The Gift", "The Skirling Pass", "The Wall", "Asshai", "Astapor", "Braavos", "The Dothraki Sea", "Lys", "Meereen", "Myr", "Norvos", "Pentos", "Qarth", "Qohor", "The Red Waste", "Tyrosh", "Vaes Dothrak", "Valyria", "Village of the Lhazareen", "Volantis", "Yunkai"] # noqa
|
||||||
|
for i, name in enumerate(names):
|
||||||
|
with reversion.create_revision():
|
||||||
|
reversion.set_user(random.choice(self.profiles))
|
||||||
|
newVenue = models.Venue.objects.create(name=name)
|
||||||
|
if i % 2 == 0:
|
||||||
|
newVenue.three_phase_available = True
|
||||||
|
|
||||||
|
if i % 3 == 0:
|
||||||
|
newVenue.email = "address@venue.com"
|
||||||
|
|
||||||
|
if i % 5 == 0:
|
||||||
|
newVenue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||||
|
|
||||||
|
if i % 7 == 0:
|
||||||
|
newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||||
|
|
||||||
|
if i % 9 == 0:
|
||||||
|
newVenue.phone = "01234 567894"
|
||||||
|
|
||||||
|
newVenue.save()
|
||||||
|
self.venues.append(newVenue)
|
||||||
|
|
||||||
|
def setupGroups(self):
|
||||||
|
self.keyholder_group = Group.objects.create(name='Keyholders')
|
||||||
|
self.finance_group = Group.objects.create(name='Finance')
|
||||||
|
|
||||||
|
keyholderPerms = ["add_event", "change_event", "view_event",
|
||||||
|
"add_eventitem", "change_eventitem", "delete_eventitem",
|
||||||
|
"add_organisation", "change_organisation", "view_organisation",
|
||||||
|
"add_person", "change_person", "view_person", "view_profile",
|
||||||
|
"add_venue", "change_venue", "view_venue",
|
||||||
|
"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", "Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
|
||||||
|
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show", "Awards Ceremony", "Debating Event", "Club Night", "DJ Evening", "Building Projection", "Choir Concert"]
|
||||||
|
descriptions = ["A brief desciption of the event", "This event is boring", "Probably wont happen", "Warning: this has lots of kit"]
|
||||||
|
notes = ["The client came into the office at some point", "Who knows if this will happen", "Probably should check this event", "Maybe not happening", "Run away!"]
|
||||||
|
|
||||||
|
itemOptions = [{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2, 'cost': 200.00},
|
||||||
|
{'name': 'Projector', 'description': 'Some kind of video thinamejig, probably with unnecessary processing for free', 'quantity': 1, 'cost': 500.00},
|
||||||
|
{'name': 'Lighting Desk', 'description': 'Cannot provide guarentee that it will work', 'quantity': 1, 'cost': 200.52},
|
||||||
|
{'name': 'Moving lights', 'description': 'Flashy lights, with the copper', 'quantity': 8, 'cost': 50.00},
|
||||||
|
{'name': 'Microphones', 'description': 'Make loud noise \n you will want speakers with this', 'quantity': 5, 'cost': 0.50},
|
||||||
|
{'name': 'Sound Mixer Thing', 'description': 'Might be analogue, might be digital', 'quantity': 1, 'cost': 100.00},
|
||||||
|
{'name': 'Electricity', 'description': 'You need this', 'quantity': 1, 'cost': 200.00},
|
||||||
|
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
|
||||||
|
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
|
||||||
|
|
||||||
|
dayDelta = -120 # start adding events from 4 months ago
|
||||||
|
|
||||||
for i in range(150): # Let's add 100 events
|
for i in range(150): # Let's add 100 events
|
||||||
with reversion.create_revision():
|
with reversion.create_revision():
|
||||||
@@ -193,100 +198,63 @@ 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, power_mic=random.choice(self.profiles),
|
|
||||||
safe_parking=bool(random.getrandbits(1)),
|
|
||||||
safe_packing=bool(random.getrandbits(1)),
|
|
||||||
exits=bool(random.getrandbits(1)),
|
|
||||||
trip_hazard=bool(random.getrandbits(1)),
|
|
||||||
warning_signs=bool(random.getrandbits(1)),
|
|
||||||
ear_plugs=bool(random.getrandbits(1)),
|
|
||||||
hs_location="Locked away safely",
|
|
||||||
extinguishers_location="Somewhere, I forgot",
|
|
||||||
earthing=bool(random.getrandbits(1)),
|
|
||||||
pat=bool(random.getrandbits(1)),
|
|
||||||
date=timezone.now(), venue=random.choice(self.venues))
|
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 2.0.13 on 2020-01-10 14:52
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0035_auto_20191124_1319'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='profile',
|
|
||||||
name='is_approved',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='profile',
|
|
||||||
name='last_emailed',
|
|
||||||
field=models.DateTimeField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 2.0.13 on 2020-01-11 18:29
|
|
||||||
# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
def approve_legacy(apps, schema_editor):
|
|
||||||
Profile = apps.get_model('RIGS', 'Profile')
|
|
||||||
for person in Profile.objects.all():
|
|
||||||
person.is_approved = True
|
|
||||||
person.save()
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0036_profile_is_approved'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(approve_legacy, migrations.RunPython.noop)
|
|
||||||
]
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Generated by Django 2.0.13 on 2020-03-06 20:00
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0037_approve_legacy'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='event',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='invoice',
|
|
||||||
options={'ordering': ['-invoice_date']},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='organisation',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='person',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='profile',
|
|
||||||
options={'verbose_name': 'user', 'verbose_name_plural': 'users'},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='venue',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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)
|
|
||||||
]
|
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Generated by Django 3.2.12 on 2022-10-15 19:36
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import versioning.versioning
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0044_profile_is_supervisor'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Subhire',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=255)),
|
|
||||||
('description', models.TextField(blank=True, default='')),
|
|
||||||
('status', models.IntegerField(choices=[(0, 'Provisional'), (1, 'Confirmed'), (2, 'Booked'), (3, 'Cancelled')], default=0)),
|
|
||||||
('start_date', models.DateField()),
|
|
||||||
('start_time', models.TimeField(blank=True, null=True)),
|
|
||||||
('end_date', models.DateField(blank=True, null=True)),
|
|
||||||
('end_time', models.TimeField(blank=True, null=True)),
|
|
||||||
('purchase_order', models.CharField(blank=True, default='', max_length=255, verbose_name='PO')),
|
|
||||||
('insurance_value', models.DecimalField(decimal_places=2, max_digits=10)),
|
|
||||||
('organisation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='RIGS.organisation')),
|
|
||||||
('person', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='RIGS.person')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 3.2.16 on 2022-10-20 11:56
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('RIGS', '0045_subhire'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='subhire',
|
|
||||||
name='events',
|
|
||||||
field=models.ManyToManyField(to='RIGS.Event'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
865
RIGS/models.py
@@ -1,7 +1,7 @@
|
|||||||
|
from RIGS.models import Profile
|
||||||
|
from RIGS.forms import ProfileRegistrationFormUniqueEmail
|
||||||
from registration.signals import user_registered
|
from registration.signals import user_registered
|
||||||
|
|
||||||
from users.forms import ProfileRegistrationFormUniqueEmail
|
|
||||||
|
|
||||||
|
|
||||||
def user_created(sender, user, request, **kwargs):
|
def user_created(sender, user, request, **kwargs):
|
||||||
form = ProfileRegistrationFormUniqueEmail(request.POST)
|
form = ProfileRegistrationFormUniqueEmail(request.POST)
|
||||||
@@ -1,95 +1,123 @@
|
|||||||
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
|
|
||||||
|
|
||||||
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 PyRIGS import decorators
|
|
||||||
from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related
|
|
||||||
from RIGS import models, forms
|
|
||||||
|
|
||||||
__author__ = 'ghost'
|
__author__ = 'ghost'
|
||||||
|
|
||||||
|
|
||||||
class RigboardIndex(generic.TemplateView):
|
class RigboardIndex(generic.TemplateView):
|
||||||
template_name = 'rigboard.html'
|
template_name = 'RIGS/rigboard.html'
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
class WebCalendar(generic.TemplateView):
|
class WebCalendar(generic.TemplateView):
|
||||||
template_name = 'calendar.html'
|
template_name = 'RIGS/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'
|
|
||||||
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])
|
||||||
return context
|
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
|
||||||
|
'version': '1.0',
|
||||||
|
'type': 'rich',
|
||||||
|
'height': '250'
|
||||||
|
}
|
||||||
|
|
||||||
|
json = simplejson.JSONEncoderForHTML().encode(data)
|
||||||
|
return HttpResponse(json, content_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
class EventEmbed(EventDetail):
|
class EventEmbed(EventDetail):
|
||||||
template_name = 'event_embed.html'
|
template_name = 'RIGS/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):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
form_class = forms.EventForm
|
form_class = forms.EventForm
|
||||||
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('"-\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):
|
||||||
@@ -99,30 +127,30 @@ class EventCreate(generic.CreateView):
|
|||||||
class EventUpdate(generic.UpdateView):
|
class EventUpdate(generic.UpdateView):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
form_class = forms.EventForm
|
form_class = forms.EventForm
|
||||||
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, 'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.')
|
||||||
'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.')
|
|
||||||
|
|
||||||
if hasattr(self.object, 'authorised'):
|
if hasattr(self.object, 'authorised'):
|
||||||
messages.warning(self.request,
|
messages.warning(self.request, 'This event has already been authorised by client, any changes to price will require reauthorisation.')
|
||||||
'This event has already been authorised by the client, any changes to the price will require reauthorisation.')
|
return super(EventUpdate, self).render_to_response(context, **response_kwargs)
|
||||||
return super().render_to_response(context, **response_kwargs)
|
|
||||||
|
|
||||||
def get_success_url(self):
|
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})
|
||||||
@@ -130,19 +158,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
|
||||||
|
|
||||||
@@ -155,37 +181,56 @@ 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('RIGS/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': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
|
||||||
class EventArchive(generic.ListView):
|
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
|
||||||
template_name = "event_archive.html"
|
}
|
||||||
|
},
|
||||||
|
'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('[^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.ArchiveIndexView):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
|
date_field = "start_date"
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['start'] = self.request.GET.get('start', None)
|
|
||||||
context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
|
|
||||||
context['statuses'] = models.Event.EVENT_STATUS_CHOICES
|
|
||||||
context['page_title'] = 'Event Archive'
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
start = self.request.GET.get('start', None)
|
start = self.request.GET.get('start', None)
|
||||||
end = self.request.GET.get('end', datetime.date.today())
|
end = self.request.GET.get('end', datetime.date.today())
|
||||||
@@ -196,38 +241,32 @@ class EventArchive(generic.ListView):
|
|||||||
"Muppet! Check the dates, it has been fixed for you.")
|
"Muppet! Check the dates, it has been fixed for you.")
|
||||||
start, end = end, start # Stop the impending fail
|
start, end = end, start # Stop the impending fail
|
||||||
|
|
||||||
filter = Q()
|
filter = False
|
||||||
if end != "":
|
if end != "":
|
||||||
filter &= Q(start_date__lte=end)
|
filter = Q(start_date__lte=end)
|
||||||
if start:
|
if start:
|
||||||
filter &= Q(start_date__gte=start)
|
if filter:
|
||||||
|
filter = filter & Q(start_date__gte=start)
|
||||||
|
else:
|
||||||
|
filter = Q(start_date__gte=start)
|
||||||
|
|
||||||
q = self.request.GET.get('q', "")
|
if filter:
|
||||||
objects = self.model.objects.all()
|
qs = self.model.objects.filter(filter).order_by('-start_date')
|
||||||
|
else:
|
||||||
if q:
|
qs = self.model.objects.all().order_by('-start_date')
|
||||||
objects = self.model.objects.search(q)
|
|
||||||
|
|
||||||
status = self.request.GET.getlist('status', "")
|
|
||||||
|
|
||||||
if len(status) > 0:
|
|
||||||
filter &= Q(status__in=status)
|
|
||||||
|
|
||||||
qs = 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
|
||||||
|
|
||||||
|
|
||||||
class EventAuthorise(generic.UpdateView):
|
class EventAuthorise(generic.UpdateView):
|
||||||
template_name = 'eventauthorisation_form.html'
|
template_name = 'RIGS/eventauthorisation_form.html'
|
||||||
success_template = 'eventauthorisation_success.html'
|
success_template = 'RIGS/eventauthorisation_success.html'
|
||||||
preview = False
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.object = form.save()
|
self.object = form.save()
|
||||||
@@ -235,7 +274,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
|
||||||
@@ -249,13 +288,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):
|
||||||
@@ -268,10 +304,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
|
||||||
@@ -287,24 +323,24 @@ 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):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
form_class = forms.EventAuthorisationRequestForm
|
form_class = forms.EventAuthorisationRequestForm
|
||||||
template_name = 'eventauthorisation_request.html'
|
template_name = 'RIGS/eventauthorisation_request.html'
|
||||||
|
|
||||||
@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:
|
||||||
@@ -318,7 +354,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()
|
||||||
|
|
||||||
@@ -337,13 +373,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("eventauthorisation_client_request.txt").render(context),
|
get_template("RIGS/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("eventauthorisation_client_request.html").render(context),
|
html = premailer.Premailer(get_template("RIGS/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')
|
||||||
|
|
||||||
@@ -353,23 +389,47 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
|||||||
|
|
||||||
|
|
||||||
class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
||||||
template_name = "eventauthorisation_client_request.html"
|
template_name = "RIGS/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
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
|
class LogRiskAssessment(generic.View):
|
||||||
|
http_method_names = ["post"]
|
||||||
|
|
||||||
|
def post(self, request, **kwargs):
|
||||||
|
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 shared_secret != settings.RISK_ASSESSMENT_SECRET:
|
||||||
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
|
rig_number = int(re.sub("[^0-9]", "", rig_number))
|
||||||
|
|
||||||
|
event = get_object_or_404(models.Event, pk=rig_number)
|
||||||
|
event.risk_assessment_edit_url = edit_url
|
||||||
|
event.save()
|
||||||
|
|
||||||
|
return HttpResponse(status=200)
|
||||||
@@ -1,21 +1,16 @@
|
|||||||
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.utils import timezone
|
|
||||||
from premailer import Premailer
|
from premailer import Premailer
|
||||||
from registration.signals import user_activated
|
|
||||||
from reversion import revisions as reversion
|
|
||||||
from z3c.rml import rml2pdf
|
from z3c.rml import rml2pdf
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
@@ -25,11 +20,17 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
template = get_template('event_print.xml')
|
template = get_template('RIGS/event_print.xml')
|
||||||
merger = PdfFileMerger()
|
merger = PdfFileMerger()
|
||||||
|
|
||||||
rml = template.render(context)
|
rml = template.render(context)
|
||||||
@@ -54,23 +55,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("eventauthorisation_client_success.txt").render(context),
|
get_template("RIGS/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("eventauthorisation_client_success.html").render(context),
|
html = Premailer(get_template("RIGS/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('[^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 +83,7 @@ def send_eventauthorisation_success_email(instance):
|
|||||||
|
|
||||||
mic_email = EmailMessage(
|
mic_email = EmailMessage(
|
||||||
subject,
|
subject,
|
||||||
get_template("eventauthorisation_mic_success.txt").render(context),
|
get_template("RIGS/eventauthorisation_mic_success.txt").render(context),
|
||||||
to=[mic_email_address]
|
to=[mic_email_address]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -101,43 +102,3 @@ def on_revision_commit(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
post_save.connect(on_revision_commit, sender=models.EventAuthorisation)
|
post_save.connect(on_revision_commit, sender=models.EventAuthorisation)
|
||||||
|
|
||||||
|
|
||||||
def send_admin_awaiting_approval_email(user, request, **kwargs):
|
|
||||||
# Bit more controlled than just emailing all superusers
|
|
||||||
for admin in models.Profile.admins():
|
|
||||||
# Check we've ever emailed them before and if so, if cooldown has passed.
|
|
||||||
if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now():
|
|
||||||
context = {
|
|
||||||
'request': request,
|
|
||||||
'link_suffix': reverse("admin:RIGS_profile_changelist") + '?is_approved__exact=0',
|
|
||||||
'number_of_users': models.Profile.users_awaiting_approval_count(),
|
|
||||||
'to_name': admin.first_name
|
|
||||||
}
|
|
||||||
|
|
||||||
email = EmailMultiAlternatives(
|
|
||||||
f"{context['number_of_users']} new users awaiting approval on RIGS",
|
|
||||||
get_template("admin_awaiting_approval.txt").render(context),
|
|
||||||
to=[admin.email],
|
|
||||||
reply_to=[user.email],
|
|
||||||
)
|
|
||||||
css = finders.find('css/email.css')
|
|
||||||
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
|
|
||||||
external_styles=css).transform()
|
|
||||||
email.attach_alternative(html, 'text/html')
|
|
||||||
email.send()
|
|
||||||
|
|
||||||
# Update last sent
|
|
||||||
admin.last_emailed = timezone.now()
|
|
||||||
admin.save()
|
|
||||||
|
|
||||||
|
|
||||||
user_activated.connect(send_admin_awaiting_approval_email)
|
|
||||||
|
|
||||||
|
|
||||||
def update_cache(sender, instance, created, **kwargs):
|
|
||||||
cache.clear()
|
|
||||||
|
|
||||||
|
|
||||||
for model in reversion.get_registered_models():
|
|
||||||
post_save.connect(update_cache, sender=model)
|
|
||||||
|
|||||||
27
RIGS/static/config.rb
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Require any additional compass plugins here.
|
||||||
|
require 'bootstrap-sass'
|
||||||
|
|
||||||
|
# Set this to the root of your project when deployed:
|
||||||
|
http_path = "/static/"
|
||||||
|
css_dir = "css"
|
||||||
|
sass_dir = "scss"
|
||||||
|
images_dir = "img"
|
||||||
|
javascripts_dir = "js"
|
||||||
|
fonts_dir = "fonts"
|
||||||
|
|
||||||
|
# You can select your preferred output style here (can be overridden via the command line):
|
||||||
|
# output_style = :expanded or :nested or :compact or :compressed
|
||||||
|
output_style = :compressed
|
||||||
|
|
||||||
|
# To enable relative paths to assets via compass helper functions. Uncomment:
|
||||||
|
# relative_assets = true
|
||||||
|
|
||||||
|
# To disable debugging comments that display the original location of your selectors. Uncomment:
|
||||||
|
# line_comments = false
|
||||||
|
|
||||||
|
|
||||||
|
# If you prefer the indented syntax, you might want to regenerate this
|
||||||
|
# project again passing --syntax sass, or you can uncomment this:
|
||||||
|
# preferred_syntax = :sass
|
||||||
|
# and then run:
|
||||||
|
# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass
|
||||||
27
RIGS/static/css/ajax-bootstrap-select.css
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
/*!
|
||||||
|
* Ajax Bootstrap Select
|
||||||
|
*
|
||||||
|
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
|
||||||
|
*
|
||||||
|
* @version 1.3.1
|
||||||
|
* @author Adam Heim - https://github.com/truckingsim
|
||||||
|
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
|
||||||
|
* @copyright 2015 Adam Heim
|
||||||
|
* @license Released under the MIT license.
|
||||||
|
*
|
||||||
|
* Contributors:
|
||||||
|
* Mark Carver - https://github.com/markcarver
|
||||||
|
*
|
||||||
|
* Last build: 2015-01-06 8:43:11 PM EST
|
||||||
|
*/
|
||||||
|
.bootstrap-select .status {
|
||||||
|
background: #f0f0f0;
|
||||||
|
clear: both;
|
||||||
|
color: #999;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
366
RIGS/static/css/bootstrap-datetimepicker.min.css
vendored
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
/*!
|
||||||
|
* Datetimepicker for Bootstrap 3
|
||||||
|
* ! version : 4.7.14
|
||||||
|
* https://github.com/Eonasdan/bootstrap-datetimepicker/
|
||||||
|
*/
|
||||||
|
.bootstrap-datetimepicker-widget {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu {
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 4px;
|
||||||
|
width: 19em;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
||||||
|
width: 38em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
||||||
|
width: 38em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
|
||||||
|
width: 38em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu:before,
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu:after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before {
|
||||||
|
border-left: 7px solid transparent;
|
||||||
|
border-right: 7px solid transparent;
|
||||||
|
border-bottom: 7px solid #cccccc;
|
||||||
|
border-bottom-color: rgba(0, 0, 0, 0.2);
|
||||||
|
top: -7px;
|
||||||
|
left: 7px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-bottom: 6px solid white;
|
||||||
|
top: -6px;
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.top:before {
|
||||||
|
border-left: 7px solid transparent;
|
||||||
|
border-right: 7px solid transparent;
|
||||||
|
border-top: 7px solid #cccccc;
|
||||||
|
border-top-color: rgba(0, 0, 0, 0.2);
|
||||||
|
bottom: -7px;
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.top:after {
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 6px solid white;
|
||||||
|
bottom: -6px;
|
||||||
|
left: 7px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before {
|
||||||
|
left: auto;
|
||||||
|
right: 6px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after {
|
||||||
|
left: auto;
|
||||||
|
right: 7px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .list-unstyled {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget a[data-action] {
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget a[data-action]:active {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .timepicker-hour,
|
||||||
|
.bootstrap-datetimepicker-widget .timepicker-minute,
|
||||||
|
.bootstrap-datetimepicker-widget .timepicker-second {
|
||||||
|
width: 54px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget button[data-action] {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Increment Hours";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Increment Minutes";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Decrement Hours";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Decrement Minutes";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Show Hours";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Show Minutes";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Toggle AM/PM";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Clear the picker";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .btn[data-action="today"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Set the date to today";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .picker-switch {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .picker-switch::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Toggle Date and Time Screens";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .picker-switch td {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget .picker-switch td span {
|
||||||
|
line-height: 2.5;
|
||||||
|
height: 2.5em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td,
|
||||||
|
.bootstrap-datetimepicker-widget table th {
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table th {
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table th.picker-switch {
|
||||||
|
width: 145px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table th.disabled,
|
||||||
|
.bootstrap-datetimepicker-widget table th.disabled:hover {
|
||||||
|
background: none;
|
||||||
|
color: #777777;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table th.prev::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Previous Month";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table th.next::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
content: "Next Month";
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table thead tr:first-child th {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table thead tr:first-child th:hover {
|
||||||
|
background: #eeeeee;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td {
|
||||||
|
height: 54px;
|
||||||
|
line-height: 54px;
|
||||||
|
width: 54px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.cw {
|
||||||
|
font-size: .8em;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.day {
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.day:hover,
|
||||||
|
.bootstrap-datetimepicker-widget table td.hour:hover,
|
||||||
|
.bootstrap-datetimepicker-widget table td.minute:hover,
|
||||||
|
.bootstrap-datetimepicker-widget table td.second:hover {
|
||||||
|
background: #eeeeee;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.old,
|
||||||
|
.bootstrap-datetimepicker-widget table td.new {
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.today {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.today:before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
border: 0 0 7px 7px solid transparent;
|
||||||
|
border-bottom-color: #337ab7;
|
||||||
|
border-top-color: rgba(0, 0, 0, 0.2);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.active,
|
||||||
|
.bootstrap-datetimepicker-widget table td.active:hover {
|
||||||
|
background-color: #337ab7;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.active.today:before {
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td.disabled,
|
||||||
|
.bootstrap-datetimepicker-widget table td.disabled:hover {
|
||||||
|
background: none;
|
||||||
|
color: #777777;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
line-height: 54px;
|
||||||
|
margin: 2px 1.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td span:hover {
|
||||||
|
background: #eeeeee;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td span.active {
|
||||||
|
background-color: #337ab7;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td span.old {
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget table td span.disabled,
|
||||||
|
.bootstrap-datetimepicker-widget table td span.disabled:hover {
|
||||||
|
background: none;
|
||||||
|
color: #777777;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.bootstrap-datetimepicker-widget.usetwentyfour td.hour {
|
||||||
|
height: 27px;
|
||||||
|
line-height: 27px;
|
||||||
|
}
|
||||||
|
.input-group.date .input-group-addon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
6
RIGS/static/css/bootstrap-select.min.css
vendored
Executable file
1
RIGS/static/css/email.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
body{margin:0px}.main-table{width:100%;border-collapse:collapse}.client-header{background-image:url("https://www.nottinghamtec.co.uk/imgs/wof2014-1.jpg");background-color:#222;background-repeat:no-repeat;background-position:center;width:100%;margin-bottom:28px}.client-header .logos{width:100%;max-width:640px}.client-header img{height:110px}.content-container{width:100%}.content-container .content{font-family:"Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;width:100%;max-width:600px;padding:10px;text-align:left}.content-container .content .button-container{width:100%}.content-container .content .button-container .button{padding:6px 12px;background-color:#357ebf;border-radius:4px}.content-container .content .button-container .button a{color:#fff;text-decoration:none}
|
||||||
1061
RIGS/static/css/fullcalendar.css
Executable file
202
RIGS/static/css/fullcalendar.print.css
Executable file
@@ -0,0 +1,202 @@
|
|||||||
|
/*!
|
||||||
|
* FullCalendar v2.3.1 Print Stylesheet
|
||||||
|
* Docs & License: http://fullcalendar.io/
|
||||||
|
* (c) 2015 Adam Shaw
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Include this stylesheet on your page to get a more printer-friendly calendar.
|
||||||
|
* When including this stylesheet, use the media='print' attribute of the <link> tag.
|
||||||
|
* Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.fc {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Global Event Restyling
|
||||||
|
--------------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
.fc-event {
|
||||||
|
background: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-event .fc-resizer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Table & Day-Row Restyling
|
||||||
|
--------------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
th,
|
||||||
|
td,
|
||||||
|
hr,
|
||||||
|
thead,
|
||||||
|
tbody,
|
||||||
|
.fc-row {
|
||||||
|
border-color: #ccc !important;
|
||||||
|
background: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* kill the overlaid, absolutely-positioned common components */
|
||||||
|
.fc-bg,
|
||||||
|
.fc-bgevent-skeleton,
|
||||||
|
.fc-highlight-skeleton,
|
||||||
|
.fc-helper-skeleton {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* don't force a min-height on rows (for DayGrid) */
|
||||||
|
.fc tbody .fc-row {
|
||||||
|
height: auto !important; /* undo height that JS set in distributeHeight */
|
||||||
|
min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc tbody .fc-row .fc-content-skeleton {
|
||||||
|
position: static; /* undo .fc-rigid */
|
||||||
|
padding-bottom: 0 !important; /* use a more border-friendly method for this... */
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */
|
||||||
|
padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc tbody .fc-row .fc-content-skeleton table {
|
||||||
|
/* provides a min-height for the row, but only effective for IE, which exaggerates this value,
|
||||||
|
making it look more like 3em. for other browers, it will already be this tall */
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Undo month-view event limiting. Display all events and hide the "more" links
|
||||||
|
--------------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
.fc-more-cell,
|
||||||
|
.fc-more {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc tr.fc-limited {
|
||||||
|
display: table-row !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc td.fc-limited {
|
||||||
|
display: table-cell !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-popover {
|
||||||
|
display: none; /* never display the "more.." popover in print mode */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* TimeGrid Restyling
|
||||||
|
--------------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/* undo the min-height 100% trick used to fill the container's height */
|
||||||
|
.fc-time-grid {
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* don't display the side axis at all ("all-day" and time cells) */
|
||||||
|
.fc-agenda-view .fc-axis {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* don't display the horizontal lines */
|
||||||
|
.fc-slats,
|
||||||
|
.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */
|
||||||
|
display: none !important; /* important overrides inline declaration */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* let the container that holds the events be naturally positioned and create real height */
|
||||||
|
.fc-time-grid .fc-content-skeleton {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* in case there are no events, we still want some height */
|
||||||
|
.fc-time-grid .fc-content-skeleton table {
|
||||||
|
height: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* kill the horizontal spacing made by the event container. event margins will be done below */
|
||||||
|
.fc-time-grid .fc-event-container {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* TimeGrid *Event* Restyling
|
||||||
|
--------------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/* naturally position events, vertically stacking them */
|
||||||
|
.fc-time-grid .fc-event {
|
||||||
|
position: static !important;
|
||||||
|
margin: 3px 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* for events that continue to a future day, give the bottom border back */
|
||||||
|
.fc-time-grid .fc-event.fc-not-end {
|
||||||
|
border-bottom-width: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* indicate the event continues via "..." text */
|
||||||
|
.fc-time-grid .fc-event.fc-not-end:after {
|
||||||
|
content: "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* for events that are continuations from previous days, give the top border back */
|
||||||
|
.fc-time-grid .fc-event.fc-not-start {
|
||||||
|
border-top-width: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* indicate the event is a continuation via "..." text */
|
||||||
|
.fc-time-grid .fc-event.fc-not-start:before {
|
||||||
|
content: "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* time */
|
||||||
|
|
||||||
|
/* undo a previous declaration and let the time text span to a second line */
|
||||||
|
.fc-time-grid .fc-event .fc-time {
|
||||||
|
white-space: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hide the the time that is normally displayed... */
|
||||||
|
.fc-time-grid .fc-event .fc-time span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */
|
||||||
|
.fc-time-grid .fc-event .fc-time:after {
|
||||||
|
content: attr(data-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Vertical Scroller & Containers
|
||||||
|
--------------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/* kill the scrollbars and allow natural height */
|
||||||
|
.fc-scroller,
|
||||||
|
.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */
|
||||||
|
.fc-time-grid-container { /* */
|
||||||
|
overflow: visible !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* kill the horizontal border/padding used to compensate for scrollbars */
|
||||||
|
.fc-row {
|
||||||
|
border: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Button Controls
|
||||||
|
--------------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
.fc-button-group,
|
||||||
|
.fc button {
|
||||||
|
display: none; /* don't display any button-related controls */
|
||||||
|
}
|
||||||
5
RIGS/static/css/print.css
Normal file
23
RIGS/static/css/screen.css
Normal file
BIN
RIGS/static/fonts/GEORGIA.TTF
Normal file
BIN
RIGS/static/fonts/GEORGIAB.TTF
Normal file
BIN
RIGS/static/fonts/GEORGIAI.TTF
Normal file
BIN
RIGS/static/fonts/GEORGIAZ.TTF
Normal file
BIN
RIGS/static/fonts/OPENSANS-BOLDITALIC.TTF
Normal file
BIN
RIGS/static/fonts/OPENSANS-ITALIC.TTF
Normal file
BIN
RIGS/static/fonts/glyphicons-halflings-regular.eot
Normal file
1
RIGS/static/fonts/glyphicons-halflings-regular.svg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
RIGS/static/fonts/glyphicons-halflings-regular.ttf
Normal file
BIN
RIGS/static/fonts/glyphicons-halflings-regular.woff
Normal file
BIN
RIGS/static/fonts/glyphicons-halflings-regular.woff2
Normal file
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 5.4 MiB |
|
Before Width: | Height: | Size: 852 KiB |
|
Before Width: | Height: | Size: 164 KiB |
162
RIGS/static/js/affix.js
Executable file
@@ -0,0 +1,162 @@
|
|||||||
|
/* ========================================================================
|
||||||
|
* Bootstrap: affix.js v3.3.7
|
||||||
|
* http://getbootstrap.com/javascript/#affix
|
||||||
|
* ========================================================================
|
||||||
|
* Copyright 2011-2016 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* ======================================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
+function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// AFFIX CLASS DEFINITION
|
||||||
|
// ======================
|
||||||
|
|
||||||
|
var Affix = function (element, options) {
|
||||||
|
this.options = $.extend({}, Affix.DEFAULTS, options)
|
||||||
|
|
||||||
|
this.$target = $(this.options.target)
|
||||||
|
.on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this))
|
||||||
|
.on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this))
|
||||||
|
|
||||||
|
this.$element = $(element)
|
||||||
|
this.affixed = null
|
||||||
|
this.unpin = null
|
||||||
|
this.pinnedOffset = null
|
||||||
|
|
||||||
|
this.checkPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
Affix.VERSION = '3.3.7'
|
||||||
|
|
||||||
|
Affix.RESET = 'affix affix-top affix-bottom'
|
||||||
|
|
||||||
|
Affix.DEFAULTS = {
|
||||||
|
offset: 0,
|
||||||
|
target: window
|
||||||
|
}
|
||||||
|
|
||||||
|
Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) {
|
||||||
|
var scrollTop = this.$target.scrollTop()
|
||||||
|
var position = this.$element.offset()
|
||||||
|
var targetHeight = this.$target.height()
|
||||||
|
|
||||||
|
if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false
|
||||||
|
|
||||||
|
if (this.affixed == 'bottom') {
|
||||||
|
if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom'
|
||||||
|
return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom'
|
||||||
|
}
|
||||||
|
|
||||||
|
var initializing = this.affixed == null
|
||||||
|
var colliderTop = initializing ? scrollTop : position.top
|
||||||
|
var colliderHeight = initializing ? targetHeight : height
|
||||||
|
|
||||||
|
if (offsetTop != null && scrollTop <= offsetTop) return 'top'
|
||||||
|
if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom'
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
Affix.prototype.getPinnedOffset = function () {
|
||||||
|
if (this.pinnedOffset) return this.pinnedOffset
|
||||||
|
this.$element.removeClass(Affix.RESET).addClass('affix')
|
||||||
|
var scrollTop = this.$target.scrollTop()
|
||||||
|
var position = this.$element.offset()
|
||||||
|
return (this.pinnedOffset = position.top - scrollTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
Affix.prototype.checkPositionWithEventLoop = function () {
|
||||||
|
setTimeout($.proxy(this.checkPosition, this), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Affix.prototype.checkPosition = function () {
|
||||||
|
if (!this.$element.is(':visible')) return
|
||||||
|
|
||||||
|
var height = this.$element.height()
|
||||||
|
var offset = this.options.offset
|
||||||
|
var offsetTop = offset.top
|
||||||
|
var offsetBottom = offset.bottom
|
||||||
|
var scrollHeight = Math.max($(document).height(), $(document.body).height())
|
||||||
|
|
||||||
|
if (typeof offset != 'object') offsetBottom = offsetTop = offset
|
||||||
|
if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element)
|
||||||
|
if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element)
|
||||||
|
|
||||||
|
var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom)
|
||||||
|
|
||||||
|
if (this.affixed != affix) {
|
||||||
|
if (this.unpin != null) this.$element.css('top', '')
|
||||||
|
|
||||||
|
var affixType = 'affix' + (affix ? '-' + affix : '')
|
||||||
|
var e = $.Event(affixType + '.bs.affix')
|
||||||
|
|
||||||
|
this.$element.trigger(e)
|
||||||
|
|
||||||
|
if (e.isDefaultPrevented()) return
|
||||||
|
|
||||||
|
this.affixed = affix
|
||||||
|
this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null
|
||||||
|
|
||||||
|
this.$element
|
||||||
|
.removeClass(Affix.RESET)
|
||||||
|
.addClass(affixType)
|
||||||
|
.trigger(affixType.replace('affix', 'affixed') + '.bs.affix')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (affix == 'bottom') {
|
||||||
|
this.$element.offset({
|
||||||
|
top: scrollHeight - height - offsetBottom
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// AFFIX PLUGIN DEFINITION
|
||||||
|
// =======================
|
||||||
|
|
||||||
|
function Plugin(option) {
|
||||||
|
return this.each(function () {
|
||||||
|
var $this = $(this)
|
||||||
|
var data = $this.data('bs.affix')
|
||||||
|
var options = typeof option == 'object' && option
|
||||||
|
|
||||||
|
if (!data) $this.data('bs.affix', (data = new Affix(this, options)))
|
||||||
|
if (typeof option == 'string') data[option]()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var old = $.fn.affix
|
||||||
|
|
||||||
|
$.fn.affix = Plugin
|
||||||
|
$.fn.affix.Constructor = Affix
|
||||||
|
|
||||||
|
|
||||||
|
// AFFIX NO CONFLICT
|
||||||
|
// =================
|
||||||
|
|
||||||
|
$.fn.affix.noConflict = function () {
|
||||||
|
$.fn.affix = old
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// AFFIX DATA-API
|
||||||
|
// ==============
|
||||||
|
|
||||||
|
$(window).on('load', function () {
|
||||||
|
$('[data-spy="affix"]').each(function () {
|
||||||
|
var $spy = $(this)
|
||||||
|
var data = $spy.data()
|
||||||
|
|
||||||
|
data.offset = data.offset || {}
|
||||||
|
|
||||||
|
if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom
|
||||||
|
if (data.offsetTop != null) data.offset.top = data.offsetTop
|
||||||
|
|
||||||
|
Plugin.call($spy, data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}(jQuery);
|
||||||
1612
RIGS/static/js/ajax-bootstrap-select.js
Normal file
94
RIGS/static/js/alert.js
Executable file
@@ -0,0 +1,94 @@
|
|||||||
|
/* ========================================================================
|
||||||
|
* Bootstrap: alert.js v3.3.7
|
||||||
|
* http://getbootstrap.com/javascript/#alerts
|
||||||
|
* ========================================================================
|
||||||
|
* Copyright 2011-2016 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* ======================================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
+function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ALERT CLASS DEFINITION
|
||||||
|
// ======================
|
||||||
|
|
||||||
|
var dismiss = '[data-dismiss="alert"]'
|
||||||
|
var Alert = function (el) {
|
||||||
|
$(el).on('click', dismiss, this.close)
|
||||||
|
}
|
||||||
|
|
||||||
|
Alert.VERSION = '3.3.7'
|
||||||
|
|
||||||
|
Alert.TRANSITION_DURATION = 150
|
||||||
|
|
||||||
|
Alert.prototype.close = function (e) {
|
||||||
|
var $this = $(this)
|
||||||
|
var selector = $this.attr('data-target')
|
||||||
|
|
||||||
|
if (!selector) {
|
||||||
|
selector = $this.attr('href')
|
||||||
|
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
|
||||||
|
}
|
||||||
|
|
||||||
|
var $parent = $(selector === '#' ? [] : selector)
|
||||||
|
|
||||||
|
if (e) e.preventDefault()
|
||||||
|
|
||||||
|
if (!$parent.length) {
|
||||||
|
$parent = $this.closest('.alert')
|
||||||
|
}
|
||||||
|
|
||||||
|
$parent.trigger(e = $.Event('close.bs.alert'))
|
||||||
|
|
||||||
|
if (e.isDefaultPrevented()) return
|
||||||
|
|
||||||
|
$parent.removeClass('in')
|
||||||
|
|
||||||
|
function removeElement() {
|
||||||
|
// detach from parent, fire event then clean up data
|
||||||
|
$parent.detach().trigger('closed.bs.alert').remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
$.support.transition && $parent.hasClass('fade') ?
|
||||||
|
$parent
|
||||||
|
.one('bsTransitionEnd', removeElement)
|
||||||
|
.emulateTransitionEnd(Alert.TRANSITION_DURATION) :
|
||||||
|
removeElement()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ALERT PLUGIN DEFINITION
|
||||||
|
// =======================
|
||||||
|
|
||||||
|
function Plugin(option) {
|
||||||
|
return this.each(function () {
|
||||||
|
var $this = $(this)
|
||||||
|
var data = $this.data('bs.alert')
|
||||||
|
|
||||||
|
if (!data) $this.data('bs.alert', (data = new Alert(this)))
|
||||||
|
if (typeof option == 'string') data[option].call($this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var old = $.fn.alert
|
||||||
|
|
||||||
|
$.fn.alert = Plugin
|
||||||
|
$.fn.alert.Constructor = Alert
|
||||||
|
|
||||||
|
|
||||||
|
// ALERT NO CONFLICT
|
||||||
|
// =================
|
||||||
|
|
||||||
|
$.fn.alert.noConflict = function () {
|
||||||
|
$.fn.alert = old
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ALERT DATA-API
|
||||||
|
// ==============
|
||||||
|
|
||||||
|
$(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close)
|
||||||
|
|
||||||
|
}(jQuery);
|
||||||
73
RIGS/static/js/asteroids.min.js
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
|
||||||
|
(function(){function Asteroids(){if(!window.ASTEROIDS)
|
||||||
|
window.ASTEROIDS={enemiesKilled:0};function Vector(x,y){if(typeof x=='Object'){this.x=x.x;this.y=x.y;}else{this.x=x;this.y=y;}};Vector.prototype={cp:function(){return new Vector(this.x,this.y);},mul:function(factor){this.x*=factor;this.y*=factor;return this;},mulNew:function(factor){return new Vector(this.x*factor,this.y*factor);},add:function(vec){this.x+=vec.x;this.y+=vec.y;return this;},addNew:function(vec){return new Vector(this.x+vec.x,this.y+vec.y);},sub:function(vec){this.x-=vec.x;this.y-=vec.y;return this;},subNew:function(vec){return new Vector(this.x-vec.x,this.y-vec.y);},rotate:function(angle){var x=this.x,y=this.y;this.x=x*Math.cos(angle)-Math.sin(angle)*y;this.y=x*Math.sin(angle)+Math.cos(angle)*y;return this;},rotateNew:function(angle){return this.cp().rotate(angle);},setAngle:function(angle){var l=this.len();this.x=Math.cos(angle)*l;this.y=Math.sin(angle)*l;return this;},setAngleNew:function(angle){return this.cp().setAngle(angle);},setLength:function(length){var l=this.len();if(l)this.mul(length/l);else this.x=this.y=length;return this;},setLengthNew:function(length){return this.cp().setLength(length);},normalize:function(){var l=this.len();this.x/=l;this.y/=l;return this;},normalizeNew:function(){return this.cp().normalize();},angle:function(){return Math.atan2(this.y,this.x);},collidesWith:function(rect){return this.x>rect.x&&this.y>rect.y&&this.x<rect.x+rect.width&&this.y<rect.y+rect.height;},len:function(){var l=Math.sqrt(this.x*this.x+this.y*this.y);if(l<0.005&&l>-0.005)return 0;return l;},is:function(test){return typeof test=='object'&&this.x==test.x&&this.y==test.y;},toString:function(){return'[Vector('+this.x+', '+this.y+') angle: '+this.angle()+', length: '+this.len()+']';}};function Line(p1,p2){this.p1=p1;this.p2=p2;};Line.prototype={shift:function(pos){this.p1.add(pos);this.p2.add(pos);},intersectsWithRect:function(rect){var LL=new Vector(rect.x,rect.y+rect.height);var UL=new Vector(rect.x,rect.y);var LR=new Vector(rect.x+rect.width,rect.y+rect.height);var UR=new Vector(rect.x+rect.width,rect.y);if(this.p1.x>LL.x&&this.p1.x<UR.x&&this.p1.y<LL.y&&this.p1.y>UR.y&&this.p2.x>LL.x&&this.p2.x<UR.x&&this.p2.y<LL.y&&this.p2.y>UR.y)return true;if(this.intersectsLine(new Line(UL,LL)))return true;if(this.intersectsLine(new Line(LL,LR)))return true;if(this.intersectsLine(new Line(UL,UR)))return true;if(this.intersectsLine(new Line(UR,LR)))return true;return false;},intersectsLine:function(line2){var v1=this.p1,v2=this.p2;var v3=line2.p1,v4=line2.p2;var denom=((v4.y-v3.y)*(v2.x-v1.x))-((v4.x-v3.x)*(v2.y-v1.y));var numerator=((v4.x-v3.x)*(v1.y-v3.y))-((v4.y-v3.y)*(v1.x-v3.x));var numerator2=((v2.x-v1.x)*(v1.y-v3.y))-((v2.y-v1.y)*(v1.x-v3.x));if(denom==0.0){return false;}
|
||||||
|
var ua=numerator/denom;var ub=numerator2/denom;return(ua>=0.0&&ua<=1.0&&ub>=0.0&&ub<=1.0);}};var that=this;var isIE=!!window.ActiveXObject;var isIEQuirks=isIE&&document.compatMode=="BackCompat";var w=document.documentElement.clientWidth,h=document.documentElement.clientHeight;if(isIEQuirks){w=document.body.clientWidth;h=document.body.clientHeight;}
|
||||||
|
var playerWidth=20,playerHeight=30;var playerVerts=[[-1*playerHeight/2,-1*playerWidth/2],[-1*playerHeight/2,playerWidth/2],[playerHeight/2,0]];var ignoredTypes=['HTML','HEAD','BODY','SCRIPT','TITLE','META','STYLE','LINK'];if(window.ActiveXObject)
|
||||||
|
ignoredTypes=['HTML','HEAD','BODY','SCRIPT','TITLE','META','STYLE','LINK','SHAPE','LINE','GROUP','IMAGE','STROKE','FILL','SKEW','PATH','TEXTPATH','INS'];var hiddenTypes=['BR','HR'];var FPS=50;var acc=300;var maxSpeed=600;var rotSpeed=360;var bulletSpeed=700;var particleSpeed=400;var timeBetweenFire=150;var timeBetweenBlink=250;var timeBetweenEnemyUpdate=isIE?10000:2000;var bulletRadius=2;var maxParticles=isIE?20:40;var maxBullets=isIE?10:20;this.flame={r:[],y:[]};this.toggleBlinkStyle=function(){if(this.updated.blink.isActive){removeClass(document.body,'ASTEROIDSBLINK');}else{addClass(document.body,'ASTEROIDSBLINK');}
|
||||||
|
this.updated.blink.isActive=!this.updated.blink.isActive;};addStylesheet(".ASTEROIDSBLINK .ASTEROIDSYEAHENEMY","outline: 2px dotted red;");this.pos=new Vector(100,100);this.lastPos=false;this.vel=new Vector(0,0);this.dir=new Vector(0,1);this.keysPressed={};this.firedAt=false;this.updated={enemies:false,flame:new Date().getTime(),blink:{time:0,isActive:false}};this.scrollPos=new Vector(0,0);this.bullets=[];this.enemies=[];this.dying=[];this.totalEnemies=0;this.particles=[];function updateEnemyIndex(){for(var i=0,enemy;enemy=that.enemies[i];i++)
|
||||||
|
removeClass(enemy,"ASTEROIDSYEAHENEMY");var all=document.body.getElementsByTagName('*');that.enemies=[];for(var i=0,el;el=all[i];i++){if(indexOf(ignoredTypes,el.tagName.toUpperCase())==-1&&el.prefix!='g_vml_'&&hasOnlyTextualChildren(el)&&el.className!="ASTEROIDSYEAH"&&el.offsetHeight>0){el.aSize=size(el);that.enemies.push(el);addClass(el,"ASTEROIDSYEAHENEMY");if(!el.aAdded){el.aAdded=true;that.totalEnemies++;}}}};updateEnemyIndex();var createFlames;(function(){var rWidth=playerWidth,rIncrease=playerWidth*0.1,yWidth=playerWidth*0.6,yIncrease=yWidth*0.2,halfR=rWidth/2,halfY=yWidth/2,halfPlayerHeight=playerHeight/2;createFlames=function(){that.flame.r=[[-1*halfPlayerHeight,-1*halfR]];that.flame.y=[[-1*halfPlayerHeight,-1*halfY]];for(var x=0;x<rWidth;x+=rIncrease){that.flame.r.push([-random(2,7)-halfPlayerHeight,x-halfR]);}
|
||||||
|
that.flame.r.push([-1*halfPlayerHeight,halfR]);for(var x=0;x<yWidth;x+=yIncrease){that.flame.y.push([-random(2,7)-halfPlayerHeight,x-halfY]);}
|
||||||
|
that.flame.y.push([-1*halfPlayerHeight,halfY]);};})();createFlames();function radians(deg){return deg*0.0174532925;};function degrees(rad){return rad*57.2957795;};function random(from,to){return Math.floor(Math.random()*(to+1)+from);};function code(name){var table={'up':38,'down':40,'left':37,'right':39,'esc':27};if(table[name])return table[name];return name.charCodeAt(0);};function boundsCheck(vec){if(vec.x>w)
|
||||||
|
vec.x=0;else if(vec.x<0)
|
||||||
|
vec.x=w;if(vec.y>h)
|
||||||
|
vec.y=0;else if(vec.y<0)
|
||||||
|
vec.y=h;};function size(element){var el=element,left=0,top=0;do{left+=el.offsetLeft||0;top+=el.offsetTop||0;el=el.offsetParent;}while(el);return{x:left,y:top,width:element.offsetWidth||10,height:element.offsetHeight||10};};function addEvent(obj,type,fn){if(obj.addEventListener)
|
||||||
|
obj.addEventListener(type,fn,false);else if(obj.attachEvent){obj["e"+type+fn]=fn;obj[type+fn]=function(){obj["e"+type+fn](window.event);}
|
||||||
|
obj.attachEvent("on"+type,obj[type+fn]);}}
|
||||||
|
function removeEvent(obj,type,fn){if(obj.removeEventListener)
|
||||||
|
obj.removeEventListener(type,fn,false);else if(obj.detachEvent){obj.detachEvent("on"+type,obj[type+fn]);obj[type+fn]=null;obj["e"+type+fn]=null;}}
|
||||||
|
function arrayRemove(array,from,to){var rest=array.slice((to||from)+1||array.length);array.length=from<0?array.length+from:from;return array.push.apply(array,rest);};function applyVisibility(vis){for(var i=0,p;p=window.ASTEROIDSPLAYERS[i];i++){p.gameContainer.style.visibility=vis;}}
|
||||||
|
function getElementFromPoint(x,y){applyVisibility('hidden');var element=document.elementFromPoint(x,y);if(!element){applyVisibility('visible');return false;}
|
||||||
|
if(element.nodeType==3)
|
||||||
|
element=element.parentNode;applyVisibility('visible');return element;};function addParticles(startPos){var time=new Date().getTime();var amount=maxParticles;for(var i=0;i<amount;i++){that.particles.push({dir:(new Vector(Math.random()*20-10,Math.random()*20-10)).normalize(),pos:startPos.cp(),cameAlive:time});}};function setScore(){that.points.innerHTML=window.ASTEROIDS.enemiesKilled*10;};function hasOnlyTextualChildren(element){if(element.offsetLeft<-100&&element.offsetWidth>0&&element.offsetHeight>0)return false;if(indexOf(hiddenTypes,element.tagName)!=-1)return true;if(element.offsetWidth==0&&element.offsetHeight==0)return false;for(var i=0;i<element.childNodes.length;i++){if(indexOf(hiddenTypes,element.childNodes[i].tagName)==-1&&element.childNodes[i].childNodes.length!=0)return false;}
|
||||||
|
return true;};function indexOf(arr,item,from){if(arr.indexOf)return arr.indexOf(item,from);var len=arr.length;for(var i=(from<0)?Math.max(0,len+from):from||0;i<len;i++){if(arr[i]===item)return i;}
|
||||||
|
return-1;};function addClass(element,className){if(element.className.indexOf(className)==-1)
|
||||||
|
element.className=(element.className+' '+className).replace(/\s+/g,' ').replace(/^\s+|\s+$/g,'');};function removeClass(element,className){element.className=element.className.replace(new RegExp('(^|\\s)'+className+'(?:\\s|$)'),'$1');};function addStylesheet(selector,rules){var stylesheet=document.createElement('style');stylesheet.type='text/css';stylesheet.rel='stylesheet';stylesheet.id='ASTEROIDSYEAHSTYLES';try{stylesheet.innerHTML=selector+"{"+rules+"}";}catch(e){stylesheet.styleSheet.addRule(selector,rules);}
|
||||||
|
document.getElementsByTagName("head")[0].appendChild(stylesheet);};function removeStylesheet(name){var stylesheet=document.getElementById(name);if(stylesheet){stylesheet.parentNode.removeChild(stylesheet);}};this.gameContainer=document.createElement('div');this.gameContainer.className='ASTEROIDSYEAH';document.body.appendChild(this.gameContainer);this.canvas=document.createElement('canvas');this.canvas.setAttribute('width',w);this.canvas.setAttribute('height',h);this.canvas.className='ASTEROIDSYEAH';with(this.canvas.style){width=w+"px";height=h+"px";position="fixed";top="0px";left="0px";bottom="0px";right="0px";zIndex="10000";}
|
||||||
|
if(typeof G_vmlCanvasManager!='undefined'){this.canvas=G_vmlCanvasManager.initElement(this.canvas);if(!this.canvas.getContext){alert("So... you're using IE? Please join me at http://github.com/erkie/erkie.github.com if you think you can help");}}else{if(!this.canvas.getContext){alert('This program does not yet support your browser. Please join me at http://github.com/erkie/erkie.github.com if you think you can help');}}
|
||||||
|
addEvent(this.canvas,'mousedown',function(e){e=e||window.event;var message=document.createElement('span');message.style.position='absolute';message.style.border='1px solid #999';message.style.background='white';message.style.color="black";message.innerHTML='Press Esc to quit';document.body.appendChild(message);var x=e.pageX||(e.clientX+document.documentElement.scrollLeft);var y=e.pageY||(e.clientY+document.documentElement.scrollTop);message.style.left=x-message.offsetWidth/2+'px';message.style.top=y-message.offsetHeight/2+'px';setTimeout(function(){try{message.parentNode.removeChild(message);}catch(e){}},1000);});var eventResize=function(){if(!isIE){that.canvas.style.display="none";w=document.documentElement.clientWidth;h=document.documentElement.clientHeight;that.canvas.setAttribute('width',w);that.canvas.setAttribute('height',h);with(that.canvas.style){display="block";width=w+"px";height=h+"px";}}else{w=document.documentElement.clientWidth;h=document.documentElement.clientHeight;if(isIEQuirks){w=document.body.clientWidth;h=document.body.clientHeight;}
|
||||||
|
that.canvas.setAttribute('width',w);that.canvas.setAttribute('height',h);}};addEvent(window,'resize',eventResize);this.gameContainer.appendChild(this.canvas);this.ctx=this.canvas.getContext("2d");this.ctx.fillStyle="black";this.ctx.strokeStyle="black";if(!document.getElementById('ASTEROIDS-NAVIGATION')){this.navigation=document.createElement('div');this.navigation.id="ASTEROIDS-NAVIGATION";this.navigation.className="ASTEROIDSYEAH";with(this.navigation.style){fontFamily="Arial,sans-serif";position="fixed";zIndex="10001";bottom="10px";right="10px";textAlign="right";}
|
||||||
|
this.navigation.innerHTML="(press esc to quit) ";this.gameContainer.appendChild(this.navigation);this.points=document.createElement('span');this.points.id='ASTEROIDS-POINTS';this.points.style.font="28pt Arial, sans-serif";this.points.style.fontWeight="bold";this.points.className="ASTEROIDSYEAH";this.navigation.appendChild(this.points);}else{this.navigation=document.getElementById('ASTEROIDS-NAVIGATION');this.points=document.getElementById('ASTEROIDS-POINTS');}
|
||||||
|
if(isIEQuirks){this.gameContainer.style.position=this.canvas.style.position=this.navigation.style.position="absolute";}
|
||||||
|
setScore();if(typeof G_vmlCanvasManager!='undefined'){var children=this.canvas.getElementsByTagName('*');for(var i=0,c;c=children[i];i++)
|
||||||
|
addClass(c,'ASTEROIDSYEAH');}
|
||||||
|
var eventKeydown=function(event){event=event||window.event;that.keysPressed[event.keyCode]=true;switch(event.keyCode){case code(' '):that.firedAt=1;break;}
|
||||||
|
if(indexOf([code('up'),code('down'),code('right'),code('left'),code(' '),code('B'),code('W'),code('A'),code('S'),code('D')],event.keyCode)!=-1){if(event.preventDefault)
|
||||||
|
event.preventDefault();if(event.stopPropagation)
|
||||||
|
event.stopPropagation();event.returnValue=false;event.cancelBubble=true;return false;}};addEvent(document,'keydown',eventKeydown);var eventKeypress=function(event){event=event||window.event;if(indexOf([code('up'),code('down'),code('right'),code('left'),code(' '),code('W'),code('A'),code('S'),code('D')],event.keyCode||event.which)!=-1){if(event.preventDefault)
|
||||||
|
event.preventDefault();if(event.stopPropagation)
|
||||||
|
event.stopPropagation();event.returnValue=false;event.cancelBubble=true;return false;}};addEvent(document,'keypress',eventKeypress);var eventKeyup=function(event){event=event||window.event;that.keysPressed[event.keyCode]=false;if(indexOf([code('up'),code('down'),code('right'),code('left'),code(' '),code('B'),code('W'),code('A'),code('S'),code('D')],event.keyCode)!=-1){if(event.preventDefault)
|
||||||
|
event.preventDefault();if(event.stopPropagation)
|
||||||
|
event.stopPropagation();event.returnValue=false;event.cancelBubble=true;return false;}};addEvent(document,'keyup',eventKeyup);this.ctx.clear=function(){this.clearRect(0,0,w,h);};this.ctx.clear();this.ctx.drawLine=function(xFrom,yFrom,xTo,yTo){this.beginPath();this.moveTo(xFrom,yFrom);this.lineTo(xTo,yTo);this.lineTo(xTo+1,yTo+1);this.closePath();this.fill();};this.ctx.tracePoly=function(verts){this.beginPath();this.moveTo(verts[0][0],verts[0][1]);for(var i=1;i<verts.length;i++)
|
||||||
|
this.lineTo(verts[i][0],verts[i][1]);this.closePath();};this.ctx.drawPlayer=function(){this.save();this.translate(that.pos.x,that.pos.y);this.rotate(that.dir.angle());this.tracePoly(playerVerts);this.fillStyle="white";this.fill();this.tracePoly(playerVerts);this.stroke();this.restore();};var PI_SQ=Math.PI*2;this.ctx.drawBullets=function(bullets){for(var i=0;i<bullets.length;i++){this.beginPath();this.arc(bullets[i].pos.x,bullets[i].pos.y,bulletRadius,0,PI_SQ,true);this.closePath();this.fill();}};var randomParticleColor=function(){return(['red','yellow'])[random(0,1)];};this.ctx.drawParticles=function(particles){var oldColor=this.fillStyle;for(var i=0;i<particles.length;i++){this.fillStyle=randomParticleColor();this.drawLine(particles[i].pos.x,particles[i].pos.y,particles[i].pos.x-particles[i].dir.x*10,particles[i].pos.y-particles[i].dir.y*10);}
|
||||||
|
this.fillStyle=oldColor;};this.ctx.drawFlames=function(flame){this.save();this.translate(that.pos.x,that.pos.y);this.rotate(that.dir.angle());var oldColor=this.strokeStyle;this.strokeStyle="red";this.tracePoly(flame.r);this.stroke();this.strokeStyle="yellow";this.tracePoly(flame.y);this.stroke();this.strokeStyle=oldColor;this.restore();}
|
||||||
|
try{window.focus();}catch(e){}
|
||||||
|
addParticles(this.pos);addClass(document.body,'ASTEROIDSYEAH');var isRunning=true;var lastUpdate=new Date().getTime();this.update=function(){var forceChange=false;var nowTime=new Date().getTime();var tDelta=(nowTime-lastUpdate)/1000;lastUpdate=nowTime;var drawFlame=false;if(nowTime-this.updated.flame>50){createFlames();this.updated.flame=nowTime;}
|
||||||
|
this.scrollPos.x=window.pageXOffset||document.documentElement.scrollLeft;this.scrollPos.y=window.pageYOffset||document.documentElement.scrollTop;if((this.keysPressed[code('up')])||(this.keysPressed[code('W')])){this.vel.add(this.dir.mulNew(acc*tDelta));drawFlame=true;}else{this.vel.mul(0.96);}
|
||||||
|
if((this.keysPressed[code('left')])||(this.keysPressed[code('A')])){forceChange=true;this.dir.rotate(radians(rotSpeed*tDelta*-1));}
|
||||||
|
if((this.keysPressed[code('right')])||(this.keysPressed[code('D')])){forceChange=true;this.dir.rotate(radians(rotSpeed*tDelta));}
|
||||||
|
if(this.keysPressed[code(' ')]&&nowTime-this.firedAt>timeBetweenFire){this.bullets.unshift({'dir':this.dir.cp(),'pos':this.pos.cp(),'startVel':this.vel.cp(),'cameAlive':nowTime});this.firedAt=nowTime;if(this.bullets.length>maxBullets){this.bullets.pop();}}
|
||||||
|
if(this.keysPressed[code('B')]){if(!this.updated.enemies){updateEnemyIndex();this.updated.enemies=true;}
|
||||||
|
forceChange=true;this.updated.blink.time+=tDelta*1000;if(this.updated.blink.time>timeBetweenBlink){this.toggleBlinkStyle();this.updated.blink.time=0;}}else{this.updated.enemies=false;}
|
||||||
|
if(this.keysPressed[code('esc')]){destroy.apply(this);return;}
|
||||||
|
if(this.vel.len()>maxSpeed){this.vel.setLength(maxSpeed);}
|
||||||
|
this.pos.add(this.vel.mulNew(tDelta));if(this.pos.x>w){window.scrollTo(this.scrollPos.x+50,this.scrollPos.y);this.pos.x=0;}else if(this.pos.x<0){window.scrollTo(this.scrollPos.x-50,this.scrollPos.y);this.pos.x=w;}
|
||||||
|
if(this.pos.y>h){window.scrollTo(this.scrollPos.x,this.scrollPos.y+h*0.75);this.pos.y=0;}else if(this.pos.y<0){window.scrollTo(this.scrollPos.x,this.scrollPos.y-h*0.75);this.pos.y=h;}
|
||||||
|
for(var i=this.bullets.length-1;i>=0;i--){if(nowTime-this.bullets[i].cameAlive>2000){this.bullets.splice(i,1);forceChange=true;continue;}
|
||||||
|
var bulletVel=this.bullets[i].dir.setLengthNew(bulletSpeed*tDelta).add(this.bullets[i].startVel.mulNew(tDelta));this.bullets[i].pos.add(bulletVel);boundsCheck(this.bullets[i].pos);var murdered=getElementFromPoint(this.bullets[i].pos.x,this.bullets[i].pos.y);if(murdered&&murdered.tagName&&indexOf(ignoredTypes,murdered.tagName.toUpperCase())==-1&&hasOnlyTextualChildren(murdered)&&murdered.className!="ASTEROIDSYEAH"){didKill=true;addParticles(this.bullets[i].pos);this.dying.push(murdered);this.bullets.splice(i,1);continue;}}
|
||||||
|
if(this.dying.length){for(var i=this.dying.length-1;i>=0;i--){try{if(this.dying[i].parentNode)
|
||||||
|
window.ASTEROIDS.enemiesKilled++;this.dying[i].parentNode.removeChild(this.dying[i]);}catch(e){}}
|
||||||
|
setScore();this.dying=[];}
|
||||||
|
for(var i=this.particles.length-1;i>=0;i--){this.particles[i].pos.add(this.particles[i].dir.mulNew(particleSpeed*tDelta*Math.random()));if(nowTime-this.particles[i].cameAlive>1000){this.particles.splice(i,1);forceChange=true;continue;}}
|
||||||
|
if(isIEQuirks){this.gameContainer.style.left=this.canvas.style.left=document.documentElement.scrollLeft+"px";this.gameContainer.style.top=this.canvas.style.top=document.documentElement.scrollTop+"px";this.navigation.style.right="10px";this.navigation.style.top=document.documentElement.scrollTop+document.body.clientHeight-this.navigation.clientHeight-10+"px";}
|
||||||
|
if(forceChange||this.bullets.length!=0||this.particles.length!=0||!this.pos.is(this.lastPos)||this.vel.len()>0){this.ctx.clear();this.ctx.drawPlayer();if(drawFlame)
|
||||||
|
this.ctx.drawFlames(that.flame);if(this.bullets.length){this.ctx.drawBullets(this.bullets);}
|
||||||
|
if(this.particles.length){this.ctx.drawParticles(this.particles);}}
|
||||||
|
this.lastPos=this.pos;}
|
||||||
|
var updateFunc=function(){try{that.update.call(that);}
|
||||||
|
catch(e){clearInterval(interval);throw e;}};var interval=setInterval(updateFunc,1000/FPS);function destroy(){removeEvent(document,'keydown',eventKeydown);removeEvent(document,'keypress',eventKeypress);removeEvent(document,'keyup',eventKeyup);removeEvent(window,'resize',eventResize);isRunning=false;removeStylesheet("ASTEROIDSYEAHSTYLES");removeClass(document.body,'ASTEROIDSYEAH');this.gameContainer.parentNode.removeChild(this.gameContainer);};}
|
||||||
|
if(!window.ASTEROIDSPLAYERS)
|
||||||
|
window.ASTEROIDSPLAYERS=[];if(window.ActiveXObject&&!document.createElement('canvas').getContext){try{var xamlScript=document.createElement('script');xamlScript.setAttribute('type','text/xaml');xamlScript.textContent='<?xml version="1.0"?><Canvas xmlns="http://schemas.microsoft.com/client/2007"></Canvas>';document.getElementsByTagName('head')[0].appendChild(xamlScript);}catch(e){}
|
||||||
|
var script=document.createElement("script");script.setAttribute('type','text/javascript');script.onreadystatechange=function(){if(script.readyState=='loaded'||script.readyState=='complete'){if(typeof G_vmlCanvasManager!="undefined")
|
||||||
|
window.ASTEROIDSPLAYERS[window.ASTEROIDSPLAYERS.length]=new Asteroids();}};script.src="http://erkie.github.com/excanvas.js";document.getElementsByTagName('head')[0].appendChild(script);}
|
||||||
|
else window.ASTEROIDSPLAYERS[window.ASTEROIDSPLAYERS.length]=new Asteroids();})();
|
||||||
@@ -1,37 +1,41 @@
|
|||||||
|
$(document).ready(function() {
|
||||||
|
clearSelectionLabel = '(no selection)';
|
||||||
|
|
||||||
function changeSelectedValue(obj,pk,text,update_url) { //Pass in JQuery object and new parameters
|
function changeSelectedValue(obj,pk,text,update_url) { //Pass in JQuery object and new parameters
|
||||||
//console.log('Changing selected value');
|
//console.log('Changing selected value');
|
||||||
obj.find('option').remove(); //Remove all the available options
|
obj.find('option').remove(); //Remove all the available options
|
||||||
obj.append( //Add the new option
|
obj.append( //Add the new option
|
||||||
$("<option></option>")
|
$("<option></option>")
|
||||||
.attr("value",pk)
|
.attr("value",pk)
|
||||||
.text(text)
|
.text(text)
|
||||||
.data('update_url',update_url)
|
.data('update_url',update_url)
|
||||||
);
|
);
|
||||||
obj.selectpicker('render'); //Re-render the UI
|
obj.selectpicker('render'); //Re-render the UI
|
||||||
obj.selectpicker('refresh'); //Re-render the UI
|
obj.selectpicker('refresh'); //Re-render the UI
|
||||||
obj.selectpicker('val', pk); //Set the new value to be selected
|
obj.selectpicker('val', pk); //Set the new value to be selected
|
||||||
obj.change(); //Trigger the change function manually
|
obj.change(); //Trigger the change function manually
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshUpdateHref(obj) {
|
function refreshUpdateHref(obj) {
|
||||||
//console.log('Refreshing Update URL');
|
//console.log('Refreshing Update URL');
|
||||||
targetObject = $('#'+obj.attr('id')+'-update');
|
targetObject = $('#'+obj.attr('id')+'-update');
|
||||||
update_url = $('option:selected', obj).data('update_url');
|
update_url = $('option:selected', obj).data('update_url');
|
||||||
|
|
||||||
if (update_url=="") { //Probably "clear selection" has been chosen
|
if (update_url=="") { //Probably "clear selection" has been chosen
|
||||||
//console.log('Trying to disable');
|
// console.log('Trying to disable');
|
||||||
targetObject.removeAttr('href');
|
targetObject.attr('disabled', true);
|
||||||
targetObject.addClass('disabled');
|
} else {
|
||||||
} else {
|
targetObject.attr('href', update_url);
|
||||||
targetObject.prop('href', update_url);
|
targetObject.attr('disabled', false);
|
||||||
targetObject.removeClass('disabled');
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initPicker(obj) {
|
|
||||||
|
$(".selectpicker").each(function() {
|
||||||
|
|
||||||
var options = {
|
var options = {
|
||||||
ajax: {
|
ajax: {
|
||||||
url: obj.data('sourceurl'),
|
url: $(this).data('sourceurl'),
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
// Use "{{{q}}}" as a placeholder and Ajax Bootstrap Select will
|
// Use "{{{q}}}" as a placeholder and Ajax Bootstrap Select will
|
||||||
@@ -72,31 +76,29 @@ function initPicker(obj) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
obj.prepend($("<option></option>")
|
$(this).prepend($("<option></option>")
|
||||||
.attr("value",'')
|
.attr("value",'')
|
||||||
.text(clearSelectionLabel)
|
.text(clearSelectionLabel)
|
||||||
.data('update_url','')); //Add "clear selection" option
|
.data('update_url','')); //Add "clear selection" option
|
||||||
|
|
||||||
|
|
||||||
obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker
|
$(this).selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker
|
||||||
obj.change(function(){ //on change, update the edit button href
|
|
||||||
//console.log('Selectbox Changed');
|
$(this).change(function(){ //on change, update the edit button href
|
||||||
refreshUpdateHref(obj);
|
// console.log('Selectbox Changed');
|
||||||
|
refreshUpdateHref($(this));
|
||||||
});
|
});
|
||||||
|
|
||||||
refreshUpdateHref(obj); //Ensure href is correct at the beginning
|
refreshUpdateHref($(this)); //Ensure href is correct at the beginning
|
||||||
}
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
});
|
||||||
clearSelectionLabel = '(no selection)';
|
|
||||||
|
|
||||||
$(".selectpicker").each(function(){initPicker($(this))});
|
//When update/edit modal box submitted
|
||||||
|
|
||||||
//When update/edit modal box submitted
|
|
||||||
$('#modal').on('hide.bs.modal', function (e) {
|
$('#modal').on('hide.bs.modal', function (e) {
|
||||||
if (modaltarget != undefined && modalobject != "") {
|
if (modaltarget != undefined && modalobject != "") {
|
||||||
//Update the selector with new values
|
//Update the selector with new values
|
||||||
changeSelectedValue($(modaltarget),modalobject[0]['pk'],modalobject[0]['fields']['name'],modalobject[0]['update_url']);
|
changeSelectedValue($(modaltarget),modalobject[0]['pk'],modalobject[0]['fields']['name'],modalobject[0]['update_url']);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
8
RIGS/static/js/bootstrap-datetimepicker.min.js
vendored
Normal file
1846
RIGS/static/js/bootstrap-select.js
vendored
Executable file
125
RIGS/static/js/button.js
Executable file
@@ -0,0 +1,125 @@
|
|||||||
|
/* ========================================================================
|
||||||
|
* Bootstrap: button.js v3.3.7
|
||||||
|
* http://getbootstrap.com/javascript/#buttons
|
||||||
|
* ========================================================================
|
||||||
|
* Copyright 2011-2016 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* ======================================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
+function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// BUTTON PUBLIC CLASS DEFINITION
|
||||||
|
// ==============================
|
||||||
|
|
||||||
|
var Button = function (element, options) {
|
||||||
|
this.$element = $(element)
|
||||||
|
this.options = $.extend({}, Button.DEFAULTS, options)
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Button.VERSION = '3.3.7'
|
||||||
|
|
||||||
|
Button.DEFAULTS = {
|
||||||
|
loadingText: 'loading...'
|
||||||
|
}
|
||||||
|
|
||||||
|
Button.prototype.setState = function (state) {
|
||||||
|
var d = 'disabled'
|
||||||
|
var $el = this.$element
|
||||||
|
var val = $el.is('input') ? 'val' : 'html'
|
||||||
|
var data = $el.data()
|
||||||
|
|
||||||
|
state += 'Text'
|
||||||
|
|
||||||
|
if (data.resetText == null) $el.data('resetText', $el[val]())
|
||||||
|
|
||||||
|
// push to event loop to allow forms to submit
|
||||||
|
setTimeout($.proxy(function () {
|
||||||
|
$el[val](data[state] == null ? this.options[state] : data[state])
|
||||||
|
|
||||||
|
if (state == 'loadingText') {
|
||||||
|
this.isLoading = true
|
||||||
|
$el.addClass(d).attr(d, d).prop(d, true)
|
||||||
|
} else if (this.isLoading) {
|
||||||
|
this.isLoading = false
|
||||||
|
$el.removeClass(d).removeAttr(d).prop(d, false)
|
||||||
|
}
|
||||||
|
}, this), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button.prototype.toggle = function () {
|
||||||
|
var changed = true
|
||||||
|
var $parent = this.$element.closest('[data-toggle="buttons"]')
|
||||||
|
|
||||||
|
if ($parent.length) {
|
||||||
|
var $input = this.$element.find('input')
|
||||||
|
if ($input.prop('type') == 'radio') {
|
||||||
|
if ($input.prop('checked')) changed = false
|
||||||
|
$parent.find('.active').removeClass('active')
|
||||||
|
this.$element.addClass('active')
|
||||||
|
} else if ($input.prop('type') == 'checkbox') {
|
||||||
|
if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false
|
||||||
|
this.$element.toggleClass('active')
|
||||||
|
}
|
||||||
|
$input.prop('checked', this.$element.hasClass('active'))
|
||||||
|
if (changed) $input.trigger('change')
|
||||||
|
} else {
|
||||||
|
this.$element.attr('aria-pressed', !this.$element.hasClass('active'))
|
||||||
|
this.$element.toggleClass('active')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// BUTTON PLUGIN DEFINITION
|
||||||
|
// ========================
|
||||||
|
|
||||||
|
function Plugin(option) {
|
||||||
|
return this.each(function () {
|
||||||
|
var $this = $(this)
|
||||||
|
var data = $this.data('bs.button')
|
||||||
|
var options = typeof option == 'object' && option
|
||||||
|
|
||||||
|
if (!data) $this.data('bs.button', (data = new Button(this, options)))
|
||||||
|
|
||||||
|
if (option == 'toggle') data.toggle()
|
||||||
|
else if (option) data.setState(option)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var old = $.fn.button
|
||||||
|
|
||||||
|
$.fn.button = Plugin
|
||||||
|
$.fn.button.Constructor = Button
|
||||||
|
|
||||||
|
|
||||||
|
// BUTTON NO CONFLICT
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
$.fn.button.noConflict = function () {
|
||||||
|
$.fn.button = old
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// BUTTON DATA-API
|
||||||
|
// ===============
|
||||||
|
|
||||||
|
$(document)
|
||||||
|
.on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) {
|
||||||
|
var $btn = $(e.target).closest('.btn')
|
||||||
|
Plugin.call($btn, 'toggle')
|
||||||
|
if (!($(e.target).is('input[type="radio"], input[type="checkbox"]'))) {
|
||||||
|
// Prevent double click on radios, and the double selections (so cancellation) on checkboxes
|
||||||
|
e.preventDefault()
|
||||||
|
// The target component still receive the focus
|
||||||
|
if ($btn.is('input,button')) $btn.trigger('focus')
|
||||||
|
else $btn.find('input:visible,button:visible').first().trigger('focus')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) {
|
||||||
|
$(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type))
|
||||||
|
})
|
||||||
|
|
||||||
|
}(jQuery);
|
||||||
237
RIGS/static/js/carousel.js
Executable file
@@ -0,0 +1,237 @@
|
|||||||
|
/* ========================================================================
|
||||||
|
* Bootstrap: carousel.js v3.3.7
|
||||||
|
* http://getbootstrap.com/javascript/#carousel
|
||||||
|
* ========================================================================
|
||||||
|
* Copyright 2011-2016 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* ======================================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
+function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// CAROUSEL CLASS DEFINITION
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
var Carousel = function (element, options) {
|
||||||
|
this.$element = $(element)
|
||||||
|
this.$indicators = this.$element.find('.carousel-indicators')
|
||||||
|
this.options = options
|
||||||
|
this.paused = null
|
||||||
|
this.sliding = null
|
||||||
|
this.interval = null
|
||||||
|
this.$active = null
|
||||||
|
this.$items = null
|
||||||
|
|
||||||
|
this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this))
|
||||||
|
|
||||||
|
this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element
|
||||||
|
.on('mouseenter.bs.carousel', $.proxy(this.pause, this))
|
||||||
|
.on('mouseleave.bs.carousel', $.proxy(this.cycle, this))
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.VERSION = '3.3.7'
|
||||||
|
|
||||||
|
Carousel.TRANSITION_DURATION = 600
|
||||||
|
|
||||||
|
Carousel.DEFAULTS = {
|
||||||
|
interval: 5000,
|
||||||
|
pause: 'hover',
|
||||||
|
wrap: true,
|
||||||
|
keyboard: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.keydown = function (e) {
|
||||||
|
if (/input|textarea/i.test(e.target.tagName)) return
|
||||||
|
switch (e.which) {
|
||||||
|
case 37: this.prev(); break
|
||||||
|
case 39: this.next(); break
|
||||||
|
default: return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.cycle = function (e) {
|
||||||
|
e || (this.paused = false)
|
||||||
|
|
||||||
|
this.interval && clearInterval(this.interval)
|
||||||
|
|
||||||
|
this.options.interval
|
||||||
|
&& !this.paused
|
||||||
|
&& (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.getItemIndex = function (item) {
|
||||||
|
this.$items = item.parent().children('.item')
|
||||||
|
return this.$items.index(item || this.$active)
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.getItemForDirection = function (direction, active) {
|
||||||
|
var activeIndex = this.getItemIndex(active)
|
||||||
|
var willWrap = (direction == 'prev' && activeIndex === 0)
|
||||||
|
|| (direction == 'next' && activeIndex == (this.$items.length - 1))
|
||||||
|
if (willWrap && !this.options.wrap) return active
|
||||||
|
var delta = direction == 'prev' ? -1 : 1
|
||||||
|
var itemIndex = (activeIndex + delta) % this.$items.length
|
||||||
|
return this.$items.eq(itemIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.to = function (pos) {
|
||||||
|
var that = this
|
||||||
|
var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active'))
|
||||||
|
|
||||||
|
if (pos > (this.$items.length - 1) || pos < 0) return
|
||||||
|
|
||||||
|
if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid"
|
||||||
|
if (activeIndex == pos) return this.pause().cycle()
|
||||||
|
|
||||||
|
return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos))
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.pause = function (e) {
|
||||||
|
e || (this.paused = true)
|
||||||
|
|
||||||
|
if (this.$element.find('.next, .prev').length && $.support.transition) {
|
||||||
|
this.$element.trigger($.support.transition.end)
|
||||||
|
this.cycle(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.interval = clearInterval(this.interval)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.next = function () {
|
||||||
|
if (this.sliding) return
|
||||||
|
return this.slide('next')
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.prev = function () {
|
||||||
|
if (this.sliding) return
|
||||||
|
return this.slide('prev')
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.slide = function (type, next) {
|
||||||
|
var $active = this.$element.find('.item.active')
|
||||||
|
var $next = next || this.getItemForDirection(type, $active)
|
||||||
|
var isCycling = this.interval
|
||||||
|
var direction = type == 'next' ? 'left' : 'right'
|
||||||
|
var that = this
|
||||||
|
|
||||||
|
if ($next.hasClass('active')) return (this.sliding = false)
|
||||||
|
|
||||||
|
var relatedTarget = $next[0]
|
||||||
|
var slideEvent = $.Event('slide.bs.carousel', {
|
||||||
|
relatedTarget: relatedTarget,
|
||||||
|
direction: direction
|
||||||
|
})
|
||||||
|
this.$element.trigger(slideEvent)
|
||||||
|
if (slideEvent.isDefaultPrevented()) return
|
||||||
|
|
||||||
|
this.sliding = true
|
||||||
|
|
||||||
|
isCycling && this.pause()
|
||||||
|
|
||||||
|
if (this.$indicators.length) {
|
||||||
|
this.$indicators.find('.active').removeClass('active')
|
||||||
|
var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)])
|
||||||
|
$nextIndicator && $nextIndicator.addClass('active')
|
||||||
|
}
|
||||||
|
|
||||||
|
var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid"
|
||||||
|
if ($.support.transition && this.$element.hasClass('slide')) {
|
||||||
|
$next.addClass(type)
|
||||||
|
$next[0].offsetWidth // force reflow
|
||||||
|
$active.addClass(direction)
|
||||||
|
$next.addClass(direction)
|
||||||
|
$active
|
||||||
|
.one('bsTransitionEnd', function () {
|
||||||
|
$next.removeClass([type, direction].join(' ')).addClass('active')
|
||||||
|
$active.removeClass(['active', direction].join(' '))
|
||||||
|
that.sliding = false
|
||||||
|
setTimeout(function () {
|
||||||
|
that.$element.trigger(slidEvent)
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
.emulateTransitionEnd(Carousel.TRANSITION_DURATION)
|
||||||
|
} else {
|
||||||
|
$active.removeClass('active')
|
||||||
|
$next.addClass('active')
|
||||||
|
this.sliding = false
|
||||||
|
this.$element.trigger(slidEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
isCycling && this.cycle()
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CAROUSEL PLUGIN DEFINITION
|
||||||
|
// ==========================
|
||||||
|
|
||||||
|
function Plugin(option) {
|
||||||
|
return this.each(function () {
|
||||||
|
var $this = $(this)
|
||||||
|
var data = $this.data('bs.carousel')
|
||||||
|
var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
||||||
|
var action = typeof option == 'string' ? option : options.slide
|
||||||
|
|
||||||
|
if (!data) $this.data('bs.carousel', (data = new Carousel(this, options)))
|
||||||
|
if (typeof option == 'number') data.to(option)
|
||||||
|
else if (action) data[action]()
|
||||||
|
else if (options.interval) data.pause().cycle()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var old = $.fn.carousel
|
||||||
|
|
||||||
|
$.fn.carousel = Plugin
|
||||||
|
$.fn.carousel.Constructor = Carousel
|
||||||
|
|
||||||
|
|
||||||
|
// CAROUSEL NO CONFLICT
|
||||||
|
// ====================
|
||||||
|
|
||||||
|
$.fn.carousel.noConflict = function () {
|
||||||
|
$.fn.carousel = old
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CAROUSEL DATA-API
|
||||||
|
// =================
|
||||||
|
|
||||||
|
var clickHandler = function (e) {
|
||||||
|
var href
|
||||||
|
var $this = $(this)
|
||||||
|
var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7
|
||||||
|
if (!$target.hasClass('carousel')) return
|
||||||
|
var options = $.extend({}, $target.data(), $this.data())
|
||||||
|
var slideIndex = $this.attr('data-slide-to')
|
||||||
|
if (slideIndex) options.interval = false
|
||||||
|
|
||||||
|
Plugin.call($target, options)
|
||||||
|
|
||||||
|
if (slideIndex) {
|
||||||
|
$target.data('bs.carousel').to(slideIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document)
|
||||||
|
.on('click.bs.carousel.data-api', '[data-slide]', clickHandler)
|
||||||
|
.on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler)
|
||||||
|
|
||||||
|
$(window).on('load', function () {
|
||||||
|
$('[data-ride="carousel"]').each(function () {
|
||||||
|
var $carousel = $(this)
|
||||||
|
Plugin.call($carousel, $carousel.data())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}(jQuery);
|
||||||
212
RIGS/static/js/collapse.js
Executable file
@@ -0,0 +1,212 @@
|
|||||||
|
/* ========================================================================
|
||||||
|
* Bootstrap: collapse.js v3.3.7
|
||||||
|
* http://getbootstrap.com/javascript/#collapse
|
||||||
|
* ========================================================================
|
||||||
|
* Copyright 2011-2016 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* ======================================================================== */
|
||||||
|
|
||||||
|
/* jshint latedef: false */
|
||||||
|
|
||||||
|
+function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// COLLAPSE PUBLIC CLASS DEFINITION
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
var Collapse = function (element, options) {
|
||||||
|
this.$element = $(element)
|
||||||
|
this.options = $.extend({}, Collapse.DEFAULTS, options)
|
||||||
|
this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' +
|
||||||
|
'[data-toggle="collapse"][data-target="#' + element.id + '"]')
|
||||||
|
this.transitioning = null
|
||||||
|
|
||||||
|
if (this.options.parent) {
|
||||||
|
this.$parent = this.getParent()
|
||||||
|
} else {
|
||||||
|
this.addAriaAndCollapsedClass(this.$element, this.$trigger)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.toggle) this.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.VERSION = '3.3.7'
|
||||||
|
|
||||||
|
Collapse.TRANSITION_DURATION = 350
|
||||||
|
|
||||||
|
Collapse.DEFAULTS = {
|
||||||
|
toggle: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.prototype.dimension = function () {
|
||||||
|
var hasWidth = this.$element.hasClass('width')
|
||||||
|
return hasWidth ? 'width' : 'height'
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.prototype.show = function () {
|
||||||
|
if (this.transitioning || this.$element.hasClass('in')) return
|
||||||
|
|
||||||
|
var activesData
|
||||||
|
var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing')
|
||||||
|
|
||||||
|
if (actives && actives.length) {
|
||||||
|
activesData = actives.data('bs.collapse')
|
||||||
|
if (activesData && activesData.transitioning) return
|
||||||
|
}
|
||||||
|
|
||||||
|
var startEvent = $.Event('show.bs.collapse')
|
||||||
|
this.$element.trigger(startEvent)
|
||||||
|
if (startEvent.isDefaultPrevented()) return
|
||||||
|
|
||||||
|
if (actives && actives.length) {
|
||||||
|
Plugin.call(actives, 'hide')
|
||||||
|
activesData || actives.data('bs.collapse', null)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dimension = this.dimension()
|
||||||
|
|
||||||
|
this.$element
|
||||||
|
.removeClass('collapse')
|
||||||
|
.addClass('collapsing')[dimension](0)
|
||||||
|
.attr('aria-expanded', true)
|
||||||
|
|
||||||
|
this.$trigger
|
||||||
|
.removeClass('collapsed')
|
||||||
|
.attr('aria-expanded', true)
|
||||||
|
|
||||||
|
this.transitioning = 1
|
||||||
|
|
||||||
|
var complete = function () {
|
||||||
|
this.$element
|
||||||
|
.removeClass('collapsing')
|
||||||
|
.addClass('collapse in')[dimension]('')
|
||||||
|
this.transitioning = 0
|
||||||
|
this.$element
|
||||||
|
.trigger('shown.bs.collapse')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$.support.transition) return complete.call(this)
|
||||||
|
|
||||||
|
var scrollSize = $.camelCase(['scroll', dimension].join('-'))
|
||||||
|
|
||||||
|
this.$element
|
||||||
|
.one('bsTransitionEnd', $.proxy(complete, this))
|
||||||
|
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize])
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.prototype.hide = function () {
|
||||||
|
if (this.transitioning || !this.$element.hasClass('in')) return
|
||||||
|
|
||||||
|
var startEvent = $.Event('hide.bs.collapse')
|
||||||
|
this.$element.trigger(startEvent)
|
||||||
|
if (startEvent.isDefaultPrevented()) return
|
||||||
|
|
||||||
|
var dimension = this.dimension()
|
||||||
|
|
||||||
|
this.$element[dimension](this.$element[dimension]())[0].offsetHeight
|
||||||
|
|
||||||
|
this.$element
|
||||||
|
.addClass('collapsing')
|
||||||
|
.removeClass('collapse in')
|
||||||
|
.attr('aria-expanded', false)
|
||||||
|
|
||||||
|
this.$trigger
|
||||||
|
.addClass('collapsed')
|
||||||
|
.attr('aria-expanded', false)
|
||||||
|
|
||||||
|
this.transitioning = 1
|
||||||
|
|
||||||
|
var complete = function () {
|
||||||
|
this.transitioning = 0
|
||||||
|
this.$element
|
||||||
|
.removeClass('collapsing')
|
||||||
|
.addClass('collapse')
|
||||||
|
.trigger('hidden.bs.collapse')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$.support.transition) return complete.call(this)
|
||||||
|
|
||||||
|
this.$element
|
||||||
|
[dimension](0)
|
||||||
|
.one('bsTransitionEnd', $.proxy(complete, this))
|
||||||
|
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.prototype.toggle = function () {
|
||||||
|
this[this.$element.hasClass('in') ? 'hide' : 'show']()
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.prototype.getParent = function () {
|
||||||
|
return $(this.options.parent)
|
||||||
|
.find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]')
|
||||||
|
.each($.proxy(function (i, element) {
|
||||||
|
var $element = $(element)
|
||||||
|
this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element)
|
||||||
|
}, this))
|
||||||
|
.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) {
|
||||||
|
var isOpen = $element.hasClass('in')
|
||||||
|
|
||||||
|
$element.attr('aria-expanded', isOpen)
|
||||||
|
$trigger
|
||||||
|
.toggleClass('collapsed', !isOpen)
|
||||||
|
.attr('aria-expanded', isOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetFromTrigger($trigger) {
|
||||||
|
var href
|
||||||
|
var target = $trigger.attr('data-target')
|
||||||
|
|| (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
|
||||||
|
|
||||||
|
return $(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// COLLAPSE PLUGIN DEFINITION
|
||||||
|
// ==========================
|
||||||
|
|
||||||
|
function Plugin(option) {
|
||||||
|
return this.each(function () {
|
||||||
|
var $this = $(this)
|
||||||
|
var data = $this.data('bs.collapse')
|
||||||
|
var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
||||||
|
|
||||||
|
if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false
|
||||||
|
if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
|
||||||
|
if (typeof option == 'string') data[option]()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var old = $.fn.collapse
|
||||||
|
|
||||||
|
$.fn.collapse = Plugin
|
||||||
|
$.fn.collapse.Constructor = Collapse
|
||||||
|
|
||||||
|
|
||||||
|
// COLLAPSE NO CONFLICT
|
||||||
|
// ====================
|
||||||
|
|
||||||
|
$.fn.collapse.noConflict = function () {
|
||||||
|
$.fn.collapse = old
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// COLLAPSE DATA-API
|
||||||
|
// =================
|
||||||
|
|
||||||
|
$(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) {
|
||||||
|
var $this = $(this)
|
||||||
|
|
||||||
|
if (!$this.attr('data-target')) e.preventDefault()
|
||||||
|
|
||||||
|
var $target = getTargetFromTrigger($this)
|
||||||
|
var data = $target.data('bs.collapse')
|
||||||
|
var option = data ? 'toggle' : $this.data()
|
||||||
|
|
||||||
|
Plugin.call($target, option)
|
||||||
|
})
|
||||||
|
|
||||||
|
}(jQuery);
|
||||||