diff --git a/.codeclimate.yml b/.codeclimate.yml
deleted file mode 100644
index d8fb239d..00000000
--- a/.codeclimate.yml
+++ /dev/null
@@ -1,32 +0,0 @@
----
-engines:
- csslint:
- enabled: true
- duplication:
- enabled: true
- config:
- languages:
- - ruby
- - javascript
- - python
- - php
- eslint:
- enabled: true
- fixme:
- enabled: true
- radon:
- enabled: true
- rubocop:
- enabled: true
-ratings:
- paths:
- - "**.css"
- - "**.inc"
- - "**.js"
- - "**.jsx"
- - "**.module"
- - "**.php"
- - "**.py"
- - "**.rb"
-exclude_paths:
-- config/
diff --git a/.coveragerc b/.coveragerc
index b369f80b..fa5feeb4 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,6 +1,5 @@
[run]
-source =
- ./
-
-omit =
- */migrations/*
+omit = */migrations/*
+ */tests/*
+ */site-packages/*
+ */distutils/*
diff --git a/.csslintrc b/.csslintrc
deleted file mode 100644
index aacba956..00000000
--- a/.csslintrc
+++ /dev/null
@@ -1,2 +0,0 @@
---exclude-exts=.min.css
---ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index 96212a35..00000000
--- a/.eslintignore
+++ /dev/null
@@ -1 +0,0 @@
-**/*{.,-}min.js
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 9faa3750..00000000
--- a/.eslintrc
+++ /dev/null
@@ -1,213 +0,0 @@
-ecmaFeatures:
- modules: true
- jsx: true
-
-env:
- amd: true
- browser: true
- es6: true
- jquery: true
- node: true
-
-# http://eslint.org/docs/rules/
-rules:
- # Possible Errors
- comma-dangle: [2, never]
- no-cond-assign: 2
- no-console: 0
- no-constant-condition: 2
- no-control-regex: 2
- no-debugger: 2
- no-dupe-args: 2
- no-dupe-keys: 2
- no-duplicate-case: 2
- no-empty: 2
- no-empty-character-class: 2
- no-ex-assign: 2
- no-extra-boolean-cast: 2
- no-extra-parens: 0
- no-extra-semi: 2
- no-func-assign: 2
- no-inner-declarations: [2, functions]
- no-invalid-regexp: 2
- no-irregular-whitespace: 2
- no-negated-in-lhs: 2
- no-obj-calls: 2
- no-regex-spaces: 2
- no-sparse-arrays: 2
- no-unexpected-multiline: 2
- no-unreachable: 2
- use-isnan: 2
- valid-jsdoc: 0
- valid-typeof: 2
-
- # Best Practices
- accessor-pairs: 2
- block-scoped-var: 0
- complexity: [2, 6]
- consistent-return: 0
- curly: 0
- default-case: 0
- dot-location: 0
- dot-notation: 0
- eqeqeq: 2
- guard-for-in: 2
- no-alert: 2
- no-caller: 2
- no-case-declarations: 2
- no-div-regex: 2
- no-else-return: 0
- no-empty-label: 2
- no-empty-pattern: 2
- no-eq-null: 2
- no-eval: 2
- no-extend-native: 2
- no-extra-bind: 2
- no-fallthrough: 2
- no-floating-decimal: 0
- no-implicit-coercion: 0
- no-implied-eval: 2
- no-invalid-this: 0
- no-iterator: 2
- no-labels: 0
- no-lone-blocks: 2
- no-loop-func: 2
- no-magic-number: 0
- no-multi-spaces: 0
- no-multi-str: 0
- no-native-reassign: 2
- no-new-func: 2
- no-new-wrappers: 2
- no-new: 2
- no-octal-escape: 2
- no-octal: 2
- no-proto: 2
- no-redeclare: 2
- no-return-assign: 2
- no-script-url: 2
- no-self-compare: 2
- no-sequences: 0
- no-throw-literal: 0
- no-unused-expressions: 2
- no-useless-call: 2
- no-useless-concat: 2
- no-void: 2
- no-warning-comments: 0
- no-with: 2
- radix: 2
- vars-on-top: 0
- wrap-iife: 2
- yoda: 0
-
- # Strict
- strict: 0
-
- # Variables
- init-declarations: 0
- no-catch-shadow: 2
- no-delete-var: 2
- no-label-var: 2
- no-shadow-restricted-names: 2
- no-shadow: 0
- no-undef-init: 2
- no-undef: 0
- no-undefined: 0
- no-unused-vars: 0
- no-use-before-define: 0
-
- # Node.js and CommonJS
- callback-return: 2
- global-require: 2
- handle-callback-err: 2
- no-mixed-requires: 0
- no-new-require: 0
- no-path-concat: 2
- no-process-exit: 2
- no-restricted-modules: 0
- no-sync: 0
-
- # Stylistic Issues
- array-bracket-spacing: 0
- block-spacing: 0
- brace-style: 0
- camelcase: 0
- comma-spacing: 0
- comma-style: 0
- computed-property-spacing: 0
- consistent-this: 0
- eol-last: 0
- func-names: 0
- func-style: 0
- id-length: 0
- id-match: 0
- indent: 0
- jsx-quotes: 0
- key-spacing: 0
- linebreak-style: 0
- lines-around-comment: 0
- max-depth: 0
- max-len: 0
- max-nested-callbacks: 0
- max-params: 0
- max-statements: [2, 30]
- new-cap: 0
- new-parens: 0
- newline-after-var: 0
- no-array-constructor: 0
- no-bitwise: 0
- no-continue: 0
- no-inline-comments: 0
- no-lonely-if: 0
- no-mixed-spaces-and-tabs: 0
- no-multiple-empty-lines: 0
- no-negated-condition: 0
- no-nested-ternary: 0
- no-new-object: 0
- no-plusplus: 0
- no-restricted-syntax: 0
- no-spaced-func: 0
- no-ternary: 0
- no-trailing-spaces: 0
- no-underscore-dangle: 0
- no-unneeded-ternary: 0
- object-curly-spacing: 0
- one-var: 0
- operator-assignment: 0
- operator-linebreak: 0
- padded-blocks: 0
- quote-props: 0
- quotes: 0
- require-jsdoc: 0
- semi-spacing: 0
- semi: 0
- sort-vars: 0
- space-after-keywords: 0
- space-before-blocks: 0
- space-before-function-paren: 0
- space-before-keywords: 0
- space-in-parens: 0
- space-infix-ops: 0
- space-return-throw-case: 0
- space-unary-ops: 0
- spaced-comment: 0
- wrap-regex: 0
-
- # ECMAScript 6
- arrow-body-style: 0
- arrow-parens: 0
- arrow-spacing: 0
- constructor-super: 0
- generator-star-spacing: 0
- no-arrow-condition: 0
- no-class-assign: 0
- no-const-assign: 0
- no-dupe-class-members: 0
- no-this-before-super: 0
- no-var: 0
- object-shorthand: 0
- prefer-arrow-callback: 0
- prefer-const: 0
- prefer-reflect: 0
- prefer-spread: 0
- prefer-template: 0
- require-yield: 0
diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml
new file mode 100644
index 00000000..f698098f
--- /dev/null
+++ b/.github/workflows/django.yml
@@ -0,0 +1,58 @@
+name: Django CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ build:
+ if: "!contains(github.event.head_commit.message, '[ci skip]')"
+ runs-on: ubuntu-latest
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9.1
+ - uses: actions/cache@v2
+ id: pcache
+ with:
+ path: ~/.local/share/virtualenvs
+ key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-pipenv-
+ - name: Install Dependencies
+ run: |
+ python -m pip install --upgrade pip pipenv
+ pipenv install -d
+ # if: steps.pcache.outputs.cache-hit != 'true'
+ - name: Cache Static Files
+ id: static-cache
+ uses: actions/cache@v2
+ with:
+ path: 'pipeline/built_assets'
+ key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
+ - uses: bahmutov/npm-install@v1
+ if: steps.static-cache.outputs.cache-hit != 'true'
+ - run: node node_modules/gulp/bin/gulp build
+ if: steps.static-cache.outputs.cache-hit != 'true'
+ - name: Basic Checks
+ run: |
+ pipenv run pycodestyle . --exclude=migrations,node_modules
+ pipenv run python manage.py check
+ pipenv run python manage.py makemigrations --check --dry-run
+ pipenv run python manage.py collectstatic --noinput
+ - name: Run Tests
+ run: pipenv run pytest -n auto -vv --cov
+ - uses: actions/upload-artifact@v2
+ if: failure()
+ with:
+ name: failure-screenshots ${{ matrix.test-group }}
+ path: screenshots/
+ retention-days: 5
+ - name: Coveralls
+ run: pipenv run coveralls --service=github
diff --git a/.gitignore b/.gitignore
index 041dcbd3..79eef597 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,7 @@ var/
*.egg-info/
.installed.cfg
*.egg
+node_modules/
# Continer extras
.vagrant
@@ -54,7 +55,6 @@ coverage.xml
# Django stuff:
*.log
-db.sqlite3
# Sphinx documentation
docs/_build/
@@ -68,19 +68,9 @@ target/
## Directory-based project format:
.idea/
-# if you remove the above rule, at least ignore the following:
-# User-specific stuff:
-# .idea/workspace.xml
-# .idea/tasks.xml
-# .idea/dictionaries
-
-# Sensitive or high-churn files:
-# .idea/dataSources.ids
-# .idea/dataSources.xml
-# .idea/sqlDataSources.xml
-# .idea/dynamic.xml
-# .idea/uiDesigner.xml
+#Built dependencies
+pipeline/built_assets
# Gradle:
# .idea/gradle.xml
@@ -108,4 +98,5 @@ atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
-.vscode/
\ No newline at end of file
+.vscode/
+screenshots/
diff --git a/.idea/.name b/.idea/.name
deleted file mode 100644
index b822ae3d..00000000
--- a/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-PyRIGS
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
deleted file mode 100644
index e206d70d..00000000
--- a/.idea/encodings.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 75e2a525..00000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml
deleted file mode 100644
index 922003b8..00000000
--- a/.idea/scopes/scope_settings.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 275077f8..00000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/.rubocop.yml b/.rubocop.yml
deleted file mode 100644
index 3f1d2224..00000000
--- a/.rubocop.yml
+++ /dev/null
@@ -1,1156 +0,0 @@
-AllCops:
- DisabledByDefault: true
-
-#################### Lint ################################
-
-Lint/AmbiguousOperator:
- Description: >-
- Checks for ambiguous operators in the first argument of a
- method invocation without parentheses.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-as-args'
- Enabled: true
-
-Lint/AmbiguousRegexpLiteral:
- Description: >-
- Checks for ambiguous regexp literals in the first argument of
- a method invocation without parenthesis.
- Enabled: true
-
-Lint/AssignmentInCondition:
- Description: "Don't use assignment in conditions."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition'
- Enabled: true
-
-Lint/BlockAlignment:
- Description: 'Align block ends correctly.'
- Enabled: true
-
-Lint/CircularArgumentReference:
- Description: "Don't refer to the keyword argument in the default value."
- Enabled: true
-
-Lint/ConditionPosition:
- Description: >-
- Checks for condition placed in a confusing position relative to
- the keyword.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition'
- Enabled: true
-
-Lint/Debugger:
- Description: 'Check for debugger calls.'
- Enabled: true
-
-Lint/DefEndAlignment:
- Description: 'Align ends corresponding to defs correctly.'
- Enabled: true
-
-Lint/DeprecatedClassMethods:
- Description: 'Check for deprecated class method calls.'
- Enabled: true
-
-Lint/DuplicateMethods:
- Description: 'Check for duplicate methods calls.'
- Enabled: true
-
-Lint/EachWithObjectArgument:
- Description: 'Check for immutable argument given to each_with_object.'
- Enabled: true
-
-Lint/ElseLayout:
- Description: 'Check for odd code arrangement in an else block.'
- Enabled: true
-
-Lint/EmptyEnsure:
- Description: 'Checks for empty ensure block.'
- Enabled: true
-
-Lint/EmptyInterpolation:
- Description: 'Checks for empty string interpolation.'
- Enabled: true
-
-Lint/EndAlignment:
- Description: 'Align ends correctly.'
- Enabled: true
-
-Lint/EndInMethod:
- Description: 'END blocks should not be placed inside method definitions.'
- Enabled: true
-
-Lint/EnsureReturn:
- Description: 'Do not use return in an ensure block.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-return-ensure'
- Enabled: true
-
-Lint/Eval:
- Description: 'The use of eval represents a serious security risk.'
- Enabled: true
-
-Lint/FormatParameterMismatch:
- Description: 'The number of parameters to format/sprint must match the fields.'
- Enabled: true
-
-Lint/HandleExceptions:
- Description: "Don't suppress exception."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions'
- Enabled: true
-
-Lint/InvalidCharacterLiteral:
- Description: >-
- Checks for invalid character literals with a non-escaped
- whitespace character.
- Enabled: true
-
-Lint/LiteralInCondition:
- Description: 'Checks of literals used in conditions.'
- Enabled: true
-
-Lint/LiteralInInterpolation:
- Description: 'Checks for literals used in interpolation.'
- Enabled: true
-
-Lint/Loop:
- Description: >-
- Use Kernel#loop with break rather than begin/end/until or
- begin/end/while for post-loop tests.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break'
- Enabled: true
-
-Lint/NestedMethodDefinition:
- Description: 'Do not use nested method definitions.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-methods'
- Enabled: true
-
-Lint/NonLocalExitFromIterator:
- Description: 'Do not use return in iterator to cause non-local exit.'
- Enabled: true
-
-Lint/ParenthesesAsGroupedExpression:
- Description: >-
- Checks for method calls with a space before the opening
- parenthesis.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
- Enabled: true
-
-Lint/RequireParentheses:
- Description: >-
- Use parentheses in the method call to avoid confusion
- about precedence.
- Enabled: true
-
-Lint/RescueException:
- Description: 'Avoid rescuing the Exception class.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues'
- Enabled: true
-
-Lint/ShadowingOuterLocalVariable:
- Description: >-
- Do not use the same name as outer local variable
- for block arguments or block local variables.
- Enabled: true
-
-Lint/StringConversionInInterpolation:
- Description: 'Checks for Object#to_s usage in string interpolation.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s'
- Enabled: true
-
-Lint/UnderscorePrefixedVariableName:
- Description: 'Do not use prefix `_` for a variable that is used.'
- Enabled: true
-
-Lint/UnneededDisable:
- Description: >-
- Checks for rubocop:disable comments that can be removed.
- Note: this cop is not disabled when disabling all cops.
- It must be explicitly disabled.
- Enabled: true
-
-Lint/UnusedBlockArgument:
- Description: 'Checks for unused block arguments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
- Enabled: true
-
-Lint/UnusedMethodArgument:
- Description: 'Checks for unused method arguments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
- Enabled: true
-
-Lint/UnreachableCode:
- Description: 'Unreachable code.'
- Enabled: true
-
-Lint/UselessAccessModifier:
- Description: 'Checks for useless access modifiers.'
- Enabled: true
-
-Lint/UselessAssignment:
- Description: 'Checks for useless assignment to a local variable.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
- Enabled: true
-
-Lint/UselessComparison:
- Description: 'Checks for comparison of something with itself.'
- Enabled: true
-
-Lint/UselessElseWithoutRescue:
- Description: 'Checks for useless `else` in `begin..end` without `rescue`.'
- Enabled: true
-
-Lint/UselessSetterCall:
- Description: 'Checks for useless setter call to a local variable.'
- Enabled: true
-
-Lint/Void:
- Description: 'Possible use of operator/literal/variable in void context.'
- Enabled: true
-
-###################### Metrics ####################################
-
-Metrics/AbcSize:
- Description: >-
- A calculated magnitude based on number of assignments,
- branches, and conditions.
- Reference: 'http://c2.com/cgi/wiki?AbcMetric'
- Enabled: false
- Max: 20
-
-Metrics/BlockNesting:
- Description: 'Avoid excessive block nesting'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count'
- Enabled: true
- Max: 4
-
-Metrics/ClassLength:
- Description: 'Avoid classes longer than 250 lines of code.'
- Enabled: true
- Max: 250
-
-Metrics/CyclomaticComplexity:
- Description: >-
- A complexity metric that is strongly correlated to the number
- of test cases needed to validate a method.
- Enabled: true
-
-Metrics/LineLength:
- Description: 'Limit lines to 80 characters.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits'
- Enabled: false
-
-Metrics/MethodLength:
- Description: 'Avoid methods longer than 30 lines of code.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods'
- Enabled: true
- Max: 30
-
-Metrics/ModuleLength:
- Description: 'Avoid modules longer than 250 lines of code.'
- Enabled: true
- Max: 250
-
-Metrics/ParameterLists:
- Description: 'Avoid parameter lists longer than three or four parameters.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params'
- Enabled: true
-
-Metrics/PerceivedComplexity:
- Description: >-
- A complexity metric geared towards measuring complexity for a
- human reader.
- Enabled: false
-
-##################### Performance #############################
-
-Performance/Count:
- Description: >-
- Use `count` instead of `select...size`, `reject...size`,
- `select...count`, `reject...count`, `select...length`,
- and `reject...length`.
- Enabled: true
-
-Performance/Detect:
- Description: >-
- Use `detect` instead of `select.first`, `find_all.first`,
- `select.last`, and `find_all.last`.
- Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code'
- Enabled: true
-
-Performance/FlatMap:
- Description: >-
- Use `Enumerable#flat_map`
- instead of `Enumerable#map...Array#flatten(1)`
- or `Enumberable#collect..Array#flatten(1)`
- Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablemaparrayflatten-vs-enumerableflat_map-code'
- Enabled: true
- EnabledForFlattenWithoutParams: false
- # If enabled, this cop will warn about usages of
- # `flatten` being called without any parameters.
- # This can be dangerous since `flat_map` will only flatten 1 level, and
- # `flatten` without any parameters can flatten multiple levels.
-
-Performance/ReverseEach:
- Description: 'Use `reverse_each` instead of `reverse.each`.'
- Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code'
- Enabled: true
-
-Performance/Sample:
- Description: >-
- Use `sample` instead of `shuffle.first`,
- `shuffle.last`, and `shuffle[Fixnum]`.
- Reference: 'https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code'
- Enabled: true
-
-Performance/Size:
- Description: >-
- Use `size` instead of `count` for counting
- the number of elements in `Array` and `Hash`.
- Reference: 'https://github.com/JuanitoFatas/fast-ruby#arraycount-vs-arraysize-code'
- Enabled: true
-
-Performance/StringReplacement:
- Description: >-
- Use `tr` instead of `gsub` when you are replacing the same
- number of characters. Use `delete` instead of `gsub` when
- you are deleting characters.
- Reference: 'https://github.com/JuanitoFatas/fast-ruby#stringgsub-vs-stringtr-code'
- Enabled: true
-
-##################### Rails ##################################
-
-Rails/ActionFilter:
- Description: 'Enforces consistent use of action filter methods.'
- Enabled: false
-
-Rails/Date:
- Description: >-
- Checks the correct usage of date aware methods,
- such as Date.today, Date.current etc.
- Enabled: false
-
-Rails/Delegate:
- Description: 'Prefer delegate method for delegations.'
- Enabled: false
-
-Rails/FindBy:
- Description: 'Prefer find_by over where.first.'
- Enabled: false
-
-Rails/FindEach:
- Description: 'Prefer all.find_each over all.find.'
- Enabled: false
-
-Rails/HasAndBelongsToMany:
- Description: 'Prefer has_many :through to has_and_belongs_to_many.'
- Enabled: false
-
-Rails/Output:
- Description: 'Checks for calls to puts, print, etc.'
- Enabled: false
-
-Rails/ReadWriteAttribute:
- Description: >-
- Checks for read_attribute(:attr) and
- write_attribute(:attr, val).
- Enabled: false
-
-Rails/ScopeArgs:
- Description: 'Checks the arguments of ActiveRecord scopes.'
- Enabled: false
-
-Rails/TimeZone:
- Description: 'Checks the correct usage of time zone aware methods.'
- StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time'
- Reference: 'http://danilenko.org/2012/7/6/rails_timezones'
- Enabled: false
-
-Rails/Validation:
- Description: 'Use validates :attribute, hash of validations.'
- Enabled: false
-
-################## Style #################################
-
-Style/AccessModifierIndentation:
- Description: Check indentation of private/protected visibility modifiers.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-public-private-protected'
- Enabled: false
-
-Style/AccessorMethodName:
- Description: Check the naming of accessor methods for get_/set_.
- Enabled: false
-
-Style/Alias:
- Description: 'Use alias_method instead of alias.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method'
- Enabled: false
-
-Style/AlignArray:
- Description: >-
- Align the elements of an array literal if they span more than
- one line.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays'
- Enabled: false
-
-Style/AlignHash:
- Description: >-
- Align the elements of a hash literal if they span more than
- one line.
- Enabled: false
-
-Style/AlignParameters:
- Description: >-
- Align the parameters of a method call if they span more
- than one line.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent'
- Enabled: false
-
-Style/AndOr:
- Description: 'Use &&/|| instead of and/or.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-and-or-or'
- Enabled: false
-
-Style/ArrayJoin:
- Description: 'Use Array#join instead of Array#*.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join'
- Enabled: false
-
-Style/AsciiComments:
- Description: 'Use only ascii symbols in comments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments'
- Enabled: false
-
-Style/AsciiIdentifiers:
- Description: 'Use only ascii symbols in identifiers.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers'
- Enabled: false
-
-Style/Attr:
- Description: 'Checks for uses of Module#attr.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr'
- Enabled: false
-
-Style/BeginBlock:
- Description: 'Avoid the use of BEGIN blocks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks'
- Enabled: false
-
-Style/BarePercentLiterals:
- Description: 'Checks if usage of %() or %Q() matches configuration.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q-shorthand'
- Enabled: false
-
-Style/BlockComments:
- Description: 'Do not use block comments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-block-comments'
- Enabled: false
-
-Style/BlockEndNewline:
- Description: 'Put end statement of multiline block on its own line.'
- Enabled: false
-
-Style/BlockDelimiters:
- Description: >-
- Avoid using {...} for multi-line blocks (multiline chaining is
- always ugly).
- Prefer {...} over do...end for single-line blocks.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
- Enabled: false
-
-Style/BracesAroundHashParameters:
- Description: 'Enforce braces style around hash parameters.'
- Enabled: false
-
-Style/CaseEquality:
- Description: 'Avoid explicit use of the case equality operator(===).'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality'
- Enabled: false
-
-Style/CaseIndentation:
- Description: 'Indentation of when in a case/when/[else/]end.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-when-to-case'
- Enabled: false
-
-Style/CharacterLiteral:
- Description: 'Checks for uses of character literals.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-character-literals'
- Enabled: false
-
-Style/ClassAndModuleCamelCase:
- Description: 'Use CamelCase for classes and modules.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#camelcase-classes'
- Enabled: false
-
-Style/ClassAndModuleChildren:
- Description: 'Checks style of children classes and modules.'
- Enabled: false
-
-Style/ClassCheck:
- Description: 'Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.'
- Enabled: false
-
-Style/ClassMethods:
- Description: 'Use self when defining module/class methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#def-self-class-methods'
- Enabled: false
-
-Style/ClassVars:
- Description: 'Avoid the use of class variables.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-class-vars'
- Enabled: false
-
-Style/ClosingParenthesisIndentation:
- Description: 'Checks the indentation of hanging closing parentheses.'
- Enabled: false
-
-Style/ColonMethodCall:
- Description: 'Do not use :: for method call.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons'
- Enabled: false
-
-Style/CommandLiteral:
- Description: 'Use `` or %x around command literals.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-x'
- Enabled: false
-
-Style/CommentAnnotation:
- Description: 'Checks formatting of annotation comments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords'
- Enabled: false
-
-Style/CommentIndentation:
- Description: 'Indentation of comments.'
- Enabled: false
-
-Style/ConstantName:
- Description: 'Constants should use SCREAMING_SNAKE_CASE.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#screaming-snake-case'
- Enabled: false
-
-Style/DefWithParentheses:
- Description: 'Use def with parentheses when there are arguments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
- Enabled: false
-
-Style/DeprecatedHashMethods:
- Description: 'Checks for use of deprecated Hash methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-key'
- Enabled: false
-
-Style/Documentation:
- Description: 'Document classes and non-namespace modules.'
- Enabled: false
-
-Style/DotPosition:
- Description: 'Checks the position of the dot in multi-line method calls.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains'
- Enabled: false
-
-Style/DoubleNegation:
- Description: 'Checks for uses of double negation (!!).'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang'
- Enabled: false
-
-Style/EachWithObject:
- Description: 'Prefer `each_with_object` over `inject` or `reduce`.'
- Enabled: false
-
-Style/ElseAlignment:
- Description: 'Align elses and elsifs correctly.'
- Enabled: false
-
-Style/EmptyElse:
- Description: 'Avoid empty else-clauses.'
- Enabled: false
-
-Style/EmptyLineBetweenDefs:
- Description: 'Use empty lines between defs.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods'
- Enabled: false
-
-Style/EmptyLines:
- Description: "Don't use several empty lines in a row."
- Enabled: false
-
-Style/EmptyLinesAroundAccessModifier:
- Description: "Keep blank lines around access modifiers."
- Enabled: false
-
-Style/EmptyLinesAroundBlockBody:
- Description: "Keeps track of empty lines around block bodies."
- Enabled: false
-
-Style/EmptyLinesAroundClassBody:
- Description: "Keeps track of empty lines around class bodies."
- Enabled: false
-
-Style/EmptyLinesAroundModuleBody:
- Description: "Keeps track of empty lines around module bodies."
- Enabled: false
-
-Style/EmptyLinesAroundMethodBody:
- Description: "Keeps track of empty lines around method bodies."
- Enabled: false
-
-Style/EmptyLiteral:
- Description: 'Prefer literals to Array.new/Hash.new/String.new.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash'
- Enabled: false
-
-Style/EndBlock:
- Description: 'Avoid the use of END blocks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks'
- Enabled: false
-
-Style/EndOfLine:
- Description: 'Use Unix-style line endings.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf'
- Enabled: false
-
-Style/EvenOdd:
- Description: 'Favor the use of Fixnum#even? && Fixnum#odd?'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
- Enabled: false
-
-Style/ExtraSpacing:
- Description: 'Do not use unnecessary spacing.'
- Enabled: false
-
-Style/FileName:
- Description: 'Use snake_case for source file names.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files'
- Enabled: false
-
-Style/InitialIndentation:
- Description: >-
- Checks the indentation of the first non-blank non-comment line in a file.
- Enabled: false
-
-Style/FirstParameterIndentation:
- Description: 'Checks the indentation of the first parameter in a method call.'
- Enabled: false
-
-Style/FlipFlop:
- Description: 'Checks for flip flops'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops'
- Enabled: false
-
-Style/For:
- Description: 'Checks use of for or each in multiline loops.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-for-loops'
- Enabled: false
-
-Style/FormatString:
- Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf'
- Enabled: false
-
-Style/GlobalVars:
- Description: 'Do not introduce global variables.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars'
- Reference: 'http://www.zenspider.com/Languages/Ruby/QuickRef.html'
- Enabled: false
-
-Style/GuardClause:
- Description: 'Check for conditionals that can be replaced with guard clauses'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals'
- Enabled: false
-
-Style/HashSyntax:
- Description: >-
- Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax
- { :a => 1, :b => 2 }.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-literals'
- Enabled: false
-
-Style/IfUnlessModifier:
- Description: >-
- Favor modifier if/unless usage when you have a
- single-line body.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier'
- Enabled: false
-
-Style/IfWithSemicolon:
- Description: 'Do not use if x; .... Use the ternary operator instead.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs'
- Enabled: false
-
-Style/IndentationConsistency:
- Description: 'Keep indentation straight.'
- Enabled: false
-
-Style/IndentationWidth:
- Description: 'Use 2 spaces for indentation.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
- Enabled: false
-
-Style/IndentArray:
- Description: >-
- Checks the indentation of the first element in an array
- literal.
- Enabled: false
-
-Style/IndentHash:
- Description: 'Checks the indentation of the first key in a hash literal.'
- Enabled: false
-
-Style/InfiniteLoop:
- Description: 'Use Kernel#loop for infinite loops.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#infinite-loop'
- Enabled: false
-
-Style/Lambda:
- Description: 'Use the new lambda literal syntax for single-line blocks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#lambda-multi-line'
- Enabled: false
-
-Style/LambdaCall:
- Description: 'Use lambda.call(...) instead of lambda.(...).'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc-call'
- Enabled: false
-
-Style/LeadingCommentSpace:
- Description: 'Comments should start with a space.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space'
- Enabled: false
-
-Style/LineEndConcatenation:
- Description: >-
- Use \ instead of + or << to concatenate two string literals at
- line end.
- Enabled: false
-
-Style/MethodCallParentheses:
- Description: 'Do not use parentheses for method calls with no arguments.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens'
- Enabled: false
-
-Style/MethodDefParentheses:
- Description: >-
- Checks if the method definitions have or don't have
- parentheses.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
- Enabled: false
-
-Style/MethodName:
- Description: 'Use the configured style when naming methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars'
- Enabled: false
-
-Style/ModuleFunction:
- Description: 'Checks for usage of `extend self` in modules.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#module-function'
- Enabled: false
-
-Style/MultilineBlockChain:
- Description: 'Avoid multi-line chains of blocks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
- Enabled: false
-
-Style/MultilineBlockLayout:
- Description: 'Ensures newlines after multiline block do statements.'
- Enabled: false
-
-Style/MultilineIfThen:
- Description: 'Do not use then for multi-line if/unless.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-then'
- Enabled: false
-
-Style/MultilineOperationIndentation:
- Description: >-
- Checks indentation of binary operations that span more than
- one line.
- Enabled: false
-
-Style/MultilineTernaryOperator:
- Description: >-
- Avoid multi-line ?: (the ternary operator);
- use if/unless instead.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-ternary'
- Enabled: false
-
-Style/NegatedIf:
- Description: >-
- Favor unless over if for negative conditions
- (or control flow or).
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives'
- Enabled: false
-
-Style/NegatedWhile:
- Description: 'Favor until over while for negative conditions.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#until-for-negatives'
- Enabled: false
-
-Style/NestedTernaryOperator:
- Description: 'Use one expression per branch in a ternary operator.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary'
- Enabled: false
-
-Style/Next:
- Description: 'Use `next` to skip iteration instead of a condition at the end.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals'
- Enabled: false
-
-Style/NilComparison:
- Description: 'Prefer x.nil? to x == nil.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
- Enabled: false
-
-Style/NonNilCheck:
- Description: 'Checks for redundant nil checks.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks'
- Enabled: false
-
-Style/Not:
- Description: 'Use ! instead of not.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not'
- Enabled: false
-
-Style/NumericLiterals:
- Description: >-
- Add underscores to large numeric literals to improve their
- readability.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics'
- Enabled: false
-
-Style/OneLineConditional:
- Description: >-
- Favor the ternary operator(?:) over
- if/then/else/end constructs.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator'
- Enabled: false
-
-Style/OpMethod:
- Description: 'When defining binary operators, name the argument other.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg'
- Enabled: false
-
-Style/OptionalArguments:
- Description: >-
- Checks for optional arguments that do not appear at the end
- of the argument list
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#optional-arguments'
- Enabled: false
-
-Style/ParallelAssignment:
- Description: >-
- Check for simple usages of parallel assignment.
- It will only warn when the number of variables
- matches on both sides of the assignment.
- This also provides performance benefits
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parallel-assignment'
- Enabled: false
-
-Style/ParenthesesAroundCondition:
- Description: >-
- Don't use parentheses around the condition of an
- if/unless/while.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if'
- Enabled: false
-
-Style/PercentLiteralDelimiters:
- Description: 'Use `%`-literal delimiters consistently'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces'
- Enabled: false
-
-Style/PercentQLiterals:
- Description: 'Checks if uses of %Q/%q match the configured preference.'
- Enabled: false
-
-Style/PerlBackrefs:
- Description: 'Avoid Perl-style regex back references.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers'
- Enabled: false
-
-Style/PredicateName:
- Description: 'Check the names of predicate methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark'
- Enabled: false
-
-Style/Proc:
- Description: 'Use proc instead of Proc.new.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc'
- Enabled: false
-
-Style/RaiseArgs:
- Description: 'Checks the arguments passed to raise/fail.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#exception-class-messages'
- Enabled: false
-
-Style/RedundantBegin:
- Description: "Don't use begin blocks when they are not needed."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#begin-implicit'
- Enabled: false
-
-Style/RedundantException:
- Description: "Checks for an obsolete RuntimeException argument in raise/fail."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-runtimeerror'
- Enabled: false
-
-Style/RedundantReturn:
- Description: "Don't use return where it's not required."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return'
- Enabled: false
-
-Style/RedundantSelf:
- Description: "Don't use self where it's not needed."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-self-unless-required'
- Enabled: false
-
-Style/RegexpLiteral:
- Description: 'Use / or %r around regular expressions.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r'
- Enabled: false
-
-Style/RescueEnsureAlignment:
- Description: 'Align rescues and ensures correctly.'
- Enabled: false
-
-Style/RescueModifier:
- Description: 'Avoid using rescue in its modifier form.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-rescue-modifiers'
- Enabled: false
-
-Style/SelfAssignment:
- Description: >-
- Checks for places where self-assignment shorthand should have
- been used.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#self-assignment'
- Enabled: false
-
-Style/Semicolon:
- Description: "Don't use semicolons to terminate expressions."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon'
- Enabled: false
-
-Style/SignalException:
- Description: 'Checks for proper usage of fail and raise.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#fail-method'
- Enabled: false
-
-Style/SingleLineBlockParams:
- Description: 'Enforces the names of some block params.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks'
- Enabled: false
-
-Style/SingleLineMethods:
- Description: 'Avoid single-line methods.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods'
- Enabled: false
-
-Style/SpaceBeforeFirstArg:
- Description: >-
- Checks that exactly one space is used between a method name
- and the first argument for method calls without parentheses.
- Enabled: true
-
-Style/SpaceAfterColon:
- Description: 'Use spaces after colons.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
- Enabled: false
-
-Style/SpaceAfterComma:
- Description: 'Use spaces after commas.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
- Enabled: false
-
-Style/SpaceAroundKeyword:
- Description: 'Use spaces around keywords.'
- Enabled: false
-
-Style/SpaceAfterMethodName:
- Description: >-
- Do not put a space between a method name and the opening
- parenthesis in a method definition.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
- Enabled: false
-
-Style/SpaceAfterNot:
- Description: Tracks redundant space after the ! operator.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-bang'
- Enabled: false
-
-Style/SpaceAfterSemicolon:
- Description: 'Use spaces after semicolons.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
- Enabled: false
-
-Style/SpaceBeforeBlockBraces:
- Description: >-
- Checks that the left block brace has or doesn't have space
- before it.
- Enabled: false
-
-Style/SpaceBeforeComma:
- Description: 'No spaces before commas.'
- Enabled: false
-
-Style/SpaceBeforeComment:
- Description: >-
- Checks for missing space between code and a comment on the
- same line.
- Enabled: false
-
-Style/SpaceBeforeSemicolon:
- Description: 'No spaces before semicolons.'
- Enabled: false
-
-Style/SpaceInsideBlockBraces:
- Description: >-
- Checks that block braces have or don't have surrounding space.
- For blocks taking parameters, checks that the left brace has
- or doesn't have trailing space.
- Enabled: false
-
-Style/SpaceAroundBlockParameters:
- Description: 'Checks the spacing inside and after block parameters pipes.'
- Enabled: false
-
-Style/SpaceAroundEqualsInParameterDefault:
- Description: >-
- Checks that the equals signs in parameter default assignments
- have or don't have surrounding space depending on
- configuration.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-around-equals'
- Enabled: false
-
-Style/SpaceAroundOperators:
- Description: 'Use a single space around operators.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
- Enabled: false
-
-Style/SpaceInsideBrackets:
- Description: 'No spaces after [ or before ].'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
- Enabled: false
-
-Style/SpaceInsideHashLiteralBraces:
- Description: "Use spaces inside hash literal braces - or don't."
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
- Enabled: false
-
-Style/SpaceInsideParens:
- Description: 'No spaces after ( or before ).'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
- Enabled: false
-
-Style/SpaceInsideRangeLiteral:
- Description: 'No spaces inside range literals.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-inside-range-literals'
- Enabled: false
-
-Style/SpaceInsideStringInterpolation:
- Description: 'Checks for padding/surrounding spaces inside string interpolation.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#string-interpolation'
- Enabled: false
-
-Style/SpecialGlobalVars:
- Description: 'Avoid Perl-style global variables.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms'
- Enabled: false
-
-Style/StringLiterals:
- Description: 'Checks if uses of quotes match the configured preference.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals'
- Enabled: false
-
-Style/StringLiteralsInInterpolation:
- Description: >-
- Checks if uses of quotes inside expressions in interpolated
- strings match the configured preference.
- Enabled: false
-
-Style/StructInheritance:
- Description: 'Checks for inheritance from Struct.new.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-extend-struct-new'
- Enabled: false
-
-Style/SymbolLiteral:
- Description: 'Use plain symbols instead of string symbols when possible.'
- Enabled: false
-
-Style/SymbolProc:
- Description: 'Use symbols as procs instead of blocks when possible.'
- Enabled: false
-
-Style/Tab:
- Description: 'No hard tabs.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
- Enabled: false
-
-Style/TrailingBlankLines:
- Description: 'Checks trailing blank lines and final newline.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof'
- Enabled: false
-
-Style/TrailingCommaInArguments:
- Description: 'Checks for trailing comma in parameter lists.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-params-comma'
- Enabled: false
-
-Style/TrailingCommaInLiteral:
- Description: 'Checks for trailing comma in literals.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
- Enabled: false
-
-Style/TrailingWhitespace:
- Description: 'Avoid trailing whitespace.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace'
- Enabled: false
-
-Style/TrivialAccessors:
- Description: 'Prefer attr_* methods to trivial readers/writers.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family'
- Enabled: false
-
-Style/UnlessElse:
- Description: >-
- Do not use unless with else. Rewrite these with the positive
- case first.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless'
- Enabled: false
-
-Style/UnneededCapitalW:
- Description: 'Checks for %W when interpolation is not needed.'
- Enabled: false
-
-Style/UnneededPercentQ:
- Description: 'Checks for %q/%Q when single quotes or double quotes would do.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q'
- Enabled: false
-
-Style/TrailingUnderscoreVariable:
- Description: >-
- Checks for the usage of unneeded trailing underscores at the
- end of parallel variable assignment.
- Enabled: false
-
-Style/VariableInterpolation:
- Description: >-
- Don't interpolate global, instance and class variables
- directly in strings.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#curlies-interpolate'
- Enabled: false
-
-Style/VariableName:
- Description: 'Use the configured style when naming variables.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars'
- Enabled: false
-
-Style/WhenThen:
- Description: 'Use when x then ... for one-line cases.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#one-line-cases'
- Enabled: false
-
-Style/WhileUntilDo:
- Description: 'Checks for redundant do after while or until.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-while-do'
- Enabled: false
-
-Style/WhileUntilModifier:
- Description: >-
- Favor modifier while/until usage when you have a
- single-line body.
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier'
- Enabled: false
-
-Style/WordArray:
- Description: 'Use %w or %W for arrays of words.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w'
- Enabled: false
diff --git a/.slugignore b/.slugignore
index c82286b6..be7815d3 100644
--- a/.slugignore
+++ b/.slugignore
@@ -1,7 +1,6 @@
*.sqlite3
-*.scss
*.md
-*.rb
-Vagrantfile
-config/vagrant/*
-config/vagrant.yml
+**/tests
+conftest.py
+pytest.ini
+Dockerfile
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index b2527c43..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-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
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 00000000..91dafe8d
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,102 @@
+[[source]]
+url = "https://pypi.python.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+ansicolors = "~=1.1.8"
+asgiref = "~=3.3.1"
+"backports.tempfile" = "~=1.0"
+"backports.weakref" = "~=1.0.post1"
+beautifulsoup4 = "~=4.9.3"
+Brotli = "~=1.0.9"
+cachetools = "~=4.2.1"
+certifi = "~=2020.12.5"
+chardet = "~=4.0.0"
+configparser = "~=5.0.1"
+contextlib2 = "~=0.6.0.post1"
+cssselect = "~=1.1.0"
+cssutils = "~=1.0.2"
+dj-database-url = "~=0.5.0"
+dj-static = "~=0.0.6"
+Django = "~=3.1.12"
+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"
+importlib-metadata = "~=3.4.0"
+lxml = "~=4.6.3"
+Markdown = "~=3.3.3"
+msgpack = "~=1.0.2"
+pep517 = "~=0.9.1"
+Pillow = "~=8.3.2"
+premailer = "~=3.7.0"
+progress = "~=1.5"
+psutil = "~=5.8.0"
+psycopg2 = "~=2.8.6"
+Pygments = "~=2.7.4"
+pyparsing = "~=2.4.7"
+PyPDF2 = "~=1.26.0"
+PyPOM = "~=2.2.0"
+python-dateutil = "~=2.8.1"
+pytoml = "~=0.1.21"
+pytz = "~=2020.5"
+reportlab = "~=3.5.59"
+requests = "~=2.25.1"
+retrying = "~=1.3.3"
+simplejson = "~=3.17.2"
+six = "~=1.15.0"
+soupsieve = "~=2.1"
+sqlparse = "~=0.4.2"
+static3 = "~=0.7.0"
+svg2rlg = "~=0.3"
+tini = "~=3.0.1"
+tornado = "~=6.1"
+urllib3 = "~=1.26.5"
+whitenoise = "~=5.2.0"
+yolk = "~=0.4.3"
+"z3c.rml" = "~=4.1.2"
+zipp = "~=3.4.0"
+"zope.component" = "~=4.6.2"
+"zope.deferredimport" = "~=4.3.1"
+"zope.deprecation" = "~=4.4.0"
+"zope.event" = "~=4.5.0"
+"zope.hookable" = "~=5.0.1"
+"zope.interface" = "~=5.2.0"
+"zope.proxy" = "~=4.3.5"
+"zope.schema" = "~=6.0.1"
+sentry-sdk = "*"
+diff-match-patch = "*"
+python-barcode = "*"
+django-hCaptcha = "*"
+
+[dev-packages]
+selenium = "~=3.141.0"
+pycodestyle = "*"
+coveralls = "*"
+django-coverage-plugin = "*"
+pytest-cov = "*"
+pytest-django = "*"
+pluggy = "*"
+pytest-splinter = "*"
+pytest = "*"
+
+[requires]
+python_version = "3.9"
+
+[dev-packages.pytest-xdist]
+extras = [ "psutil",]
+version = "*"
+
+[dev-packages.PyPOM]
+extras = [ "splinter",]
+version = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 00000000..03bc25aa
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,1526 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "ad1849939ea22858eeac17e407bacd6b5abdac3279a845ca275ea64073d71dd9"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.9"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.python.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "ansicolors": {
+ "hashes": [
+ "sha256:00d2dde5a675579325902536738dd27e4fac1fd68f773fe36c21044eb559e187",
+ "sha256:99f94f5e3348a0bcd43c82e5fc4414013ccc19d70bd939ad71e0133ce9c372e0"
+ ],
+ "index": "pypi",
+ "version": "==1.1.8"
+ },
+ "asgiref": {
+ "hashes": [
+ "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
+ "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
+ ],
+ "index": "pypi",
+ "version": "==3.3.4"
+ },
+ "backports.tempfile": {
+ "hashes": [
+ "sha256:05aa50940946f05759696156a8c39be118169a0e0f94a49d0bb106503891ff54",
+ "sha256:1c648c452e8770d759bdc5a5e2431209be70d25484e1be24876cf2168722c762"
+ ],
+ "index": "pypi",
+ "version": "==1.0"
+ },
+ "backports.weakref": {
+ "hashes": [
+ "sha256:81bc9b51c0abc58edc76aefbbc68c62a787918ffe943a37947e162c3f8e19e82",
+ "sha256:bc4170a29915f8b22c9e7c4939701859650f2eb84184aee80da329ac0b9825c2"
+ ],
+ "index": "pypi",
+ "version": "==1.0.post1"
+ },
+ "beautifulsoup4": {
+ "hashes": [
+ "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35",
+ "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25",
+ "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"
+ ],
+ "index": "pypi",
+ "version": "==4.9.3"
+ },
+ "brotli": {
+ "hashes": [
+ "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8",
+ "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b",
+ "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c",
+ "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c",
+ "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70",
+ "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f",
+ "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181",
+ "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130",
+ "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19",
+ "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429",
+ "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126",
+ "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4",
+ "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0",
+ "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438",
+ "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f",
+ "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389",
+ "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6",
+ "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26",
+ "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7",
+ "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14",
+ "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2",
+ "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430",
+ "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296",
+ "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12",
+ "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d",
+ "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452",
+ "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c",
+ "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761",
+ "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b",
+ "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea",
+ "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c",
+ "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a",
+ "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031",
+ "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267",
+ "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5",
+ "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7",
+ "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d",
+ "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43",
+ "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa",
+ "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb",
+ "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b",
+ "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4",
+ "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3",
+ "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7",
+ "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1",
+ "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb",
+ "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"
+ ],
+ "index": "pypi",
+ "version": "==1.0.9"
+ },
+ "cachetools": {
+ "hashes": [
+ "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
+ "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
+ ],
+ "index": "pypi",
+ "version": "==4.2.2"
+ },
+ "certifi": {
+ "hashes": [
+ "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
+ "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
+ ],
+ "index": "pypi",
+ "version": "==2020.12.5"
+ },
+ "chardet": {
+ "hashes": [
+ "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
+ "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
+ ],
+ "index": "pypi",
+ "version": "==4.0.0"
+ },
+ "configparser": {
+ "hashes": [
+ "sha256:85d5de102cfe6d14a5172676f09d19c465ce63d6019cf0a4ef13385fc535e828",
+ "sha256:af59f2cdd7efbdd5d111c1976ecd0b82db9066653362f0962d7bf1d3ab89a1fa"
+ ],
+ "index": "pypi",
+ "version": "==5.0.2"
+ },
+ "contextlib2": {
+ "hashes": [
+ "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e",
+ "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"
+ ],
+ "index": "pypi",
+ "version": "==0.6.0.post1"
+ },
+ "cssselect": {
+ "hashes": [
+ "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf",
+ "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc"
+ ],
+ "index": "pypi",
+ "version": "==1.1.0"
+ },
+ "cssutils": {
+ "hashes": [
+ "sha256:a2fcf06467553038e98fea9cfe36af2bf14063eb147a70958cfcaa8f5786acaf",
+ "sha256:c74dbe19c92f5052774eadb15136263548dd013250f1ed1027988e7fef125c8d"
+ ],
+ "index": "pypi",
+ "version": "==1.0.2"
+ },
+ "diff-match-patch": {
+ "hashes": [
+ "sha256:8bf9d9c4e059d917b5c6312bac0c137971a32815ddbda9c682b949f2986b4d34",
+ "sha256:da6f5a01aa586df23dfc89f3827e1cafbb5420be9d87769eeb079ddfd9477a18"
+ ],
+ "index": "pypi",
+ "version": "==20200713"
+ },
+ "dj-database-url": {
+ "hashes": [
+ "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163",
+ "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"
+ ],
+ "index": "pypi",
+ "version": "==0.5.0"
+ },
+ "dj-static": {
+ "hashes": [
+ "sha256:032ec1c532617922e6e3e956d504a6fb1acce4fc1c7c94612d0fda21828ce8ef"
+ ],
+ "index": "pypi",
+ "version": "==0.0.6"
+ },
+ "django": {
+ "hashes": [
+ "sha256:9f8be75646f62204320b195062b1d696ba28aa3d45ee72fb7c888ffaebc5bdb2",
+ "sha256:a6e0d1ff11095b7394c079ade7094c73b2dc3df4a7a373c9b58ed73b77a97feb"
+ ],
+ "index": "pypi",
+ "version": "==3.1.13"
+ },
+ "django-debug-toolbar": {
+ "hashes": [
+ "sha256:8c5b13795d4040008ee69ba82dcdd259c49db346cf7d0de6e561a49d191f0860",
+ "sha256:d7bab7573fab35b0fd029163371b7182f5826c13da69734beb675c761d06a4d3"
+ ],
+ "index": "pypi",
+ "version": "==3.2.2"
+ },
+ "django-filter": {
+ "hashes": [
+ "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06",
+ "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"
+ ],
+ "index": "pypi",
+ "version": "==2.4.0"
+ },
+ "django-hcaptcha": {
+ "hashes": [
+ "sha256:2b80197c07bb8444249bcce3758b0472d369cca309fb02d7abcd0a856431b76b"
+ ],
+ "index": "pypi",
+ "version": "==0.1.0"
+ },
+ "django-htmlmin": {
+ "hashes": [
+ "sha256:e41b2a2157570846645cc636a9bddde8aa3e03f6834a9211e61a17f2ed42b87e"
+ ],
+ "index": "pypi",
+ "version": "==0.11.0"
+ },
+ "django-ical": {
+ "hashes": [
+ "sha256:6df4dc61eb4abc55816bd16a949e497bea99828c7de648438ace7f1f85eeb405",
+ "sha256:bd5c874d2eb81329f220174cc0dde7be385f4574ce6c8a2d1579d7fd564a94f3"
+ ],
+ "index": "pypi",
+ "version": "==1.7.3"
+ },
+ "django-recurrence": {
+ "hashes": [
+ "sha256:715f681f6af029ff3a8d73c7b1460abd8cbc5d5a5001efcb127032e84d9cb963",
+ "sha256:9053b44b78b7fbfe3530673edfdd6d2f562105f8a192bc6a4b906a3df4f95f59"
+ ],
+ "index": "pypi",
+ "version": "==1.10.3"
+ },
+ "django-registration-redux": {
+ "hashes": [
+ "sha256:e3d123354a1b8cbfa005d60f1ebb89ae8541f3eaffd6174d9f2aff529b57e430",
+ "sha256:e94b8a945e1cbfa9ec6c32b549597270405328d4e26651985d287d0211120691"
+ ],
+ "index": "pypi",
+ "version": "==2.9"
+ },
+ "django-reversion": {
+ "hashes": [
+ "sha256:1b57127a136b969f4b843a915c72af271febe7f336469db6c27121f8adcad35c",
+ "sha256:a5af55f086a3f9c38be2f049c251e06005b9ed48ba7a109473736b1fc95a066f"
+ ],
+ "index": "pypi",
+ "version": "==3.0.9"
+ },
+ "django-toolbelt": {
+ "hashes": [
+ "sha256:2711b7f9c46908a3f867f4ebb5c0c3f06dcc4f2cabe48a7a53292f6f1cbb83e5"
+ ],
+ "index": "pypi",
+ "version": "==0.0.1"
+ },
+ "django-widget-tweaks": {
+ "hashes": [
+ "sha256:9f91ca4217199b7671971d3c1f323a2bec71a0c27dec6260b3c006fa541bc489",
+ "sha256:f80bff4a8a59b278bb277a405a76a8b9a884e4bae7a6c70e78a39c626cd1c836"
+ ],
+ "index": "pypi",
+ "version": "==1.4.8"
+ },
+ "envparse": {
+ "hashes": [
+ "sha256:4f3b9a27bb55d27f124eb4adf006fec05e4588891c9a054a183a112645056eb7"
+ ],
+ "index": "pypi",
+ "version": "==0.2.0"
+ },
+ "gunicorn": {
+ "hashes": [
+ "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
+ "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
+ ],
+ "index": "pypi",
+ "version": "==20.0.4"
+ },
+ "html5lib": {
+ "hashes": [
+ "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d",
+ "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"
+ ],
+ "version": "==1.1"
+ },
+ "icalendar": {
+ "hashes": [
+ "sha256:0fc18d87f66e0b5da84fa731389496cfe18e4c21304e8f6713556b2e8724a7a4",
+ "sha256:8c35be16c1d0581a276002af883297aeffa8116e366fdce4d5318e1424aa1903"
+ ],
+ "index": "pypi",
+ "version": "==4.0.7"
+ },
+ "idna": {
+ "hashes": [
+ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
+ "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
+ ],
+ "index": "pypi",
+ "version": "==2.10"
+ },
+ "importlib-metadata": {
+ "hashes": [
+ "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771",
+ "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"
+ ],
+ "index": "pypi",
+ "version": "==3.4.0"
+ },
+ "lxml": {
+ "hashes": [
+ "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d",
+ "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3",
+ "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2",
+ "sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae",
+ "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f",
+ "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927",
+ "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3",
+ "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7",
+ "sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59",
+ "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f",
+ "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade",
+ "sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96",
+ "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468",
+ "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b",
+ "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4",
+ "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354",
+ "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83",
+ "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04",
+ "sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16",
+ "sha256:64812391546a18896adaa86c77c59a4998f33c24788cadc35789e55b727a37f4",
+ "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791",
+ "sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a",
+ "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51",
+ "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1",
+ "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a",
+ "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f",
+ "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee",
+ "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec",
+ "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969",
+ "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28",
+ "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a",
+ "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa",
+ "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106",
+ "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d",
+ "sha256:c1a40c06fd5ba37ad39caa0b3144eb3772e813b5fb5b084198a985431c2f1e8d",
+ "sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617",
+ "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4",
+ "sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92",
+ "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0",
+ "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4",
+ "sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24",
+ "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2",
+ "sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e",
+ "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0",
+ "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654",
+ "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2",
+ "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23",
+ "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"
+ ],
+ "index": "pypi",
+ "version": "==4.6.3"
+ },
+ "markdown": {
+ "hashes": [
+ "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49",
+ "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"
+ ],
+ "index": "pypi",
+ "version": "==3.3.4"
+ },
+ "msgpack": {
+ "hashes": [
+ "sha256:0cb94ee48675a45d3b86e61d13c1e6f1696f0183f0715544976356ff86f741d9",
+ "sha256:1026dcc10537d27dd2d26c327e552f05ce148977e9d7b9f1718748281b38c841",
+ "sha256:26a1759f1a88df5f1d0b393eb582ec022326994e311ba9c5818adc5374736439",
+ "sha256:2a5866bdc88d77f6e1370f82f2371c9bc6fc92fe898fa2dec0c5d4f5435a2694",
+ "sha256:31c17bbf2ae5e29e48d794c693b7ca7a0c73bd4280976d408c53df421e838d2a",
+ "sha256:497d2c12426adcd27ab83144057a705efb6acc7e85957a51d43cdcf7f258900f",
+ "sha256:5a9ee2540c78659a1dd0b110f73773533ee3108d4e1219b5a15a8d635b7aca0e",
+ "sha256:8521e5be9e3b93d4d5e07cb80b7e32353264d143c1f072309e1863174c6aadb1",
+ "sha256:87869ba567fe371c4555d2e11e4948778ab6b59d6cc9d8460d543e4cfbbddd1c",
+ "sha256:8ffb24a3b7518e843cd83538cf859e026d24ec41ac5721c18ed0c55101f9775b",
+ "sha256:92be4b12de4806d3c36810b0fe2aeedd8d493db39e2eb90742b9c09299eb5759",
+ "sha256:9ea52fff0473f9f3000987f313310208c879493491ef3ccf66268eff8d5a0326",
+ "sha256:a4355d2193106c7aa77c98fc955252a737d8550320ecdb2e9ac701e15e2943bc",
+ "sha256:a99b144475230982aee16b3d249170f1cccebf27fb0a08e9f603b69637a62192",
+ "sha256:ac25f3e0513f6673e8b405c3a80500eb7be1cf8f57584be524c4fa78fe8e0c83",
+ "sha256:b28c0876cce1466d7c2195d7658cf50e4730667196e2f1355c4209444717ee06",
+ "sha256:b55f7db883530b74c857e50e149126b91bb75d35c08b28db12dcb0346f15e46e",
+ "sha256:b6d9e2dae081aa35c44af9c4298de4ee72991305503442a5c74656d82b581fe9",
+ "sha256:c747c0cc08bd6d72a586310bda6ea72eeb28e7505990f342552315b229a19b33",
+ "sha256:d6c64601af8f3893d17ec233237030e3110f11b8a962cb66720bf70c0141aa54",
+ "sha256:d8167b84af26654c1124857d71650404336f4eb5cc06900667a493fc619ddd9f",
+ "sha256:de6bd7990a2c2dabe926b7e62a92886ccbf809425c347ae7de277067f97c2887",
+ "sha256:e36a812ef4705a291cdb4a2fd352f013134f26c6ff63477f20235138d1d21009",
+ "sha256:e89ec55871ed5473a041c0495b7b4e6099f6263438e0bd04ccd8418f92d5d7f2",
+ "sha256:f3e6aaf217ac1c7ce1563cf52a2f4f5d5b1f64e8729d794165db71da57257f0c",
+ "sha256:f484cd2dca68502de3704f056fa9b318c94b1539ed17a4c784266df5d6978c87",
+ "sha256:fae04496f5bc150eefad4e9571d1a76c55d021325dcd484ce45065ebbdd00984",
+ "sha256:fe07bc6735d08e492a327f496b7850e98cb4d112c56df69b0c844dbebcbb47f6"
+ ],
+ "index": "pypi",
+ "version": "==1.0.2"
+ },
+ "pep517": {
+ "hashes": [
+ "sha256:3985b91ebf576883efe5fa501f42a16de2607684f3797ddba7202b71b7d0da51",
+ "sha256:aeb78601f2d1aa461960b43add204cc7955667687fbcf9cdb5170f00556f117f"
+ ],
+ "index": "pypi",
+ "version": "==0.9.1"
+ },
+ "pikepdf": {
+ "hashes": [
+ "sha256:1de8b978bbea7024576db18f0e8c58cba1092b07898e7b6350fbc1d6b51a736d",
+ "sha256:2b5dcb5857743c5c903270bcce0184593d81d6b771e8a18480ba0026329d45c5",
+ "sha256:31473ac2506fede408d43decca8bd31fdd0f8528e202b8bb6c7a3c32dff594c4",
+ "sha256:32f83164accd8e570fcbe0378dfe5b781e37c515d74ade55a086140d85f2439b",
+ "sha256:36f48a3261904f1350dbbf63d4a8d6587ecb70fccd596ad6af688a453e449900",
+ "sha256:3a370dec316a4dad572a2c37c8734cd543ee499e4d502cedb090011f5d7fcd7b",
+ "sha256:3c17937e230b22afa975e69130e89df2911dd1e2c7bbe200138684154e428843",
+ "sha256:3d097af6bb735fe23ca1f7386f967adffdd0e51f9bc6fb92657ad0f2a0a24e76",
+ "sha256:4c2114630ae4e9fc69c127f154786765a53f87df306a802f58d8622bdb06b12e",
+ "sha256:5f8766bfdc4fd9ce879c3e0973b6b979ec7bcf4cf693179b34c3841ee39ecaf8",
+ "sha256:62932d06265c821ca161d6bba6c7b8fb4804129acbb75877351403e5e8fcd27f",
+ "sha256:6866deb4a4fd5defe186e418be04a9540b7240c06a8752b4ba6c5d6f33fcad76",
+ "sha256:81c628d7eae51b7a076b88e04b66681556f4bc7373cecf80e4c1155c18e50032",
+ "sha256:9188295bce64648bed2364b96aa7a946de6db6cdefb77fee21b0bd63b6978561",
+ "sha256:b1592772761bde77be1fb6ffc2be5131f25bb38412d89e433d5fe13a8a62f8e9",
+ "sha256:b213b4665161f1447e82fc05c51f95bbcdb9b26c733f0ded2d6d7427bbef4b7c",
+ "sha256:bbded0e77da1b07a4bfde920a44514ebeecd57174c2c39c02a630250953e5553",
+ "sha256:c0c4d4c17f644024235263b20e766939dde1e63f1cb616f74331e7d901275f35",
+ "sha256:cb2b3ef8aeb69b66a5e13834671a4a040ffc116d364e0cafd8b96472d18d1403",
+ "sha256:cc02f01011a291e2e702884438517e6ce7eee3cafb2bc61b4fa9d73dab514111",
+ "sha256:d9612d1570c698b75ff4530e866a35b100cf7588345e3fc98fdc39c2baaa637d",
+ "sha256:dd65b9c87ff8c3a6838e50e6268407505457c1bd72037d7801828b94a6bed826",
+ "sha256:e94c00bc3cefda5a4d1289f486f79b56be2f564c841d3753e749d9c3a076e67a",
+ "sha256:f147932f1090a029c208a37a979cd8b97bdd6107c4885faeabf8c9da6cd32c43",
+ "sha256:f1a31fcb7f34609eca0b3330ad4fbc38ff3b30b9341a0ff69a0cd7e376ce6b91"
+ ],
+ "version": "==3.0.0"
+ },
+ "pillow": {
+ "hashes": [
+ "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30",
+ "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9",
+ "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71",
+ "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9",
+ "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b",
+ "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630",
+ "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875",
+ "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2",
+ "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1",
+ "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7",
+ "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3",
+ "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b",
+ "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6",
+ "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba",
+ "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4",
+ "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864",
+ "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056",
+ "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228",
+ "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8",
+ "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb",
+ "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d",
+ "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da",
+ "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073",
+ "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3",
+ "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616",
+ "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa",
+ "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979",
+ "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a",
+ "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b",
+ "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6",
+ "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441",
+ "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624",
+ "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd",
+ "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550",
+ "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09",
+ "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196",
+ "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b",
+ "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1",
+ "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6",
+ "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83",
+ "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f",
+ "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4",
+ "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19",
+ "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341",
+ "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96",
+ "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355",
+ "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c",
+ "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c",
+ "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629",
+ "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2",
+ "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87",
+ "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5",
+ "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"
+ ],
+ "index": "pypi",
+ "version": "==8.3.2"
+ },
+ "pluggy": {
+ "hashes": [
+ "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
+ "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
+ ],
+ "version": "==1.0.0"
+ },
+ "premailer": {
+ "hashes": [
+ "sha256:5eec9603e84cee583a390de69c75192e50d76e38ef0292b027bd64923766aca7",
+ "sha256:c7ac48986984a810afea5147bc8410a8fe0659bf52f357e78b28a1b949209b91"
+ ],
+ "index": "pypi",
+ "version": "==3.7.0"
+ },
+ "progress": {
+ "hashes": [
+ "sha256:c9c86e98b5c03fa1fe11e3b67c1feda4788b8d0fe7336c2ff7d5644ccfba34cd"
+ ],
+ "index": "pypi",
+ "version": "==1.6"
+ },
+ "psutil": {
+ "hashes": [
+ "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64",
+ "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131",
+ "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c",
+ "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6",
+ "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023",
+ "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df",
+ "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394",
+ "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4",
+ "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b",
+ "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2",
+ "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d",
+ "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65",
+ "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d",
+ "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef",
+ "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7",
+ "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60",
+ "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6",
+ "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8",
+ "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b",
+ "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d",
+ "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac",
+ "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935",
+ "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d",
+ "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28",
+ "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876",
+ "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0",
+ "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3",
+ "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"
+ ],
+ "index": "pypi",
+ "version": "==5.8.0"
+ },
+ "psycopg2": {
+ "hashes": [
+ "sha256:00195b5f6832dbf2876b8bf77f12bdce648224c89c880719c745b90515233301",
+ "sha256:068115e13c70dc5982dfc00c5d70437fe37c014c808acce119b5448361c03725",
+ "sha256:26e7fd115a6db75267b325de0fba089b911a4a12ebd3d0b5e7acb7028bc46821",
+ "sha256:2c93d4d16933fea5bbacbe1aaf8fa8c1348740b2e50b3735d1b0bf8154cbf0f3",
+ "sha256:56007a226b8e95aa980ada7abdea6b40b75ce62a433bd27cec7a8178d57f4051",
+ "sha256:56fee7f818d032f802b8eed81ef0c1232b8b42390df189cab9cfa87573fe52c5",
+ "sha256:6a3d9efb6f36f1fe6aa8dbb5af55e067db802502c55a9defa47c5a1dad41df84",
+ "sha256:a49833abfdede8985ba3f3ec641f771cca215479f41523e99dace96d5b8cce2a",
+ "sha256:ad2fe8a37be669082e61fb001c185ffb58867fdbb3e7a6b0b0d2ffe232353a3e",
+ "sha256:b8cae8b2f022efa1f011cc753adb9cbadfa5a184431d09b273fb49b4167561ad",
+ "sha256:d160744652e81c80627a909a0e808f3c6653a40af435744de037e3172cf277f5",
+ "sha256:d5062ae50b222da28253059880a871dc87e099c25cb68acf613d9d227413d6f7",
+ "sha256:f22ea9b67aea4f4a1718300908a2fb62b3e4276cf00bd829a97ab5894af42ea3",
+ "sha256:f974c96fca34ae9e4f49839ba6b78addf0346777b46c4da27a7bf54f48d3057d",
+ "sha256:fb23f6c71107c37fd667cb4ea363ddeb936b348bbd6449278eb92c189699f543"
+ ],
+ "index": "pypi",
+ "version": "==2.8.6"
+ },
+ "pygments": {
+ "hashes": [
+ "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435",
+ "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"
+ ],
+ "index": "pypi",
+ "version": "==2.7.4"
+ },
+ "pyparsing": {
+ "hashes": [
+ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
+ "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
+ ],
+ "index": "pypi",
+ "version": "==2.4.7"
+ },
+ "pypdf2": {
+ "hashes": [
+ "sha256:e28f902f2f0a1603ea95ebe21dff311ef09be3d0f0ef29a3e44a932729564385"
+ ],
+ "index": "pypi",
+ "version": "==1.26.0"
+ },
+ "pypom": {
+ "hashes": [
+ "sha256:4bdd57fceb72d7e6a3645cf6c9322f490d9cfb5d777eac2c851a3b658b813939",
+ "sha256:6772ec99f0a21a5bdc8c092007a8c813ed18359e67ed70258bbb233df5e28829"
+ ],
+ "index": "pypi",
+ "version": "==2.2.0"
+ },
+ "python-barcode": {
+ "hashes": [
+ "sha256:daa32fb999a843812fbb1c75ff909638811af7c465f0a991e9e41d26d2a44a24",
+ "sha256:fafba4aa24e9d969777be521c294ff18f6c2b36ad63b5fc2f2108d972e23b252"
+ ],
+ "index": "pypi",
+ "version": "==0.13.1"
+ },
+ "python-dateutil": {
+ "hashes": [
+ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+ ],
+ "index": "pypi",
+ "version": "==2.8.2"
+ },
+ "pytoml": {
+ "hashes": [
+ "sha256:57a21e6347049f73bfb62011ff34cd72774c031b9828cb628a752225136dfc33",
+ "sha256:8eecf7c8d0adcff3b375b09fe403407aa9b645c499e5ab8cac670ac4a35f61e7"
+ ],
+ "index": "pypi",
+ "version": "==0.1.21"
+ },
+ "pytz": {
+ "hashes": [
+ "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
+ "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
+ ],
+ "index": "pypi",
+ "version": "==2020.5"
+ },
+ "reportlab": {
+ "hashes": [
+ "sha256:010f86a192c397f7c8ae667953a85d913395a8a6a8da112bff1c1ea28e679bcd",
+ "sha256:08b53568979228b6969b790339d06a0b8db8883f92ae7339013f9878042dd9ca",
+ "sha256:19708801278f600d712c04ee6bfb650e45d1b2898713f7bd97b39ab89bd08c1e",
+ "sha256:28c72d27f21d74a7301789c7950b5e82a430ed38817ecee060fa1f2f3e959360",
+ "sha256:2c0c88a7cf83a20a2bb355f97a1a9d0373a6de60c3aec35d301d3cc75dc4bb72",
+ "sha256:2dc5ee0c5b659697cdfbc218ec9abea54dd9c5a95ea8ca95245fe94f5ef111f9",
+ "sha256:332f836ff4c975c92d307302e86a54d6f0e3d2ce33a35759812e7a1d17e2091f",
+ "sha256:45113c1c359ba314499032c891487802cccd7c4225a3e930d6cf492d62ea4f07",
+ "sha256:46f15f5a34a50375c332ab8eaa907a0212c88787b0885ac25a9505c0741ee9ba",
+ "sha256:580eed6d9e5c20870ea909bec6840f9ceb9d13c33316d448cae21eb3ca47c7fd",
+ "sha256:5865c4247229584408515055b5b19c7f935ae94433d6258c7a9234c4a07d6d34",
+ "sha256:6063466779e438375bcdd2c15fc551ebd68f16ebfb2766497234df9cfa57e5b1",
+ "sha256:63578cab96fc4383e71dd9fe1877bb26ab78b2a6c91139068e99d130687289ab",
+ "sha256:66b5a08cbeb910edee7201efa786bd1bf7027c7ec526dddf7d60fc2252e2b30f",
+ "sha256:6b448a1824d381d282c5ea1da1669a5fa53dac67c57a1ecad6bcc149f286d1fd",
+ "sha256:6f905390f5e5801b21b6027c8ffaed915e5eec1e46bbdf6a74c8838213717b44",
+ "sha256:70e7461aa47eff810be8c4e4a0cbc6fcf47aecaddd46de6ca4524c76065f8490",
+ "sha256:7e466276f1a1121dac23b703af6c22db0cedf6cec5139969f8387e8d8046f203",
+ "sha256:81d1958d90fccf86f62b38ecbedf9208a973d99e0747b6cd75036914ae8641c4",
+ "sha256:9a00feb8eafbce1283cd3edbb29735bd40c9566b3f45913110a301700c16b63a",
+ "sha256:a48221d4ab7de37975ad052f7e565cf13ab708def63f203a38ae9927ab5442cd",
+ "sha256:ad9a49890de59e8dd16fa0ce03ef607e46a5ff2f39de44f8556f796b3d4ddffb",
+ "sha256:b25608059558910585a9e229bae0fd3d67af49ae5e1c7a20057680c6b3d5f6f7",
+ "sha256:b57ebeb28f7a58a9da6f8c293acb6d31d89f634b3eba0b728a040cef08afc4ea",
+ "sha256:b9ae0c534c09274b80f8fd87408071c1f814d56c5f51fe450b2157f1f13e921b",
+ "sha256:c0612d9101f40679245e7d9edb169d8d79378a47f38cd8e6b38c55d7ff31db3f",
+ "sha256:ced16daf89f948eeb4e376b5d814da5d99f7205fbd42e17a96f257e35dc31bdd",
+ "sha256:dd3409ebabe699c98058690b7b730f93e6b0bd4ed5e49ca3b15e1530ae07b40b",
+ "sha256:efef6a97e3ab49f3f40037dbf9a4166668a17cc6aaba13d5ecbabdf854a9b332"
+ ],
+ "index": "pypi",
+ "version": "==3.5.68"
+ },
+ "requests": {
+ "hashes": [
+ "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
+ "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
+ ],
+ "index": "pypi",
+ "version": "==2.25.1"
+ },
+ "retrying": {
+ "hashes": [
+ "sha256:08c039560a6da2fe4f2c426d0766e284d3b736e355f8dd24b37367b0bb41973b"
+ ],
+ "index": "pypi",
+ "version": "==1.3.3"
+ },
+ "selenium": {
+ "hashes": [
+ "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c",
+ "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"
+ ],
+ "version": "==3.141.0"
+ },
+ "sentry-sdk": {
+ "hashes": [
+ "sha256:ebe99144fa9618d4b0e7617e7929b75acd905d258c3c779edcd34c0adfffe26c",
+ "sha256:f33d34c886d0ba24c75ea8885a8b3a172358853c7cbde05979fc99c29ef7bc52"
+ ],
+ "index": "pypi",
+ "version": "==1.3.1"
+ },
+ "simplejson": {
+ "hashes": [
+ "sha256:065230b9659ac38c8021fa512802562d122afb0cf8d4b89e257014dcddb5730a",
+ "sha256:07707ba69324eaf58f0c6f59d289acc3e0ed9ec528dae5b0d4219c0d6da27dc5",
+ "sha256:10defa88dd10a0a4763f16c1b5504e96ae6dc68953cfe5fc572b4a8fcaf9409b",
+ "sha256:140eb58809f24d843736edb8080b220417e22c82ac07a3dfa473f57e78216b5f",
+ "sha256:188f2c78a8ac1eb7a70a4b2b7b9ad11f52181044957bf981fb3e399c719e30ee",
+ "sha256:1c2688365743b0f190392e674af5e313ebe9d621813d15f9332e874b7c1f2d04",
+ "sha256:24e413bd845bd17d4d72063d64e053898543fb7abc81afeae13e5c43cef9c171",
+ "sha256:2b59acd09b02da97728d0bae8ff48876d7efcbbb08e569c55e2d0c2e018324f5",
+ "sha256:2df15814529a4625ea6f7b354a083609b3944c269b954ece0d0e7455872e1b2a",
+ "sha256:352c11582aa1e49a2f0f7f7d8fd5ec5311da890d1354287e83c63ab6af857cf5",
+ "sha256:36b08b886027eac67e7a0e822e3a5bf419429efad7612e69501669d6252a21f2",
+ "sha256:376023f51edaf7290332dacfb055bc00ce864cb013c0338d0dea48731f37e42f",
+ "sha256:3ba82f8b421886f4a2311c43fb98faaf36c581976192349fef2a89ed0fcdbdef",
+ "sha256:3d72aa9e73134dacd049a2d6f9bd219f7be9c004d03d52395831611d66cedb71",
+ "sha256:40ece8fa730d1a947bff792bcc7824bd02d3ce6105432798e9a04a360c8c07b0",
+ "sha256:417b7e119d66085dc45bdd563dcb2c575ee10a3b1c492dd3502a029448d4be1c",
+ "sha256:42b7c7264229860fe879be961877f7466d9f7173bd6427b3ba98144a031d49fb",
+ "sha256:457d9cfe7ece1571770381edccdad7fc255b12cd7b5b813219441146d4f47595",
+ "sha256:4a6943816e10028eeed512ea03be52b54ea83108b408d1049b999f58a760089b",
+ "sha256:5b94df70bd34a3b946c0eb272022fb0f8a9eb27cad76e7f313fedbee2ebe4317",
+ "sha256:5f5051a13e7d53430a990604b532c9124253c5f348857e2d5106d45fc8533860",
+ "sha256:5f7f53b1edd4b23fb112b89208377480c0bcee45d43a03ffacf30f3290e0ed85",
+ "sha256:5fe8c6dcb9e6f7066bdc07d3c410a2fca78c0d0b4e0e72510ffd20a60a20eb8e",
+ "sha256:71a54815ec0212b0cba23adc1b2a731bdd2df7b9e4432718b2ed20e8aaf7f01a",
+ "sha256:7332f7b06d42153255f7bfeb10266141c08d48cc1a022a35473c95238ff2aebc",
+ "sha256:78c6f0ed72b440ebe1892d273c1e5f91e55e6861bea611d3b904e673152a7a4c",
+ "sha256:7c9b30a2524ae6983b708f12741a31fbc2fb8d6fecd0b6c8584a62fd59f59e09",
+ "sha256:86fcffc06f1125cb443e2bed812805739d64ceb78597ac3c1b2d439471a09717",
+ "sha256:87572213965fd8a4fb7a97f837221e01d8fddcfb558363c671b8aa93477fb6a2",
+ "sha256:8e595de17178dd3bbeb2c5b8ea97536341c63b7278639cb8ee2681a84c0ef037",
+ "sha256:917f01db71d5e720b731effa3ff4a2c702a1b6dacad9bcdc580d86a018dfc3ca",
+ "sha256:91cfb43fb91ff6d1e4258be04eee84b51a4ef40a28d899679b9ea2556322fb50",
+ "sha256:aa86cfdeb118795875855589934013e32895715ec2d9e8eb7a59be3e7e07a7e1",
+ "sha256:ade09aa3c284d11f39640aebdcbb748e1996f0c60504f8c4a0c5a9fec821e67a",
+ "sha256:b2a5688606dffbe95e1347a05b77eb90489fe337edde888e23bbb7fd81b0d93b",
+ "sha256:b92fbc2bc549c5045c8233d954f3260ccf99e0f3ec9edfd2372b74b350917752",
+ "sha256:c2d5334d935af711f6d6dfeec2d34e071cdf73ec0df8e8bd35ac435b26d8da97",
+ "sha256:cb0afc3bad49eb89a579103616574a54b523856d20fc539a4f7a513a0a8ba4b2",
+ "sha256:ce66f730031b9b3683b2fc6ad4160a18db86557c004c3d490a29bf8d450d7ab9",
+ "sha256:e29b9cea4216ec130df85d8c36efb9985fda1c9039e4706fb30e0fb6a67602ff",
+ "sha256:e2cc4b68e59319e3de778325e34fbff487bfdb2225530e89995402989898d681",
+ "sha256:e90d2e219c3dce1500dda95f5b893c293c4d53c4e330c968afbd4e7a90ff4a5b",
+ "sha256:f13c48cc4363829bdfecc0c181b6ddf28008931de54908a492dc8ccd0066cd60",
+ "sha256:f550730d18edec4ff9d4252784b62adfe885d4542946b6d5a54c8a6521b56afd",
+ "sha256:fa843ee0d34c7193f5a816e79df8142faff851549cab31e84b526f04878ac778",
+ "sha256:fe1c33f78d2060719d52ea9459d97d7ae3a5b707ec02548575c4fbed1d1d345b"
+ ],
+ "index": "pypi",
+ "version": "==3.17.5"
+ },
+ "six": {
+ "hashes": [
+ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
+ "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
+ ],
+ "index": "pypi",
+ "version": "==1.15.0"
+ },
+ "soupsieve": {
+ "hashes": [
+ "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc",
+ "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.0'",
+ "version": "==2.2.1"
+ },
+ "sqlparse": {
+ "hashes": [
+ "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae",
+ "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"
+ ],
+ "index": "pypi",
+ "version": "==0.4.2"
+ },
+ "static3": {
+ "hashes": [
+ "sha256:674641c64bc75507af2eb20bef7e7e3593dca993dec6674be108fa15b42f47c8"
+ ],
+ "index": "pypi",
+ "version": "==0.7.0"
+ },
+ "svg2rlg": {
+ "hashes": [
+ "sha256:05db4480b90e912e08727d4cb24385fe33e8436def079b8f149b61a350638bee"
+ ],
+ "index": "pypi",
+ "version": "==0.3"
+ },
+ "tini": {
+ "hashes": [
+ "sha256:8062be51e6766c15ec402579fff422d8c2fb46bdb3f0b22bd009f32d8dd79c81",
+ "sha256:f856780a90e7a3cdf3aaea56cb8c176fc5f4f8e1d126362cb88ca104f835d99f"
+ ],
+ "index": "pypi",
+ "version": "==3.0.1"
+ },
+ "toml": {
+ "hashes": [
+ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+ "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
+ ],
+ "version": "==0.10.2"
+ },
+ "tornado": {
+ "hashes": [
+ "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb",
+ "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c",
+ "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288",
+ "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95",
+ "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558",
+ "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe",
+ "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791",
+ "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d",
+ "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326",
+ "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b",
+ "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4",
+ "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c",
+ "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910",
+ "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5",
+ "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c",
+ "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0",
+ "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675",
+ "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd",
+ "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f",
+ "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c",
+ "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea",
+ "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6",
+ "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05",
+ "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd",
+ "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575",
+ "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a",
+ "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37",
+ "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795",
+ "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f",
+ "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32",
+ "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c",
+ "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01",
+ "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4",
+ "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2",
+ "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921",
+ "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085",
+ "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df",
+ "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102",
+ "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5",
+ "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68",
+ "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"
+ ],
+ "index": "pypi",
+ "version": "==6.1"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
+ "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
+ ],
+ "index": "pypi",
+ "version": "==1.26.6"
+ },
+ "webencodings": {
+ "hashes": [
+ "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
+ "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
+ ],
+ "version": "==0.5.1"
+ },
+ "whitenoise": {
+ "hashes": [
+ "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7",
+ "sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d"
+ ],
+ "index": "pypi",
+ "version": "==5.2.0"
+ },
+ "yolk": {
+ "hashes": [
+ "sha256:1c07eb4001dc133c08e66e38c5d58faa7616ae804f8d0ab02dd44a1044e7ddb8"
+ ],
+ "index": "pypi",
+ "version": "==0.4.3"
+ },
+ "z3c.rml": {
+ "hashes": [
+ "sha256:0d730e2e61a29c69822ee955366f9d7e9a82e6909c11932329629fb0c1a128a0"
+ ],
+ "index": "pypi",
+ "version": "==4.1.2"
+ },
+ "zipp": {
+ "hashes": [
+ "sha256:a5303f8ad20aff64720bf548256646f74fc8c167065c0d177a98a7cadceed85a",
+ "sha256:ce85de43ee0ead77dd0fbee3902bec1501aef59b92a2e18265396b22a1d756ab"
+ ],
+ "index": "pypi",
+ "version": "==3.4.2"
+ },
+ "zope.component": {
+ "hashes": [
+ "sha256:607628e4c84f7887a69a958542b5c304663e726b73aba0882e3a3f059bff14f3",
+ "sha256:91628918218b3e6f6323de2a7b845e09ddc5cae131c034896c051b084bba3c92"
+ ],
+ "index": "pypi",
+ "version": "==4.6.2"
+ },
+ "zope.deferredimport": {
+ "hashes": [
+ "sha256:57b2345e7b5eef47efcd4f634ff16c93e4265de3dcf325afc7315ade48d909e1",
+ "sha256:9a0c211df44aa95f1c4e6d2626f90b400f56989180d3ef96032d708da3d23e0a"
+ ],
+ "index": "pypi",
+ "version": "==4.3.1"
+ },
+ "zope.deprecation": {
+ "hashes": [
+ "sha256:0d453338f04bacf91bbfba545d8bcdf529aa829e67b705eac8c1a7fdce66e2df",
+ "sha256:f1480b74995958b24ce37b0ef04d3663d2683e5d6debc96726eff18acf4ea113"
+ ],
+ "index": "pypi",
+ "version": "==4.4.0"
+ },
+ "zope.event": {
+ "hashes": [
+ "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42",
+ "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330"
+ ],
+ "index": "pypi",
+ "version": "==4.5.0"
+ },
+ "zope.hookable": {
+ "hashes": [
+ "sha256:0194b9b9e7f614abba60c90b231908861036578297515d3d6508eb10190f266d",
+ "sha256:0c2977473918bdefc6fa8dfb311f154e7f13c6133957fe649704deca79b92093",
+ "sha256:17b8bdb3b77e03a152ca0d5ca185a7ae0156f5e5a2dbddf538676633a1f7380f",
+ "sha256:29d07681a78042cdd15b268ae9decffed9ace68a53eebeb61d65ae931d158841",
+ "sha256:36fb1b35d1150267cb0543a1ddd950c0bc2c75ed0e6e92e3aaa6ac2e29416cb7",
+ "sha256:3aed60c2bb5e812bbf9295c70f25b17ac37c233f30447a96c67913ba5073642f",
+ "sha256:3cac1565cc768911e72ca9ec4ddf5c5109e1fef0104f19f06649cf1874943b60",
+ "sha256:3d4bc0cc4a37c3cd3081063142eeb2125511db3c13f6dc932d899c512690378e",
+ "sha256:3f73096f27b8c28be53ffb6604f7b570fbbb82f273c6febe5f58119009b59898",
+ "sha256:522d1153d93f2d48aa0bd9fb778d8d4500be2e4dcf86c3150768f0e3adbbc4ef",
+ "sha256:523d2928fb7377bbdbc9af9c0b14ad73e6eaf226349f105733bdae27efd15b5a",
+ "sha256:5848309d4fc5c02150a45e8f8d2227e5bfda386a508bbd3160fed7c633c5a2fa",
+ "sha256:6781f86e6d54a110980a76e761eb54590630fd2af2a17d7edf02a079d2646c1d",
+ "sha256:6fd27921ebf3aaa945fa25d790f1f2046204f24dba4946f82f5f0a442577c3e9",
+ "sha256:70d581862863f6bf9e175e85c9d70c2d7155f53fb04dcdb2f73cf288ca559a53",
+ "sha256:81867c23b0dc66c8366f351d00923f2bc5902820a24c2534dfd7bf01a5879963",
+ "sha256:81db29edadcbb740cd2716c95a297893a546ed89db1bfe9110168732d7f0afdd",
+ "sha256:86bd12624068cea60860a0759af5e2c3adc89c12aef6f71cf12f577e28deefe3",
+ "sha256:9c184d8f9f7a76e1ced99855ccf390ffdd0ec3765e5cbf7b9cada600accc0a1e",
+ "sha256:acc789e8c29c13555e43fe4bf9fcd15a65512c9645e97bbaa5602e3201252b02",
+ "sha256:afaa740206b7660d4cc3b8f120426c85761f51379af7a5b05451f624ad12b0af",
+ "sha256:b5f5fa323f878bb16eae68ea1ba7f6c0419d4695d0248bed4b18f51d7ce5ab85",
+ "sha256:bd89e0e2c67bf4ac3aca2a19702b1a37269fb1923827f68324ac2e7afd6e3406",
+ "sha256:c212de743283ec0735db24ec6ad913758df3af1b7217550ff270038062afd6ae",
+ "sha256:ca553f524293a0bdea05e7f44c3e685e4b7b022cb37d87bc4a3efa0f86587a8d",
+ "sha256:cab67065a3db92f636128d3157cc5424a145f82d96fb47159c539132833a6d36",
+ "sha256:d3b3b3eedfdbf6b02898216e85aa6baf50207f4378a2a6803d6d47650cd37031",
+ "sha256:d9f4a5a72f40256b686d31c5c0b1fde503172307beb12c1568296e76118e402c",
+ "sha256:df5067d87aaa111ed5d050e1ee853ba284969497f91806efd42425f5348f1c06",
+ "sha256:e2587644812c6138f05b8a41594a8337c6790e3baf9a01915e52438c13fc6bef",
+ "sha256:e27fd877662db94f897f3fd532ef211ca4901eb1a70ba456f15c0866a985464a",
+ "sha256:e427ebbdd223c72e06ba94c004bb04e996c84dec8a0fa84e837556ae145c439e",
+ "sha256:e583ad4309c203ef75a09d43434cf9c2b4fa247997ecb0dcad769982c39411c7",
+ "sha256:e760b2bc8ece9200804f0c2b64d10147ecaf18455a2a90827fbec4c9d84f3ad5",
+ "sha256:ea9a9cc8bcc70e18023f30fa2f53d11ae069572a162791224e60cd65df55fb69",
+ "sha256:ecb3f17dce4803c1099bd21742cd126b59817a4e76a6544d31d2cca6e30dbffd",
+ "sha256:ed794e3b3de42486d30444fb60b5561e724ee8a2d1b17b0c2e0f81e3ddaf7a87",
+ "sha256:ee885d347279e38226d0a437b6a932f207f691c502ee565aba27a7022f1285df",
+ "sha256:fd5e7bc5f24f7e3d490698f7b854659a9851da2187414617cd5ed360af7efd63",
+ "sha256:fe45f6870f7588ac7b2763ff1ce98cce59369717afe70cc353ec5218bc854bcc"
+ ],
+ "index": "pypi",
+ "version": "==5.0.1"
+ },
+ "zope.interface": {
+ "hashes": [
+ "sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1",
+ "sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d",
+ "sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123",
+ "sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232",
+ "sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549",
+ "sha256:1b5f6c8fff4ed32aa2dd43e84061bc8346f32d3ba6ad6e58f088fe109608f102",
+ "sha256:21e49123f375703cf824214939d39df0af62c47d122d955b2a8d9153ea08cfd5",
+ "sha256:21f579134a47083ffb5ddd1307f0405c91aa8b61ad4be6fd5af0171474fe0c45",
+ "sha256:27c267dc38a0f0079e96a2945ee65786d38ef111e413c702fbaaacbab6361d00",
+ "sha256:299bde0ab9e5c4a92f01a152b7fbabb460f31343f1416f9b7b983167ab1e33bc",
+ "sha256:2ab88d8f228f803fcb8cb7d222c579d13dab2d3622c51e8cf321280da01102a7",
+ "sha256:2ced4c35061eea623bc84c7711eedce8ecc3c2c51cd9c6afa6290df3bae9e104",
+ "sha256:2dcab01c660983ba5e5a612e0c935141ccbee67d2e2e14b833e01c2354bd8034",
+ "sha256:32546af61a9a9b141ca38d971aa6eb9800450fa6620ce6323cc30eec447861f3",
+ "sha256:32b40a4c46d199827d79c86bb8cb88b1bbb764f127876f2cb6f3a47f63dbada3",
+ "sha256:3cc94c69f6bd48ed86e8e24f358cb75095c8129827df1298518ab860115269a4",
+ "sha256:42b278ac0989d6f5cf58d7e0828ea6b5951464e3cf2ff229dd09a96cb6ba0c86",
+ "sha256:495b63fd0302f282ee6c1e6ea0f1c12cb3d1a49c8292d27287f01845ff252a96",
+ "sha256:4af87cdc0d4b14e600e6d3d09793dce3b7171348a094ba818e2a68ae7ee67546",
+ "sha256:4b94df9f2fdde7b9314321bab8448e6ad5a23b80542dcab53e329527d4099dcb",
+ "sha256:4c48ddb63e2b20fba4c6a2bf81b4d49e99b6d4587fb67a6cd33a2c1f003af3e3",
+ "sha256:4df9afd17bd5477e9f8c8b6bb8507e18dd0f8b4efe73bb99729ff203279e9e3b",
+ "sha256:518950fe6a5d56f94ba125107895f938a4f34f704c658986eae8255edb41163b",
+ "sha256:538298e4e113ccb8b41658d5a4b605bebe75e46a30ceca22a5a289cf02c80bec",
+ "sha256:55465121e72e208a7b69b53de791402affe6165083b2ea71b892728bd19ba9ae",
+ "sha256:588384d70a0f19b47409cfdb10e0c27c20e4293b74fc891df3d8eb47782b8b3e",
+ "sha256:6278c080d4afffc9016e14325f8734456831124e8c12caa754fd544435c08386",
+ "sha256:64ea6c221aeee4796860405e1aedec63424cda4202a7ad27a5066876db5b0fd2",
+ "sha256:681dbb33e2b40262b33fd383bae63c36d33fd79fa1a8e4092945430744ffd34a",
+ "sha256:6936aa9da390402d646a32a6a38d5409c2d2afb2950f045a7d02ab25a4e7d08d",
+ "sha256:778d0ec38bbd288b150a3ae363c8ffd88d2207a756842495e9bffd8a8afbc89a",
+ "sha256:8251f06a77985a2729a8bdbefbae79ee78567dddc3acbd499b87e705ca59fe24",
+ "sha256:83b4aa5344cce005a9cff5d0321b2e318e871cc1dfc793b66c32dd4f59e9770d",
+ "sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b",
+ "sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50",
+ "sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523",
+ "sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a",
+ "sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095",
+ "sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a",
+ "sha256:abb61afd84f23099ac6099d804cdba9bd3b902aaaded3ffff47e490b0a495520",
+ "sha256:adf9ee115ae8ff8b6da4b854b4152f253b390ba64407a22d75456fe07dcbda65",
+ "sha256:aedc6c672b351afe6dfe17ff83ee5e7eb6ed44718f879a9328a68bdb20b57e11",
+ "sha256:b7a00ecb1434f8183395fac5366a21ee73d14900082ca37cf74993cf46baa56c",
+ "sha256:ba32f4a91c1cb7314c429b03afbf87b1fff4fb1c8db32260e7310104bd77f0c7",
+ "sha256:cbd0f2cbd8689861209cd89141371d3a22a11613304d1f0736492590aa0ab332",
+ "sha256:e4bc372b953bf6cec65a8d48482ba574f6e051621d157cf224227dbb55486b1e",
+ "sha256:eccac3d9aadc68e994b6d228cb0c8919fc47a5350d85a1b4d3d81d1e98baf40c",
+ "sha256:efd550b3da28195746bb43bd1d815058181a7ca6d9d6aa89dd37f5eefe2cacb7",
+ "sha256:efef581c8ba4d990770875e1a2218e856849d32ada2680e53aebc5d154a17e20",
+ "sha256:f057897711a630a0b7a6a03f1acf379b6ba25d37dc5dc217a97191984ba7f2fc",
+ "sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd",
+ "sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537"
+ ],
+ "index": "pypi",
+ "version": "==5.2.0"
+ },
+ "zope.proxy": {
+ "hashes": [
+ "sha256:00573dfa755d0703ab84bb23cb6ecf97bb683c34b340d4df76651f97b0bab068",
+ "sha256:092049280f2848d2ba1b57b71fe04881762a220a97b65288bcb0968bb199ec30",
+ "sha256:0cbd27b4d3718b5ec74fc65ffa53c78d34c65c6fd9411b8352d2a4f855220cf1",
+ "sha256:17fc7e16d0c81f833a138818a30f366696653d521febc8e892858041c4d88785",
+ "sha256:19577dfeb70e8a67249ba92c8ad20589a1a2d86a8d693647fa8385408a4c17b0",
+ "sha256:207aa914576b1181597a1516e1b90599dc690c095343ae281b0772e44945e6a4",
+ "sha256:219a7db5ed53e523eb4a4769f13105118b6d5b04ed169a283c9775af221e231f",
+ "sha256:2b50ea79849e46b5f4f2b0247a3687505d32d161eeb16a75f6f7e6cd81936e43",
+ "sha256:5903d38362b6c716e66bbe470f190579c530a5baf03dbc8500e5c2357aa569a5",
+ "sha256:5c24903675e271bd688c6e9e7df5775ac6b168feb87dbe0e4bcc90805f21b28f",
+ "sha256:5ef6bc5ed98139e084f4e91100f2b098a0cd3493d4e76f9d6b3f7b95d7ad0f06",
+ "sha256:61b55ae3c23a126a788b33ffb18f37d6668e79a05e756588d9e4d4be7246ab1c",
+ "sha256:63ddb992931a5e616c87d3d89f5a58db086e617548005c7f9059fac68c03a5cc",
+ "sha256:6943da9c09870490dcfd50c4909c0cc19f434fa6948f61282dc9cb07bcf08160",
+ "sha256:6ad40f85c1207803d581d5d75e9ea25327cd524925699a83dfc03bf8e4ba72b7",
+ "sha256:6b44433a79bdd7af0e3337bd7bbcf53dd1f9b0fa66bf21bcb756060ce32a96c1",
+ "sha256:6bbaa245015d933a4172395baad7874373f162955d73612f0b66b6c2c33b6366",
+ "sha256:7007227f4ea85b40a2f5e5a244479f6a6dfcf906db9b55e812a814a8f0e2c28d",
+ "sha256:74884a0aec1f1609190ec8b34b5d58fb3b5353cf22b96161e13e0e835f13518f",
+ "sha256:7d25fe5571ddb16369054f54cdd883f23de9941476d97f2b92eb6d7d83afe22d",
+ "sha256:7e162bdc5e3baad26b2262240be7d2bab36991d85a6a556e48b9dfb402370261",
+ "sha256:814d62678dc3a30f4aa081982d830b7c342cf230ffc9d030b020cb154eeebf9e",
+ "sha256:8878a34c5313ee52e20aa50b03138af8d472bae465710fb954d133a9bfd3c38d",
+ "sha256:a66a0d94e5b081d5d695e66d6667e91e74d79e273eee95c1747717ba9cb70792",
+ "sha256:a69f5cbf4addcfdf03dda564a671040127a6b7c34cf9fe4973582e68441b63fa",
+ "sha256:b00f9f0c334d07709d3f73a7cb8ae63c6ca1a90c790a63b5e7effa666ef96021",
+ "sha256:b6ed71e4a7b4690447b626f499d978aa13197a0e592950e5d7020308f6054698",
+ "sha256:bdf5041e5851526e885af579d2f455348dba68d74f14a32781933569a327fddf",
+ "sha256:be034360dd34e62608419f86e799c97d389c10a0e677a25f236a971b2f40dac9",
+ "sha256:cc8f590a5eed30b314ae6b0232d925519ade433f663de79cc3783e4b10d662ba",
+ "sha256:cd7a318a15fe6cc4584bf3c4426f092ed08c0fd012cf2a9173114234fe193e11",
+ "sha256:cf19b5f63a59c20306e034e691402b02055c8f4e38bf6792c23cad489162a642",
+ "sha256:cfc781ce442ec407c841e9aa51d0e1024f72b6ec34caa8fdb6ef9576d549acf2",
+ "sha256:dea9f6f8633571e18bc20cad83603072e697103a567f4b0738d52dd0211b4527",
+ "sha256:e4a86a1d5eb2cce83c5972b3930c7c1eac81ab3508464345e2b8e54f119d5505",
+ "sha256:e7106374d4a74ed9ff00c46cc00f0a9f06a0775f8868e423f85d4464d2333679",
+ "sha256:e98a8a585b5668aa9e34d10f7785abf9545fe72663b4bfc16c99a115185ae6a5",
+ "sha256:f64840e68483316eb58d82c376ad3585ca995e69e33b230436de0cdddf7363f9",
+ "sha256:f8f4b0a9e6683e43889852130595c8854d8ae237f2324a053cdd884de936aa9b",
+ "sha256:fc45a53219ed30a7f670a6d8c98527af0020e6fd4ee4c0a8fb59f147f06d816c"
+ ],
+ "index": "pypi",
+ "version": "==4.3.5"
+ },
+ "zope.schema": {
+ "hashes": [
+ "sha256:9b3fc3ac656099aa9ebf3beb2bbd83d2d6ee6f94b9ac6969d6e3993ec9c4a197",
+ "sha256:a15982521241c660bf287a7e86b06df7131db00e40cee7365a2d5eadf2d051a6"
+ ],
+ "index": "pypi",
+ "version": "==6.0.1"
+ }
+ },
+ "develop": {
+ "attrs": {
+ "hashes": [
+ "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
+ "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
+ ],
+ "version": "==21.2.0"
+ },
+ "certifi": {
+ "hashes": [
+ "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
+ "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
+ ],
+ "index": "pypi",
+ "version": "==2020.12.5"
+ },
+ "charset-normalizer": {
+ "hashes": [
+ "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6",
+ "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"
+ ],
+ "markers": "python_version >= '3'",
+ "version": "==2.0.6"
+ },
+ "coverage": {
+ "hashes": [
+ "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c",
+ "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6",
+ "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45",
+ "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a",
+ "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03",
+ "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529",
+ "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a",
+ "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a",
+ "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2",
+ "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6",
+ "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759",
+ "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53",
+ "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a",
+ "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4",
+ "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff",
+ "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502",
+ "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793",
+ "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb",
+ "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905",
+ "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821",
+ "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b",
+ "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81",
+ "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0",
+ "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b",
+ "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3",
+ "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184",
+ "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701",
+ "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a",
+ "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82",
+ "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638",
+ "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5",
+ "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083",
+ "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6",
+ "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90",
+ "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465",
+ "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a",
+ "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3",
+ "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e",
+ "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066",
+ "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf",
+ "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b",
+ "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae",
+ "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669",
+ "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873",
+ "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b",
+ "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6",
+ "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb",
+ "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160",
+ "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c",
+ "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079",
+ "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d",
+ "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"
+ ],
+ "version": "==5.5"
+ },
+ "coveralls": {
+ "hashes": [
+ "sha256:15a987d9df877fff44cd81948c5806ffb6eafb757b3443f737888358e96156ee",
+ "sha256:aedfcc5296b788ebaf8ace8029376e5f102f67c53d1373f2e821515c15b36527"
+ ],
+ "index": "pypi",
+ "version": "==3.2.0"
+ },
+ "django-coverage-plugin": {
+ "hashes": [
+ "sha256:5a7ac412528876563a45f9b54ad9962e33e5f95b409843c4c6c92cb0247eee66"
+ ],
+ "index": "pypi",
+ "version": "==2.0.0"
+ },
+ "docopt": {
+ "hashes": [
+ "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
+ ],
+ "version": "==0.6.2"
+ },
+ "execnet": {
+ "hashes": [
+ "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5",
+ "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"
+ ],
+ "version": "==1.9.0"
+ },
+ "idna": {
+ "hashes": [
+ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
+ "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
+ ],
+ "index": "pypi",
+ "version": "==2.10"
+ },
+ "iniconfig": {
+ "hashes": [
+ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
+ "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
+ ],
+ "version": "==1.1.1"
+ },
+ "packaging": {
+ "hashes": [
+ "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
+ "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
+ ],
+ "version": "==21.0"
+ },
+ "pluggy": {
+ "hashes": [
+ "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
+ "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
+ ],
+ "version": "==1.0.0"
+ },
+ "psutil": {
+ "hashes": [
+ "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64",
+ "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131",
+ "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c",
+ "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6",
+ "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023",
+ "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df",
+ "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394",
+ "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4",
+ "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b",
+ "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2",
+ "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d",
+ "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65",
+ "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d",
+ "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef",
+ "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7",
+ "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60",
+ "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6",
+ "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8",
+ "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b",
+ "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d",
+ "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac",
+ "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935",
+ "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d",
+ "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28",
+ "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876",
+ "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0",
+ "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3",
+ "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"
+ ],
+ "index": "pypi",
+ "version": "==5.8.0"
+ },
+ "py": {
+ "hashes": [
+ "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
+ "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
+ ],
+ "version": "==1.10.0"
+ },
+ "pycodestyle": {
+ "hashes": [
+ "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
+ "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
+ ],
+ "index": "pypi",
+ "version": "==2.7.0"
+ },
+ "pyparsing": {
+ "hashes": [
+ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
+ "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
+ ],
+ "index": "pypi",
+ "version": "==2.4.7"
+ },
+ "pypom": {
+ "hashes": [
+ "sha256:4bdd57fceb72d7e6a3645cf6c9322f490d9cfb5d777eac2c851a3b658b813939",
+ "sha256:6772ec99f0a21a5bdc8c092007a8c813ed18359e67ed70258bbb233df5e28829"
+ ],
+ "index": "pypi",
+ "version": "==2.2.0"
+ },
+ "pytest": {
+ "hashes": [
+ "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
+ "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
+ ],
+ "index": "pypi",
+ "version": "==6.2.5"
+ },
+ "pytest-cov": {
+ "hashes": [
+ "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a",
+ "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"
+ ],
+ "index": "pypi",
+ "version": "==2.12.1"
+ },
+ "pytest-django": {
+ "hashes": [
+ "sha256:65783e78382456528bd9d79a35843adde9e6a47347b20464eb2c885cb0f1f606",
+ "sha256:b5171e3798bf7e3fc5ea7072fe87324db67a4dd9f1192b037fed4cc3c1b7f455"
+ ],
+ "index": "pypi",
+ "version": "==4.4.0"
+ },
+ "pytest-forked": {
+ "hashes": [
+ "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca",
+ "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815"
+ ],
+ "version": "==1.3.0"
+ },
+ "pytest-splinter": {
+ "hashes": [
+ "sha256:16d93db719bcad19342935c1707b5c3ec7e34d9ae10df683f6fc2e9e982ddb39"
+ ],
+ "index": "pypi",
+ "version": "==3.3.1"
+ },
+ "pytest-xdist": {
+ "hashes": [
+ "sha256:e8ecde2f85d88fbcadb7d28cb33da0fa29bca5cf7d5967fa89fc0e97e5299ea5",
+ "sha256:ed3d7da961070fce2a01818b51f6888327fb88df4379edeb6b9d990e789d9c8d"
+ ],
+ "index": "pypi",
+ "version": "==2.3.0"
+ },
+ "requests": {
+ "hashes": [
+ "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
+ "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
+ ],
+ "index": "pypi",
+ "version": "==2.25.1"
+ },
+ "selenium": {
+ "hashes": [
+ "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c",
+ "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"
+ ],
+ "version": "==3.141.0"
+ },
+ "six": {
+ "hashes": [
+ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
+ "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
+ ],
+ "index": "pypi",
+ "version": "==1.15.0"
+ },
+ "splinter": {
+ "hashes": [
+ "sha256:18b9d992352953cdd61ca8ece9616c9f214b47caebf8893266cb46bce60f6d60",
+ "sha256:218f4433018f9d1710a3201de7c22284273ab8bf29e5793247844fd74aafaf89",
+ "sha256:3f63f946fd96b750b4fd3c49ffb17abd6dc03d094b4836753e6be33f53fcfee1"
+ ],
+ "version": "==0.15.0"
+ },
+ "toml": {
+ "hashes": [
+ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+ "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
+ ],
+ "version": "==0.10.2"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
+ "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
+ ],
+ "index": "pypi",
+ "version": "==1.26.6"
+ },
+ "zope.component": {
+ "hashes": [
+ "sha256:607628e4c84f7887a69a958542b5c304663e726b73aba0882e3a3f059bff14f3",
+ "sha256:91628918218b3e6f6323de2a7b845e09ddc5cae131c034896c051b084bba3c92"
+ ],
+ "index": "pypi",
+ "version": "==4.6.2"
+ },
+ "zope.event": {
+ "hashes": [
+ "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42",
+ "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330"
+ ],
+ "index": "pypi",
+ "version": "==4.5.0"
+ },
+ "zope.hookable": {
+ "hashes": [
+ "sha256:0194b9b9e7f614abba60c90b231908861036578297515d3d6508eb10190f266d",
+ "sha256:0c2977473918bdefc6fa8dfb311f154e7f13c6133957fe649704deca79b92093",
+ "sha256:17b8bdb3b77e03a152ca0d5ca185a7ae0156f5e5a2dbddf538676633a1f7380f",
+ "sha256:29d07681a78042cdd15b268ae9decffed9ace68a53eebeb61d65ae931d158841",
+ "sha256:36fb1b35d1150267cb0543a1ddd950c0bc2c75ed0e6e92e3aaa6ac2e29416cb7",
+ "sha256:3aed60c2bb5e812bbf9295c70f25b17ac37c233f30447a96c67913ba5073642f",
+ "sha256:3cac1565cc768911e72ca9ec4ddf5c5109e1fef0104f19f06649cf1874943b60",
+ "sha256:3d4bc0cc4a37c3cd3081063142eeb2125511db3c13f6dc932d899c512690378e",
+ "sha256:3f73096f27b8c28be53ffb6604f7b570fbbb82f273c6febe5f58119009b59898",
+ "sha256:522d1153d93f2d48aa0bd9fb778d8d4500be2e4dcf86c3150768f0e3adbbc4ef",
+ "sha256:523d2928fb7377bbdbc9af9c0b14ad73e6eaf226349f105733bdae27efd15b5a",
+ "sha256:5848309d4fc5c02150a45e8f8d2227e5bfda386a508bbd3160fed7c633c5a2fa",
+ "sha256:6781f86e6d54a110980a76e761eb54590630fd2af2a17d7edf02a079d2646c1d",
+ "sha256:6fd27921ebf3aaa945fa25d790f1f2046204f24dba4946f82f5f0a442577c3e9",
+ "sha256:70d581862863f6bf9e175e85c9d70c2d7155f53fb04dcdb2f73cf288ca559a53",
+ "sha256:81867c23b0dc66c8366f351d00923f2bc5902820a24c2534dfd7bf01a5879963",
+ "sha256:81db29edadcbb740cd2716c95a297893a546ed89db1bfe9110168732d7f0afdd",
+ "sha256:86bd12624068cea60860a0759af5e2c3adc89c12aef6f71cf12f577e28deefe3",
+ "sha256:9c184d8f9f7a76e1ced99855ccf390ffdd0ec3765e5cbf7b9cada600accc0a1e",
+ "sha256:acc789e8c29c13555e43fe4bf9fcd15a65512c9645e97bbaa5602e3201252b02",
+ "sha256:afaa740206b7660d4cc3b8f120426c85761f51379af7a5b05451f624ad12b0af",
+ "sha256:b5f5fa323f878bb16eae68ea1ba7f6c0419d4695d0248bed4b18f51d7ce5ab85",
+ "sha256:bd89e0e2c67bf4ac3aca2a19702b1a37269fb1923827f68324ac2e7afd6e3406",
+ "sha256:c212de743283ec0735db24ec6ad913758df3af1b7217550ff270038062afd6ae",
+ "sha256:ca553f524293a0bdea05e7f44c3e685e4b7b022cb37d87bc4a3efa0f86587a8d",
+ "sha256:cab67065a3db92f636128d3157cc5424a145f82d96fb47159c539132833a6d36",
+ "sha256:d3b3b3eedfdbf6b02898216e85aa6baf50207f4378a2a6803d6d47650cd37031",
+ "sha256:d9f4a5a72f40256b686d31c5c0b1fde503172307beb12c1568296e76118e402c",
+ "sha256:df5067d87aaa111ed5d050e1ee853ba284969497f91806efd42425f5348f1c06",
+ "sha256:e2587644812c6138f05b8a41594a8337c6790e3baf9a01915e52438c13fc6bef",
+ "sha256:e27fd877662db94f897f3fd532ef211ca4901eb1a70ba456f15c0866a985464a",
+ "sha256:e427ebbdd223c72e06ba94c004bb04e996c84dec8a0fa84e837556ae145c439e",
+ "sha256:e583ad4309c203ef75a09d43434cf9c2b4fa247997ecb0dcad769982c39411c7",
+ "sha256:e760b2bc8ece9200804f0c2b64d10147ecaf18455a2a90827fbec4c9d84f3ad5",
+ "sha256:ea9a9cc8bcc70e18023f30fa2f53d11ae069572a162791224e60cd65df55fb69",
+ "sha256:ecb3f17dce4803c1099bd21742cd126b59817a4e76a6544d31d2cca6e30dbffd",
+ "sha256:ed794e3b3de42486d30444fb60b5561e724ee8a2d1b17b0c2e0f81e3ddaf7a87",
+ "sha256:ee885d347279e38226d0a437b6a932f207f691c502ee565aba27a7022f1285df",
+ "sha256:fd5e7bc5f24f7e3d490698f7b854659a9851da2187414617cd5ed360af7efd63",
+ "sha256:fe45f6870f7588ac7b2763ff1ce98cce59369717afe70cc353ec5218bc854bcc"
+ ],
+ "index": "pypi",
+ "version": "==5.0.1"
+ },
+ "zope.interface": {
+ "hashes": [
+ "sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1",
+ "sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d",
+ "sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123",
+ "sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232",
+ "sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549",
+ "sha256:1b5f6c8fff4ed32aa2dd43e84061bc8346f32d3ba6ad6e58f088fe109608f102",
+ "sha256:21e49123f375703cf824214939d39df0af62c47d122d955b2a8d9153ea08cfd5",
+ "sha256:21f579134a47083ffb5ddd1307f0405c91aa8b61ad4be6fd5af0171474fe0c45",
+ "sha256:27c267dc38a0f0079e96a2945ee65786d38ef111e413c702fbaaacbab6361d00",
+ "sha256:299bde0ab9e5c4a92f01a152b7fbabb460f31343f1416f9b7b983167ab1e33bc",
+ "sha256:2ab88d8f228f803fcb8cb7d222c579d13dab2d3622c51e8cf321280da01102a7",
+ "sha256:2ced4c35061eea623bc84c7711eedce8ecc3c2c51cd9c6afa6290df3bae9e104",
+ "sha256:2dcab01c660983ba5e5a612e0c935141ccbee67d2e2e14b833e01c2354bd8034",
+ "sha256:32546af61a9a9b141ca38d971aa6eb9800450fa6620ce6323cc30eec447861f3",
+ "sha256:32b40a4c46d199827d79c86bb8cb88b1bbb764f127876f2cb6f3a47f63dbada3",
+ "sha256:3cc94c69f6bd48ed86e8e24f358cb75095c8129827df1298518ab860115269a4",
+ "sha256:42b278ac0989d6f5cf58d7e0828ea6b5951464e3cf2ff229dd09a96cb6ba0c86",
+ "sha256:495b63fd0302f282ee6c1e6ea0f1c12cb3d1a49c8292d27287f01845ff252a96",
+ "sha256:4af87cdc0d4b14e600e6d3d09793dce3b7171348a094ba818e2a68ae7ee67546",
+ "sha256:4b94df9f2fdde7b9314321bab8448e6ad5a23b80542dcab53e329527d4099dcb",
+ "sha256:4c48ddb63e2b20fba4c6a2bf81b4d49e99b6d4587fb67a6cd33a2c1f003af3e3",
+ "sha256:4df9afd17bd5477e9f8c8b6bb8507e18dd0f8b4efe73bb99729ff203279e9e3b",
+ "sha256:518950fe6a5d56f94ba125107895f938a4f34f704c658986eae8255edb41163b",
+ "sha256:538298e4e113ccb8b41658d5a4b605bebe75e46a30ceca22a5a289cf02c80bec",
+ "sha256:55465121e72e208a7b69b53de791402affe6165083b2ea71b892728bd19ba9ae",
+ "sha256:588384d70a0f19b47409cfdb10e0c27c20e4293b74fc891df3d8eb47782b8b3e",
+ "sha256:6278c080d4afffc9016e14325f8734456831124e8c12caa754fd544435c08386",
+ "sha256:64ea6c221aeee4796860405e1aedec63424cda4202a7ad27a5066876db5b0fd2",
+ "sha256:681dbb33e2b40262b33fd383bae63c36d33fd79fa1a8e4092945430744ffd34a",
+ "sha256:6936aa9da390402d646a32a6a38d5409c2d2afb2950f045a7d02ab25a4e7d08d",
+ "sha256:778d0ec38bbd288b150a3ae363c8ffd88d2207a756842495e9bffd8a8afbc89a",
+ "sha256:8251f06a77985a2729a8bdbefbae79ee78567dddc3acbd499b87e705ca59fe24",
+ "sha256:83b4aa5344cce005a9cff5d0321b2e318e871cc1dfc793b66c32dd4f59e9770d",
+ "sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b",
+ "sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50",
+ "sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523",
+ "sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a",
+ "sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095",
+ "sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a",
+ "sha256:abb61afd84f23099ac6099d804cdba9bd3b902aaaded3ffff47e490b0a495520",
+ "sha256:adf9ee115ae8ff8b6da4b854b4152f253b390ba64407a22d75456fe07dcbda65",
+ "sha256:aedc6c672b351afe6dfe17ff83ee5e7eb6ed44718f879a9328a68bdb20b57e11",
+ "sha256:b7a00ecb1434f8183395fac5366a21ee73d14900082ca37cf74993cf46baa56c",
+ "sha256:ba32f4a91c1cb7314c429b03afbf87b1fff4fb1c8db32260e7310104bd77f0c7",
+ "sha256:cbd0f2cbd8689861209cd89141371d3a22a11613304d1f0736492590aa0ab332",
+ "sha256:e4bc372b953bf6cec65a8d48482ba574f6e051621d157cf224227dbb55486b1e",
+ "sha256:eccac3d9aadc68e994b6d228cb0c8919fc47a5350d85a1b4d3d81d1e98baf40c",
+ "sha256:efd550b3da28195746bb43bd1d815058181a7ca6d9d6aa89dd37f5eefe2cacb7",
+ "sha256:efef581c8ba4d990770875e1a2218e856849d32ada2680e53aebc5d154a17e20",
+ "sha256:f057897711a630a0b7a6a03f1acf379b6ba25d37dc5dc217a97191984ba7f2fc",
+ "sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd",
+ "sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537"
+ ],
+ "index": "pypi",
+ "version": "==5.2.0"
+ }
+ }
+}
diff --git a/PyRIGS/decorators.py b/PyRIGS/decorators.py
index 6d48e5e1..df942838 100644
--- a/PyRIGS/decorators.py
+++ b/PyRIGS/decorators.py
@@ -1,6 +1,7 @@
+from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
-from django.shortcuts import render
from django.http import HttpResponseRedirect
+from django.shortcuts import render
from django.urls import reverse
from RIGS import models
@@ -8,17 +9,14 @@ from RIGS import models
def get_oembed(login_url, request, oembed_view, kwargs):
context = {}
- context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
+ context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'],
+ reverse(oembed_view, kwargs=kwargs))
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
resp = render(request, 'login_redirect.html', context=context)
return resp
-def has_oembed(oembed_view, login_url=None):
- if not login_url:
- from django.conf import settings
- login_url = settings.LOGIN_URL
-
+def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
def _dec(view_func):
def _checklogin(request, *args, **kwargs):
if request.user.is_authenticated:
@@ -28,9 +26,11 @@ def has_oembed(oembed_view, login_url=None):
return get_oembed(login_url, request, oembed_view, kwargs)
else:
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
+
_checklogin.__doc__ = view_func.__doc__
_checklogin.__dict__ = view_func.__dict__
return _checklogin
+
return _dec
@@ -60,9 +60,11 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
resp = render(request, '403.html')
resp.status_code = 403
return resp
+
_checklogin.__doc__ = view_func.__doc__
_checklogin.__dict__ = view_func.__dict__
return _checklogin
+
return _dec
@@ -80,6 +82,7 @@ def api_key_required(function):
Failed users will be given a 403 error.
Should only be used for urls which include and kwargs
"""
+
def wrap(request, *args, **kwargs):
userid = kwargs.get('api_pk')
@@ -101,6 +104,7 @@ def api_key_required(function):
if user_object.api_key != key:
return error_resp
return function(request, *args, **kwargs)
+
return wrap
@@ -108,11 +112,13 @@ def nottinghamtec_address_required(function):
"""
Checks that the current user has an email address ending @nottinghamtec.co.uk
"""
+
def wrap(request, *args, **kwargs):
# Fail if current user's email address isn't @nottinghamtec.co.uk
if not request.user.email.endswith('@nottinghamtec.co.uk'):
- error_resp = render(request, 'RIGS/eventauthorisation_request_error.html')
+ error_resp = render(request, 'eventauthorisation_request_error.html')
return error_resp
return function(request, *args, **kwargs)
+
return wrap
diff --git a/PyRIGS/formats/en/formats.py b/PyRIGS/formats/en/formats.py
index 201335b9..0c2f21d6 100644
--- a/PyRIGS/formats/en/formats.py
+++ b/PyRIGS/formats/en/formats.py
@@ -1,5 +1,3 @@
-
-
DATETIME_FORMAT = ('d/m/Y H:i')
DATE_FORMAT = ('d/m/Y')
TIME_FORMAT = ('H:i')
diff --git a/RIGS/static/css/ie.css b/PyRIGS/forms.py
similarity index 100%
rename from RIGS/static/css/ie.css
rename to PyRIGS/forms.py
diff --git a/PyRIGS/management/__init__.py b/PyRIGS/management/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/PyRIGS/management/commands/__init__.py b/PyRIGS/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/PyRIGS/settings.py b/PyRIGS/settings.py
index 5877ad74..d0f655a4 100644
--- a/PyRIGS/settings.py
+++ b/PyRIGS/settings.py
@@ -8,25 +8,23 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/
"""
-# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
-import os
-import raven
+import datetime
+from pathlib import Path
import secrets
-BASE_DIR = os.path.dirname(os.path.dirname(__file__))
+import sentry_sdk
+from sentry_sdk.integrations.django import DjangoIntegration
+from envparse import env
-# Quick-start development settings - unsuitable for production
-# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get(
- 'SECRET_KEY') else 'gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e'
-
+SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e')
# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = bool(int(os.environ.get('DEBUG'))) if os.environ.get('DEBUG') else True
-
-
-STAGING = bool(int(os.environ.get('STAGING'))) if os.environ.get('STAGING') else False
+DEBUG = env('DEBUG', cast=bool, default=True)
+STAGING = env('STAGING', cast=bool, default=False)
+CI = env('CI', cast=bool, default=False)
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
@@ -44,33 +42,36 @@ if not DEBUG:
INTERNAL_IPS = ['127.0.0.1']
-ADMINS = (
- ('Tom Price', 'tomtom5152@gmail.com')
-)
+ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
+ ('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
+if DEBUG:
+ ADMINS.append(('Testing Superuser', 'superuser@example.com'))
# Application definition
-
INSTALLED_APPS = (
+ 'whitenoise.runserver_nostatic',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ 'django.contrib.humanize',
+ 'versioning',
+ 'users',
'RIGS',
'assets',
'debug_toolbar',
'registration',
'reversion',
- 'captcha',
'widget_tweaks',
- 'raven.contrib.django.raven_compat',
+ 'hcaptcha',
)
MIDDLEWARE = (
- 'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
'django.middleware.security.SecurityMiddleware',
+ 'whitenoise.middleware.WhiteNoiseMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
@@ -79,6 +80,8 @@ MIDDLEWARE = (
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'htmlmin.middleware.HtmlMinifyMiddleware',
+ 'htmlmin.middleware.MarkRequestMiddleware',
)
ROOT_URLCONF = 'PyRIGS.urls'
@@ -86,11 +89,10 @@ ROOT_URLCONF = 'PyRIGS.urls'
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
# Database
-# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ 'NAME': str(BASE_DIR / 'db.sqlite3'),
}
}
@@ -147,12 +149,33 @@ LOGGING = {
}
}
-RAVEN_CONFIG = {
- 'dsn': os.environ.get('RAVEN_DSN'),
- # If you are using git, you can also automatically configure the
- # release based on the git info.
- # 'release': raven.fetch_git_sha(os.path.dirname(os.path.dirname(__file__))),
-}
+# Tests lock up SQLite otherwise
+if STAGING or CI:
+ CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
+ }
+ }
+elif DEBUG:
+ CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
+ }
+ }
+else:
+ CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
+ 'LOCATION': 'cache_table',
+ }
+ }
+
+# Error/performance monitoring
+sentry_sdk.init(
+ dsn=env('SENTRY_DSN', default=""),
+ integrations=[DjangoIntegration()],
+ traces_sample_rate=1.0,
+)
# User system
AUTH_USER_MODEL = 'RIGS.Profile'
@@ -163,25 +186,30 @@ LOGOUT_URL = '/user/logout/'
ACCOUNT_ACTIVATION_DAYS = 7
-# reCAPTCHA settings
-RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI") # If not set, use development key
-RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe") # If not set, use development key
-NOCAPTCHA = True
+# CAPTCHA settings
+if DEBUG or CI:
+ HCAPTCHA_SITEKEY = '10000000-ffff-ffff-ffff-000000000001'
+ HCAPTCHA_SECRET = '0x0000000000000000000000000000000000000000'
+else:
+ HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY')
+ HCAPTCHA_SECRET = env('HCAPTCHA_SECRET')
# Email
EMAILER_TEST = False
if not DEBUG or EMAILER_TEST:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
- EMAIL_HOST = os.environ.get('EMAIL_HOST')
- EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 25))
- EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
- EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
- EMAIL_USE_TLS = bool(int(os.environ.get('EMAIL_USE_TLS', 0)))
- EMAIL_USE_SSL = bool(int(os.environ.get('EMAIL_USE_SSL', 0)))
- DEFAULT_FROM_EMAIL = os.environ.get('EMAIL_FROM')
+ EMAIL_HOST = env('EMAIL_HOST')
+ EMAIL_PORT = env('EMAIL_PORT', cast=int, default=25)
+ EMAIL_HOST_USER = env('EMAIL_HOST_USER')
+ EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
+ EMAIL_USE_TLS = env('EMAIL_USE_TLS', cast=bool, default=False)
+ EMAIL_USE_SSL = env('EMAIL_USE_SSL', cast=bool, default=False)
+ DEFAULT_FROM_EMAIL = env('EMAIL_FROM')
else:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+EMAIL_COOLDOWN = datetime.timedelta(minutes=15)
+
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
@@ -197,22 +225,22 @@ USE_L10N = 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')
# Static files (CSS, JavaScript, Images)
-# https://docs.djangoproject.com/en/1.7/howto/static-files/
-STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
+STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
STATIC_URL = '/static/'
-STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
-STATIC_DIRS = (
- os.path.join(BASE_DIR, 'static/')
-)
+STATIC_ROOT = str(BASE_DIR / 'static/')
+STATICFILES_DIRS = [
+ str(BASE_DIR / 'pipeline/built_assets'),
+]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
- os.path.join(BASE_DIR, 'templates'),
+ BASE_DIR / 'templates'
],
'APP_DIRS': True,
'OPTIONS': {
@@ -235,7 +263,3 @@ USE_GRAVATAR = True
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
-RISK_ASSESSMENT_URL = os.environ.get('RISK_ASSESSMENT_URL') if os.environ.get(
- 'RISK_ASSESSMENT_URL') else "http://example.com"
-RISK_ASSESSMENT_SECRET = os.environ.get('RISK_ASSESSMENT_SECRET') if os.environ.get(
- 'RISK_ASSESSMENT_SECRET') else secrets.token_hex(15)
diff --git a/PyRIGS/tests/__init__.py b/PyRIGS/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/PyRIGS/tests/base.py b/PyRIGS/tests/base.py
index ecdacd21..d1669d5e 100644
--- a/PyRIGS/tests/base.py
+++ b/PyRIGS/tests/base.py
@@ -1,17 +1,32 @@
+import os
+import pathlib
+import sys
+from datetime import datetime
+
+import pytz
+from django.conf import settings
from django.test import LiveServerTestCase
from selenium import webdriver
+from selenium.webdriver.support.wait import WebDriverWait
+
from RIGS import models as rigsmodels
from . import pages
-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():
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
- if os.environ.get('CI', False):
- options.add_argument("--headless")
+ options.add_argument("--headless")
+ if settings.CI:
options.add_argument("--no-sandbox")
- driver = webdriver.Chrome(chrome_options=options)
+ driver = webdriver.Chrome(options=options)
return driver
@@ -19,6 +34,7 @@ class BaseTest(LiveServerTestCase):
def setUp(self):
super().setUpClass()
self.driver = create_browser()
+ self.wait = WebDriverWait(self.driver, 15)
def tearDown(self):
super().tearDown()
@@ -32,5 +48,58 @@ class AutoLoginTest(BaseTest):
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
self.profile.set_password("EventTestPassword")
self.profile.save()
- loginPage = pages.LoginPage(self.driver, self.live_server_url).open()
- loginPage.login("EventTest", "EventTestPassword")
+ login_page = pages.LoginPage(self.driver, self.live_server_url).open()
+ login_page.login("EventTest", "EventTestPassword")
+
+
+# FIXME Refactor as a pytest fixture
+def screenshot_failure(func):
+ def wrapper_func(self, *args, **kwargs):
+ try:
+ func(self, *args, **kwargs)
+ except Exception as e:
+ screenshot_name = func.__module__ + "." + func.__qualname__
+ screenshot_file = "screenshots/" + func.__qualname__ + ".png"
+ if not pathlib.Path("screenshots").is_dir():
+ os.mkdir("screenshots")
+ self.driver.save_screenshot(screenshot_file)
+ print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
+ raise e
+
+ return wrapper_func
+
+
+def screenshot_failure_cls(cls):
+ for attr in cls.__dict__:
+ if callable(getattr(cls, attr)) and attr.startswith("test"):
+ setattr(cls, attr, screenshot_failure(getattr(cls, attr)))
+ return cls
+
+
+def assert_times_almost_equal(first_time, second_time):
+ assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0)
+
+
+def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url):
+ # Test the meta tag is in place
+ response = client.get(event_url, follow=True, HTTP_HOST='example.com')
+ assertContains(response, 'application/json+oembed')
+ assertContains(response, oembed_url)
+ # Test that the JSON exists
+ response = client.get(oembed_url, follow=True, HTTP_HOST='example.com')
+ assert response.status_code == 200
+ assertContains(response, event_embed_url)
+ # Should also work for non-existant events
+ response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
+ assert response.status_code == 200
+ assertContains(response, alt_event_embed_url)
+
+
+def login(client, django_user_model):
+ pwd = 'testuser'
+ usr = 'TestUser'
+ user = django_user_model.objects.create_user(username=usr, email="TestUser@test.com", password=pwd,
+ is_superuser=True,
+ is_active=True, is_staff=True)
+ assert client.login(username=usr, password=pwd)
+ return user
diff --git a/PyRIGS/tests/pages.py b/PyRIGS/tests/pages.py
index 4bf34a6d..dec5e20e 100644
--- a/PyRIGS/tests/pages.py
+++ b/PyRIGS/tests/pages.py
@@ -1,7 +1,9 @@
-from pypom import Page, Region
-from selenium.webdriver.common.by import By
-from selenium.webdriver import Chrome
+from pypom import Page
from selenium.common.exceptions import NoSuchElementException
+from selenium.webdriver.common.action_chains import ActionChains
+from selenium.webdriver.common.by import By
+
+from PyRIGS.tests import regions
class BasePage(Page):
@@ -29,42 +31,30 @@ class BasePage(Page):
class FormPage(BasePage):
_errors_selector = (By.CLASS_NAME, "alert-danger")
+ _submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
def remove_all_required(self):
- self.driver.execute_script("Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
- self.driver.execute_script("Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
+ self.driver.execute_script(
+ "Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
+ self.driver.execute_script(
+ "Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
+
+ def submit(self):
+ previous_errors = self.errors
+ submit = self.find_element(*self._submit_locator)
+ ActionChains(self.driver).move_to_element(submit).perform()
+ submit.click()
+ self.wait.until(animation_is_finished())
+ self.wait.until(lambda x: self.errors != previous_errors or self.success)
@property
def errors(self):
try:
- error_page = self.ErrorPage(self, self.find_element(*self._errors_selector))
+ error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector))
return error_page.errors
except NoSuchElementException:
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):
URL_TEMPLATE = '/user/login'
@@ -84,3 +74,13 @@ class LoginPage(BasePage):
password_element.send_keys(password)
self.find_element(*self._submit_locator).click()
+
+
+class animation_is_finished():
+ def __call__(self, driver):
+ number_animating = driver.execute_script('return $(":animated").length')
+ finished = number_animating == 0
+ if finished:
+ import time
+ time.sleep(0.1)
+ return finished
diff --git a/PyRIGS/tests/regions.py b/PyRIGS/tests/regions.py
index 5dd364ff..6daef657 100644
--- a/PyRIGS/tests/regions.py
+++ b/PyRIGS/tests/regions.py
@@ -1,11 +1,13 @@
-from pypom import Region
-from selenium.webdriver.common.by import By
-from selenium.webdriver.support import expected_conditions
-from selenium.webdriver.remote.webelement import WebElement
-from selenium.webdriver.support.ui import WebDriverWait
-from selenium.webdriver.support.select import Select
import datetime
+from django.conf import settings
+from pypom import Region
+from selenium.common.exceptions import NoSuchElementException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.common.keys import Keys
+from selenium.webdriver.support import expected_conditions
+from selenium.webdriver.support.select import Select
+
def parse_bool_from_string(string):
# Used to convert from attribute strings to boolean values, written after I found this:
@@ -17,10 +19,25 @@ def parse_bool_from_string(string):
return False
+def get_time_format():
+ # Default
+ time_format = "%H%M"
+ if settings.CI: # The CI is American
+ time_format = "%I%M%p"
+ return time_format
+
+
+def get_date_format():
+ date_format = "%d%m%Y"
+ if settings.CI: # And try as I might I can't stop it being so
+ date_format = "%m%d%Y"
+ return date_format
+
+
class BootstrapSelectElement(Region):
_main_button_locator = (By.CSS_SELECTOR, 'button.dropdown-toggle')
_option_box_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu')
- _option_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu.inner>li>a[role=option]')
+ _option_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu.inner>li>a.dropdown-item')
_select_all_locator = (By.CLASS_NAME, 'bs-select-all')
_deselect_all_locator = (By.CLASS_NAME, 'bs-deselect-all')
_search_locator = (By.CSS_SELECTOR, '.bs-searchbox>input')
@@ -32,12 +49,12 @@ class BootstrapSelectElement(Region):
def toggle(self):
original_state = self.is_open
- 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))
+ if not original_state:
+ self.wait.until(expected_conditions.invisibility_of_element(option_box))
else:
- self.wait.until(expected_conditions.visibility_of_element_located(option_box))
+ self.wait.until(expected_conditions.visibility_of(option_box))
+ return self.find_element(*self._main_button_locator).click()
def open(self):
if not self.is_open:
@@ -54,10 +71,11 @@ class BootstrapSelectElement(Region):
self.find_element(*self._deselect_all_locator).click()
def search(self, query):
+ # self.wait.until(expected_conditions.visibility_of_element_located(self._status_locator))
search_box = self.find_element(*self._search_locator)
+ self.open()
search_box.clear()
search_box.send_keys(query)
- status_text = self.find_element(*self._status_locator)
self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator))
@property
@@ -112,6 +130,22 @@ class CheckBox(Region):
self.toggle()
+class RadioSelect(Region): # Currently only works for yes/no radio selects
+ def set_value(self, value):
+ if value:
+ value = "0"
+ else:
+ value = "1"
+ self.find_element(By.XPATH, "//label[@for='{}_{}']".format(self.root.get_attribute("id"), value)).click()
+
+ @property
+ def value(self):
+ try:
+ return parse_bool_from_string(self.find_element(By.CSS_SELECTOR, '.custom-control-input:checked').get_attribute("value").lower())
+ except NoSuchElementException:
+ return None
+
+
class DatePicker(Region):
@property
def value(self):
@@ -119,7 +153,33 @@ class DatePicker(Region):
def set_value(self, value):
self.root.clear()
- self.root.send_keys(value.strftime("%d%m%Y"))
+ self.root.send_keys(value.strftime(get_date_format()))
+
+
+class TimePicker(Region):
+ @property
+ def value(self):
+ return datetime.datetime.strptime(self.root.get_attribute("value"), "%H:%M")
+
+ def set_value(self, value):
+ self.root.clear()
+ self.root.send_keys(value.strftime(get_time_format()))
+
+
+class DateTimePicker(Region):
+ @property
+ def value(self):
+ return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d %H:%M")
+
+ def set_value(self, value):
+ self.root.clear()
+
+ date = value.date().strftime(get_date_format())
+ time = value.time().strftime(get_time_format())
+
+ self.root.send_keys(date)
+ self.root.send_keys(Keys.TAB)
+ self.root.send_keys(time)
class SingleSelectPicker(Region):
@@ -131,3 +191,63 @@ class SingleSelectPicker(Region):
def set_value(self, value):
picker = Select(self.root)
picker.select_by_visible_text(value)
+
+
+class ErrorPage(Region):
+ _error_item_selector = (By.CSS_SELECTOR, "dl>span")
+
+ class ErrorItem(Region):
+ _field_selector = (By.CSS_SELECTOR, "dt")
+ _error_selector = (By.CSS_SELECTOR, "dd>ul>li")
+
+ @property
+ def field_name(self):
+ return self.find_element(*self._field_selector).text
+
+ @property
+ def errors(self):
+ return [x.text for x in self.find_elements(*self._error_selector)]
+
+ @property
+ def errors(self):
+ error_items = [self.ErrorItem(self, x) for x in self.find_elements(*self._error_item_selector)]
+ errors = {}
+ for error in error_items:
+ errors[error.field_name] = error.errors
+ return errors
+
+
+class Modal(Region):
+ _submit_locator = (By.CSS_SELECTOR, '.btn-primary')
+ _header_selector = (By.TAG_NAME, 'h4')
+
+ form_items = {
+ 'name': (TextBox, (By.ID, 'id_name'))
+ }
+
+ @property
+ def header(self):
+ return self.find_element(*self._header_selector).text
+
+ @property
+ def is_open(self):
+ return self.root.is_displayed()
+
+ def submit(self):
+ self.root.find_element(*self._submit_locator).click()
+
+ def __getattr__(self, name):
+ if name in self.form_items:
+ element = self.form_items[name]
+ form_element = element[0](self, self.find_element(*element[1]))
+ return form_element.value
+ else:
+ return super().__getattribute__(name)
+
+ def __setattr__(self, name, value):
+ if name in self.form_items:
+ element = self.form_items[name]
+ form_element = element[0](self, self.find_element(*element[1]))
+ form_element.set_value(value)
+ else:
+ self.__dict__[name] = value
diff --git a/PyRIGS/tests/test_unit.py b/PyRIGS/tests/test_unit.py
new file mode 100644
index 00000000..dd03fbd8
--- /dev/null
+++ b/PyRIGS/tests/test_unit.py
@@ -0,0 +1,145 @@
+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
+from assets.models import Asset
+from django.db import connection
+import pytest
+from django.core.management import call_command
+from django.template.defaultfilters import striptags
+from django.urls.exceptions import NoReverseMatch
+
+from RIGS.models import Event
+from assets.models import Asset
+from django.db import connection
+from django.test import TestCase
+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'])
+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
+
+
+class TestSampleDataGenerator(TestCase):
+ @override_settings(DEBUG=True)
+ def setUp(self):
+ call_command('generateSampleData')
+
+ def test_unauthenticated(self): # Nothing should be available to the unauthenticated
+ 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 = self.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)
+
+ def test_page_titles(self):
+ assert self.client.login(username='superuser', password='superuser')
+ for url in filter((lambda u: "embed" not in u.name), find_urls_recursive(urls.urlpatterns)):
+ request_url = get_request_url(url)
+ response = self.client.get(request_url)
+ if hasattr(response, "context_data") and "page_title" in response.context_data:
+ expected_title = striptags(response.context_data["page_title"])
+ assertInHTML('{} | Rig Information Gathering System'.format(expected_title),
+ response.content.decode())
+ print("{} | {}".format(request_url, expected_title)) # If test fails, tell me where!
+ self.client.logout()
+
+ def test_basic_access(self):
+ assert self.client.login(username="basic", password="basic")
+
+ url = reverse('asset_list')
+ response = self.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 = self.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 = self.client.get(request_url, follow=True)
+ assert response.status_code == 403
+
+ request_url = reverse('supplier_create')
+ response = self.client.get(request_url, follow=True)
+ assert response.status_code == 403
+
+ request_url = reverse('supplier_update', kwargs={'pk': 1})
+ response = self.client.get(request_url, follow=True)
+ assert response.status_code == 403
+ self.client.logout()
+
+ def test_keyholder_access(self):
+ assert self.client.login(username="keyholder", password="keyholder")
+
+ url = reverse('asset_list')
+ response = self.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 = self.client.get(url)
+ assertContains(response, 'Purchase Details')
+ assertContains(response, 'View Revision History')
+ self.client.logout()
diff --git a/PyRIGS/urls.py b/PyRIGS/urls.py
index cb78130c..da12b768 100644
--- a/PyRIGS/urls.py
+++ b/PyRIGS/urls.py
@@ -1,30 +1,40 @@
-from django.conf.urls import include, url
-from django.contrib import admin
-from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.conf import settings
-from registration.backends.default.views import RegistrationView
-import RIGS
-from RIGS import regbackend
+from django.conf.urls import include
+from django.contrib import admin
+from django.contrib.auth.decorators import login_required
+from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+from django.urls import path
+from django.views.generic import TemplateView
+
+from PyRIGS import views
urlpatterns = [
- # Examples:
- # url(r'^$', 'PyRIGS.views.home', name='home'),
- # url(r'^blog/', include('blog.urls')),
+ path('', include('versioning.urls')),
+ path('', include('RIGS.urls')),
+ path('assets/', include('assets.urls')),
- 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')),
+ path('', login_required(views.Index.as_view()), name='index'),
- url(r'^admin/', admin.site.urls),
+ # API
+ path('api//', login_required(views.SecureAPIRequest.as_view()),
+ name="api_secure"),
+ path('api///', login_required(views.SecureAPIRequest.as_view()),
+ name="api_secure"),
+
+ path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
+ path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'),
+
+ path('', include('users.urls')),
+
+ path('admin/', admin.site.urls),
+ path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
]
if settings.DEBUG:
urlpatterns += staticfiles_urlpatterns()
import debug_toolbar
- urlpatterns = [
- url(r'^__debug__/', include(debug_toolbar.urls)),
- ] + urlpatterns
+ urlpatterns += [
+ path('__debug__/', include(debug_toolbar.urls)),
+ path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
+ ]
diff --git a/PyRIGS/views.py b/PyRIGS/views.py
new file mode 100644
index 00000000..0216b104
--- /dev/null
+++ b/PyRIGS/views.py
@@ -0,0 +1,258 @@
+import datetime
+import operator
+from functools import reduce
+
+import simplejson
+from django.contrib.auth.decorators import login_required
+from django.contrib import messages
+from django.core import serializers
+from django.core.exceptions import PermissionDenied
+from django.db.models import Q
+from django.http import HttpResponse
+from django.shortcuts import get_object_or_404
+from django.urls import reverse_lazy, reverse, NoReverseMatch
+from django.views import generic
+from django.views.decorators.clickjacking import xframe_options_exempt
+
+from RIGS import models
+from assets import models as asset_models
+
+
+def is_ajax(request):
+ return request.headers.get('x-requested-with') == 'XMLHttpRequest'
+
+
+class Index(generic.TemplateView): # Displays the current rig count along with a few other bits and pieces
+ template_name = 'index.html'
+
+ def get_context_data(self, **kwargs):
+ context = super(Index, self).get_context_data(**kwargs)
+ context['rig_count'] = models.Event.objects.rig_count()
+ return context
+
+
+class SecureAPIRequest(generic.View):
+ models = {
+ 'venue': models.Venue,
+ 'person': models.Person,
+ 'organisation': models.Organisation,
+ 'profile': models.Profile,
+ 'event': models.Event,
+ 'supplier': asset_models.Supplier
+ }
+
+ perms = {
+ 'venue': 'RIGS.view_venue',
+ 'person': 'RIGS.view_person',
+ 'organisation': 'RIGS.view_organisation',
+ 'profile': 'RIGS.view_profile',
+ 'event': None,
+ 'supplier': 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(",")
+
+ # 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))
+
+ # Build the data response list
+ results = []
+ query = reduce(operator.and_, queries)
+ objects = self.models[model].objects.filter(query)
+ for o in objects:
+ data = {
+ 'pk': o.pk,
+ 'value': o.pk,
+ 'text': o.name,
+ }
+ try: # See if there is a valid update URL
+ data['update'] = reverse("%s_update" % model, kwargs={'pk': o.pk})
+ except NoReverseMatch:
+ pass
+ results.append(data)
+
+ # return a data response
+ json = simplejson.dumps(results)
+ return HttpResponse(json, content_type="application/json") # Always json
+
+ start = request.GET.get('start', None)
+ end = request.GET.get('end', None)
+
+ if model == "event" and start and end:
+ # Probably a calendar request
+ start_datetime = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S")
+ end_datetime = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S")
+
+ objects = self.models[model].objects.events_in_bounds(start_datetime, end_datetime)
+
+ results = []
+ for item in objects:
+ data = {
+ 'pk': item.pk,
+ 'title': item.name,
+ 'is_rig': item.is_rig,
+ 'status': str(item.get_status_display()),
+ 'earliest': item.earliest_time.isoformat(),
+ 'latest': item.latest_time.isoformat(),
+ 'url': str(item.get_absolute_url())
+ }
+
+ results.append(data)
+ json = simplejson.dumps(results)
+ return HttpResponse(json, content_type="application/json") # Always json
+
+ return HttpResponse(model)
+
+
+class ModalURLMixin:
+ def get_close_url(self, update, detail):
+ if is_ajax(self.request):
+ url = reverse_lazy('closemodal')
+ update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
+ messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
+ messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
+ else:
+ url = reverse_lazy(detail, kwargs={
+ 'pk': self.object.pk,
+ })
+ return url
+
+
+class GenericListView(generic.ListView):
+ template_name = 'generic_list.html'
+ paginate_by = 20
+
+ def get_context_data(self, **kwargs):
+ context = super(GenericListView, self).get_context_data(**kwargs)
+ context['page_title'] = self.model.__name__ + "s"
+ if is_ajax(self.request):
+ context['override'] = "base_ajax.html"
+ return context
+
+ def get_queryset(self):
+ q = self.request.GET.get('q', "")
+
+ filter = Q(name__icontains=q) | Q(email__icontains=q) | Q(address__icontains=q) | Q(notes__icontains=q) | Q(
+ phone__startswith=q) | Q(phone__endswith=q)
+
+ # try and parse an int
+ try:
+ val = int(q)
+ filter = filter | Q(pk=val)
+ except: # noqa
+ # not an integer
+ pass
+
+ object_list = self.model.objects.filter(filter)
+
+ orderBy = self.request.GET.get('orderBy', "name")
+ if orderBy != "":
+ object_list = object_list.order_by(orderBy)
+ return object_list
+
+
+class GenericDetailView(generic.DetailView):
+ template_name = "generic_detail.html"
+
+ def get_context_data(self, **kwargs):
+ context = super(GenericDetailView, self).get_context_data(**kwargs)
+ context['page_title'] = "{} | {}".format(self.model.__name__, self.object.name)
+ if is_ajax(self.request):
+ context['override'] = "base_ajax.html"
+ return context
+
+
+class GenericUpdateView(generic.UpdateView):
+ template_name = "generic_form.html"
+
+ def get_context_data(self, **kwargs):
+ context = super(GenericUpdateView, self).get_context_data(**kwargs)
+ context['page_title'] = "Edit {}".format(self.model.__name__)
+ if is_ajax(self.request):
+ context['override'] = "base_ajax.html"
+ return context
+
+
+class GenericCreateView(generic.CreateView):
+ template_name = "generic_form.html"
+
+ def get_context_data(self, **kwargs):
+ context = super(GenericCreateView, self).get_context_data(**kwargs)
+ context['page_title'] = "Create {}".format(self.model.__name__)
+ if is_ajax(self.request):
+ context['override'] = "base_ajax.html"
+ return context
+
+
+class SearchHelp(generic.TemplateView):
+ template_name = 'search_help.html'
+
+
+class CloseModal(generic.TemplateView):
+ """
+ Called from a modal window (e.g. when an item is submitted to an event/invoice).
+ May optionally also include some javascript in a success message to cause a load of
+ the new information onto the page.
+ """
+ template_name = 'closemodal.html'
+
+ def get_context_data(self, **kwargs):
+ return {'messages': messages.get_messages(self.request)}
+
+
+class OEmbedView(generic.View):
+ def get(self, request, pk=None):
+ embed_url = reverse(self.url_name, args=[pk])
+ full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
+
+ data = {
+ 'html': ''.format(full_url),
+ 'version': '1.0',
+ 'type': 'rich',
+ 'height': '250'
+ }
+
+ json = simplejson.JSONEncoderForHTML().encode(data)
+ return HttpResponse(json, content_type="application/json")
diff --git a/PyRIGS/wsgi.py b/PyRIGS/wsgi.py
index aa4cc434..ff4fa810 100644
--- a/PyRIGS/wsgi.py
+++ b/PyRIGS/wsgi.py
@@ -7,6 +7,7 @@ For more information on this file, see
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
"""
import os
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "PyRIGS.settings")
from django.core.wsgi import get_wsgi_application # noqa
diff --git a/README.md b/README.md
index 50bf51d4..cd6a443c 100644
--- a/README.md
+++ b/README.md
@@ -1,111 +1,18 @@
# TEC PA & Lighting - PyRIGS #
-[](https://travis-ci.org/nottinghamtec/PyRIGS)
-[](https://coveralls.io/github/nottinghamtec/PyRIGS)
+
+[](https://coveralls.io/github/nottinghamtec/PyRIGS)
+[](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability)
-Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails.
+Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets.
-The purpose of this project is to make the system more compatible and easier to understand such that should future changes be needed they can be made without having to understand the intricacies of Rails.
+For setup information and other such helpful stuff check the [Wiki](https://github.com/nottinghamtec/PyRIGS/wiki)
-### What is this repository for? ###
-When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production.
-
-Most of the documents here assume a basic knowledge of how Python and Django work (hint, if I don't say something, Google it, you will find 10000's of answers). The documentation is purely to be specific to TEC's application of the framework.
-
-### Editing ###
-It is recommended that you use the PyCharm IDE by JetBrains. Whilst other editors are available, this is the best for integration with Django as it can automatically manage all the pesky admin commands that frequently need running, as well as nice integration with git.
-
-For the more experienced developer/somebody who doesn't want a full IDE and wants it to open in less than the age of the universe, I can strongly recommend [Sublime Text](http://www.sublimetext.com/). It has a bit of a steeper learning curve, and won't manage anything Django/git related out of the box, but once you get the hang of it is by far the fastest and most powerful editor I have used (for any type of project).
-
-Please contact TJP for details on how to acquire these.
-
-### Python Environment ###
-Whilst the Python version used is not critical to the running of the application, using the same version usually helps avoid a lot of issues. 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
-```
-Where dir is the directory you wish to create the virtualenv in.
-
-Next activate the virtualenv.
-```
-Windows
-/Scripts/activate.bat
-
-Unix
-source /bin/activate
-```
-Finally install the requirements using pip
-```
-cd
-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
-
-```
+# Apps
+- PyRIGS: Base app, stores 'global' information
+- RIGS: Rigboard stuff - event calendar etc
+- assets: Database of our kit, testing data etc
+- versioning: Our custom logic built on top of django-reversion. Semi-modular.
+- users: Our custom logic for registration and profiles. Semi-modular.
+- training: SoonTM
[](https://forthebadge.com) [](https://forthebadge.com)
diff --git a/RIGS/admin.py b/RIGS/admin.py
index 49b8aa1e..b3d43b71 100644
--- a/RIGS/admin.py
+++ b/RIGS/admin.py
@@ -1,34 +1,42 @@
from django.contrib import admin
-from RIGS import models, forms
-from django.contrib.auth.admin import UserAdmin
-from django.utils.translation import ugettext_lazy as _
-from reversion.admin import VersionAdmin
-
-from django.contrib.admin import helpers
-from django.template.response import TemplateResponse
from django.contrib import messages
-from django.db import transaction
+from django.contrib.admin import helpers
+from django.contrib.auth.admin import UserAdmin
from django.core.exceptions import ObjectDoesNotExist
+from django.db import transaction
from django.db.models import Count
from django.forms import ModelForm
-
+from django.template.response import TemplateResponse
+from django.utils.translation import gettext_lazy as _
from reversion import revisions as reversion
+from reversion.admin import VersionAdmin
+
+from RIGS import models
+from users import forms as user_forms
# Register your models here.
admin.site.register(models.VatRate, VersionAdmin)
admin.site.register(models.Event, VersionAdmin)
admin.site.register(models.EventItem, VersionAdmin)
-admin.site.register(models.Invoice)
-admin.site.register(models.Payment)
+admin.site.register(models.Invoice, VersionAdmin)
+
+
+def approve_user(modeladmin, request, queryset):
+ queryset.update(is_approved=True)
+
+
+approve_user.short_description = "Approve selected users"
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin):
+ # Don't know how to add 'is_approved' whilst preserving the default list...
+ list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
- (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
+ (_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {
'fields': ('last_login', 'date_joined')}),
@@ -39,8 +47,9 @@ class ProfileAdmin(UserAdmin):
'fields': ('username', 'password1', 'password2'),
}),
)
- form = forms.ProfileChangeForm
- add_form = forms.ProfileCreationForm
+ form = user_forms.ProfileChangeForm
+ add_form = user_forms.ProfileCreationForm
+ actions = [approve_user]
class AssociateAdmin(VersionAdmin):
@@ -95,7 +104,7 @@ class AssociateAdmin(VersionAdmin):
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
'forms': forms
}
- return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context)
+ return TemplateResponse(request, 'admin_associate_merge.html', context)
@admin.register(models.Person)
@@ -114,3 +123,13 @@ class VenueAdmin(AssociateAdmin):
class OrganisationAdmin(AssociateAdmin):
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account']
+
+
+@admin.register(models.RiskAssessment)
+class RiskAssessmentAdmin(VersionAdmin):
+ list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
+
+
+@admin.register(models.EventChecklist)
+class EventChecklistAdmin(VersionAdmin):
+ list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
diff --git a/RIGS/finance.py b/RIGS/finance.py
index b64b2e71..dfad3c2a 100644
--- a/RIGS/finance.py
+++ b/RIGS/finance.py
@@ -1,73 +1,68 @@
import datetime
import re
+import reversion
+from django import forms
from django.contrib import messages
-from django.urls import reverse_lazy
+from django.db import transaction
+from django.db.models import Q
from django.http import Http404, HttpResponseRedirect
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
-from django.template import RequestContext
from django.template.loader import get_template
+from django.urls import reverse
from django.views import generic
-from django.db.models import Q
from z3c.rml import rml2pdf
from RIGS import models
-from django import forms
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
class InvoiceIndex(generic.ListView):
model = models.Invoice
- template_name = 'RIGS/invoice_list_active.html'
+ template_name = 'invoice_list.html'
def get_context_data(self, **kwargs):
context = super(InvoiceIndex, self).get_context_data(**kwargs)
total = 0
for i in context['object_list']:
total += i.balance
- context['total'] = total
- context['count'] = len(list(context['object_list']))
+ context['page_title'] = "Outstanding Invoices ({} Events, £{:.2f})".format(len(list(context['object_list'])), total)
+ context['description'] = "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger"
return context
def get_queryset(self):
- # Manual query is the only way I have found to do this efficiently. Not ideal but needs must
- sql = "SELECT * FROM " \
- "(SELECT " \
- "(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
- "(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
- "(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
- "\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
- "AS sub " \
- "WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
- "ORDER BY invoice_date"
-
- query = self.model.objects.raw(sql)
-
- return query
+ return self.model.objects.outstanding_invoices()
class InvoiceDetail(generic.DetailView):
model = models.Invoice
+ template_name = 'invoice_detail.html'
+
+ def get_context_data(self, **kwargs):
+ context = super(InvoiceDetail, self).get_context_data(**kwargs)
+ context['page_title'] = "Invoice {} ({}) ".format(self.object.display_id, self.object.invoice_date.strftime("%d/%m/%Y"))
+ if self.object.void:
+ context['page_title'] += "VOID"
+ elif self.object.is_closed:
+ context['page_title'] += "PAID"
+ else:
+ context['page_title'] += "OUTSTANDING"
+ return context
class InvoicePrint(generic.View):
def get(self, request, pk):
invoice = get_object_or_404(models.Invoice, pk=pk)
object = invoice.event
- template = get_template('RIGS/event_print.xml')
+ template = get_template('event_print.xml')
context = {
'object': object,
- 'fonts': {
- 'opensans': {
- 'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
- 'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
- }
- },
'invoice': invoice,
'current_user': request.user,
+ 'filename': 'Invoice {} for {} {}.pdf'.format(invoice.display_id, object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name))
}
rml = template.render(context)
@@ -76,10 +71,8 @@ class InvoicePrint(generic.View):
pdfData = buffer.read()
- escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
-
response = HttpResponse(content_type='application/pdf')
- response['Content-Disposition'] = "filename=Invoice %05d - N%05d | %s.pdf" % (invoice.pk, invoice.event.pk, escapedEventName)
+ response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
response.write(pdfData)
return response
@@ -92,25 +85,26 @@ class InvoiceVoid(generic.View):
object.save()
if object.void:
- return HttpResponseRedirect(reverse_lazy('invoice_list'))
- return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
+ return HttpResponseRedirect(reverse('invoice_list'))
+ return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': object.pk}))
class InvoiceDelete(generic.DeleteView):
model = models.Invoice
+ template_name = 'invoice_confirm_delete.html'
def get(self, request, pk):
obj = self.get_object()
if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.')
- return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
+ return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk}))
return super(InvoiceDelete, self).get(pk)
def post(self, request, pk):
obj = self.get_object()
if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.')
- return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
+ return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk}))
return super(InvoiceDelete, self).post(pk)
def get_success_url(self):
@@ -119,47 +113,67 @@ class InvoiceDelete(generic.DeleteView):
class InvoiceArchive(generic.ListView):
model = models.Invoice
- template_name = 'RIGS/invoice_list_archive.html'
+ template_name = 'invoice_list_archive.html'
paginate_by = 25
+ def get_context_data(self, **kwargs):
+ context = super(InvoiceArchive, self).get_context_data(**kwargs)
+ context['page_title'] = "Invoice Archive"
+ context['description'] = "This page displays all invoices: outstanding, paid, and void"
+ return context
+
+ def get_queryset(self):
+ q = self.request.GET.get('q', "")
+
+ filter = Q(event__name__icontains=q)
+
+ # try and parse an int
+ try:
+ val = int(q)
+ filter = filter | Q(pk=val)
+ filter = filter | Q(event__pk=val)
+ except: # noqa
+ # not an integer
+ pass
+
+ try:
+ if q[0] == "N":
+ val = int(q[1:])
+ filter = Q(event__pk=val) # If string is Nxxxxx then filter by event number
+ elif q[0] == "#":
+ val = int(q[1:])
+ filter = Q(pk=val) # If string is #xxxxx then filter by invoice number
+ except: # noqa
+ pass
+
+ object_list = self.model.objects.filter(filter).order_by('-invoice_date')
+
+ return object_list
+
class InvoiceWaiting(generic.ListView):
model = models.Event
paginate_by = 25
- template_name = 'RIGS/event_invoice.html'
+ template_name = 'invoice_list_waiting.html'
def get_context_data(self, **kwargs):
context = super(InvoiceWaiting, self).get_context_data(**kwargs)
total = 0
- for obj in self.get_objects():
+ objects = self.get_queryset()
+ for obj in objects:
total += obj.sum_total
- context['total'] = total
- context['count'] = len(self.get_objects())
+ context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(objects), total)
return context
def get_queryset(self):
- return self.get_objects()
-
- def get_objects(self):
- # @todo find a way to select items
- events = self.model.objects.filter(
- (
- Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
- Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
- ) & Q(invoice__isnull=True) & # Has not already been invoiced
- Q(is_rig=True) # Is a rig (not non-rig)
-
- ).order_by('start_date') \
- .select_related('person',
- 'organisation',
- 'venue', 'mic') \
- .prefetch_related('items')
-
- return events
+ return self.model.objects.waiting_invoices()
class InvoiceEvent(generic.View):
+ @transaction.atomic()
+ @reversion.create_revision()
def get(self, *args, **kwargs):
+ reversion.set_user(self.request.user)
epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
invoice, created = models.Invoice.objects.get_or_create(event=event)
@@ -168,12 +182,18 @@ class InvoiceEvent(generic.View):
invoice.invoice_date = datetime.date.today()
messages.success(self.request, 'Invoice created successfully')
- return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))
+ if kwargs.get('void'):
+ invoice.void = not invoice.void
+ invoice.save()
+ messages.warning(self.request, 'Invoice voided')
+
+ return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': invoice.pk}))
class PaymentCreate(generic.CreateView):
model = models.Payment
fields = ['invoice', 'date', 'amount', 'method']
+ template_name = 'payment_form.html'
def get_initial(self):
initial = super(generic.CreateView, self).get_initial()
@@ -184,13 +204,28 @@ class PaymentCreate(generic.CreateView):
initial.update({'invoice': invoice})
return initial
+ @transaction.atomic()
+ @reversion.create_revision()
+ def form_valid(self, form, *args, **kwargs):
+ reversion.add_to_revision(form.cleaned_data['invoice'])
+ reversion.set_comment("Payment added")
+ return super().form_valid(form, *args, **kwargs)
+
def get_success_url(self):
messages.info(self.request, "location.reload()")
- return reverse_lazy('closemodal')
+ return reverse('closemodal')
class PaymentDelete(generic.DeleteView):
model = models.Payment
+ template_name = 'payment_confirm_delete.html'
+
+ @transaction.atomic()
+ @reversion.create_revision()
+ def delete(self, *args, **kwargs):
+ reversion.add_to_revision(self.get_object().invoice)
+ reversion.set_comment("Payment removed")
+ return super().delete(*args, **kwargs)
def get_success_url(self):
return self.request.POST.get('next')
diff --git a/RIGS/forms.py b/RIGS/forms.py
index 47b0f062..28c660ea 100644
--- a/RIGS/forms.py
+++ b/RIGS/forms.py
@@ -1,62 +1,23 @@
+from datetime import datetime
+
+import simplejson
from django import forms
-from django.utils import formats
from django.conf import settings
from django.core import serializers
-from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
-from registration.forms import RegistrationFormUniqueEmail
-from captcha.fields import ReCaptchaField
-import simplejson
+from django.utils import timezone
+from reversion import revisions as reversion
from RIGS import models
# Override the django form defaults to use the HTML date/time/datetime UI elements
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
-forms.TimeField.widget = forms.TextInput(attrs={'type': 'time'})
-forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'})
-
-# Registration
-
-
-class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
- captcha = ReCaptchaField()
-
- 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
+forms.TimeField.widget = forms.TimeInput(attrs={'type': 'time'}, format='%H:%M')
+forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M')
# Events Shit
class EventForm(forms.ModelForm):
- datetime_input_formats = formats.get_format_lazy("DATETIME_INPUT_FORMATS") + list(settings.DATETIME_INPUT_FORMATS)
+ datetime_input_formats = list(settings.DATETIME_INPUT_FORMATS)
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
access_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
@@ -130,8 +91,11 @@ class EventForm(forms.ModelForm):
return item
def clean(self):
- if self.cleaned_data.get("is_rig") and not (self.cleaned_data.get('person') or self.cleaned_data.get('organisation')):
- raise forms.ValidationError('You haven\'t provided any client contact details. Please add a person or organisation.', code='contact')
+ if self.cleaned_data.get("is_rig") and not (
+ self.cleaned_data.get('person') or self.cleaned_data.get('organisation')):
+ raise forms.ValidationError(
+ 'You haven\'t provided any client contact details. Please add a person or organisation.',
+ code='contact')
return super(EventForm, self).clean()
def save(self, commit=True):
@@ -185,3 +149,129 @@ class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
class EventAuthorisationRequestForm(forms.Form):
email = forms.EmailField(required=True, label='Authoriser Email')
+
+
+class EventRiskAssessmentForm(forms.ModelForm):
+ def __init__(self, *args, **kwargs):
+ super(EventRiskAssessmentForm, self).__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):
+ # Check expected values
+ unexpected_values = []
+ for field, value in models.RiskAssessment.expected_values.items():
+ if self.cleaned_data.get(field) != value:
+ unexpected_values.append("
{}
".format(self._meta.model._meta.get_field(field).help_text))
+ if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
+ raise forms.ValidationError("Your answers to these questions:
{}
require consulting with a supervisor.".format(''.join([str(elem) for elem in unexpected_values])), code='unusual_answers')
+ return super(EventRiskAssessmentForm, self).clean()
+
+ class Meta:
+ model = models.RiskAssessment
+ fields = '__all__'
+ exclude = ['reviewed_at', 'reviewed_by']
+
+
+class EventChecklistForm(forms.ModelForm):
+ def __init__(self, *args, **kwargs):
+ super(EventChecklistForm, self).__init__(*args, **kwargs)
+ self.fields['date'].widget.format = '%Y-%m-%d'
+ for name, field in self.fields.items():
+ if field.__class__ == forms.NullBooleanField:
+ # Only display yes/no to user, the 'none' is only ever set in the background
+ field.widget = forms.CheckboxInput()
+ # Parsed from incoming form data by clean, then saved into models when the form is saved
+ items = {}
+
+ related_models = {
+ 'venue': models.Venue,
+ 'power_mic': models.Profile,
+ }
+
+ # Two possible formats
+ def parsedatetime(self, date_string):
+ try:
+ return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S'))
+ except ValueError:
+ return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M'))
+
+ # There's probably a thousand better ways to do this, but this one is mine
+ def clean(self):
+ vehicles = {key: val for key, val in self.data.items()
+ if key.startswith('vehicle')}
+ for key in vehicles:
+ pk = int(key.split('_')[1])
+ driver_key = 'driver_' + str(pk)
+ if(self.data[driver_key] == ''):
+ raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch')
+ else:
+ try:
+ item = models.EventChecklistVehicle.objects.get(pk=pk)
+ except models.EventChecklistVehicle.DoesNotExist:
+ item = models.EventChecklistVehicle()
+
+ item.vehicle = vehicles['vehicle_' + str(pk)]
+ item.driver = models.Profile.objects.get(pk=self.data[driver_key])
+ item.full_clean('checklist')
+
+ # item does not have a database pk yet as it isn't saved
+ self.items['v' + str(pk)] = item
+
+ crewmembers = {key: val for key, val in self.data.items()
+ if key.startswith('crewmember')}
+ other_fields = ['start', 'role', 'end']
+ for key in crewmembers:
+ pk = int(key.split('_')[1])
+
+ for field in other_fields:
+ value = self.data['{}_{}'.format(field, pk)]
+ if value == '':
+ raise forms.ValidationError('Add a {} to crewmember {}'.format(field, pk), code='{}_mismatch'.format(field))
+
+ try:
+ item = models.EventChecklistCrew.objects.get(pk=pk)
+ except models.EventChecklistCrew.DoesNotExist:
+ item = models.EventChecklistCrew()
+
+ item.crewmember = models.Profile.objects.get(pk=self.data['crewmember_' + str(pk)])
+ item.start = self.parsedatetime(self.data['start_' + str(pk)])
+ item.role = self.data['role_' + str(pk)]
+ item.end = self.parsedatetime(self.data['end_' + str(pk)])
+ item.full_clean('checklist')
+
+ # item does not have a database pk yet as it isn't saved
+ self.items['c' + str(pk)] = item
+
+ return super(EventChecklistForm, self).clean()
+
+ def save(self, commit=True):
+ checklist = super(EventChecklistForm, self).save(commit=False)
+ if (commit):
+ # Remove all existing, to be recreated from the form
+ checklist.vehicles.all().delete()
+ checklist.crew.all().delete()
+ checklist.save()
+
+ for key in self.items:
+ item = self.items[key]
+ reversion.add_to_revision(item)
+ # finish and save new database items
+ item.checklist = checklist
+ item.full_clean()
+ item.save()
+
+ self.items.clear()
+
+ return checklist
+
+ class Meta:
+ model = models.EventChecklist
+ fields = '__all__'
+ exclude = ['reviewed_at', 'reviewed_by']
diff --git a/RIGS/hs.py b/RIGS/hs.py
new file mode 100644
index 00000000..33fa5d80
--- /dev/null
+++ b/RIGS/hs.py
@@ -0,0 +1,226 @@
+from django.contrib import messages
+from django.http import HttpResponseRedirect
+from django.urls import reverse_lazy
+from django.utils import timezone
+from django.views import generic
+from reversion import revisions as reversion
+
+from RIGS import models, forms
+
+
+class EventRiskAssessmentCreate(generic.CreateView):
+ model = models.RiskAssessment
+ template_name = 'risk_assessment_form.html'
+ form_class = forms.EventRiskAssessmentForm
+
+ def get(self, *args, **kwargs):
+ epk = kwargs.get('pk')
+ event = models.Event.objects.get(pk=epk)
+
+ # Check if RA exists
+ ra = models.RiskAssessment.objects.filter(event=event).first()
+
+ if ra is not None:
+ return HttpResponseRedirect(reverse_lazy('ra_edit', kwargs={'pk': ra.pk}))
+
+ return super(EventRiskAssessmentCreate, self).get(self)
+
+ def get_form(self, **kwargs):
+ form = super(EventRiskAssessmentCreate, self).get_form(**kwargs)
+ epk = self.kwargs.get('pk')
+ event = models.Event.objects.get(pk=epk)
+ form.instance.event = event
+ return form
+
+ def get_context_data(self, **kwargs):
+ context = super(EventRiskAssessmentCreate, self).get_context_data(**kwargs)
+ epk = self.kwargs.get('pk')
+ event = models.Event.objects.get(pk=epk)
+ context['event'] = event
+ context['page_title'] = 'Create Risk Assessment for Event {}'.format(event.display_id)
+ return context
+
+ def get_success_url(self):
+ return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
+
+
+class EventRiskAssessmentEdit(generic.UpdateView):
+ model = models.RiskAssessment
+ template_name = 'risk_assessment_form.html'
+ form_class = forms.EventRiskAssessmentForm
+
+ def get_success_url(self):
+ ra = self.get_object()
+ ra.reviewed_by = None
+ ra.reviewed_at = None
+ ra.save()
+ return reverse_lazy('ra_detail', kwargs={'pk': self.object.pk})
+
+ def get_context_data(self, **kwargs):
+ context = super(EventRiskAssessmentEdit, self).get_context_data(**kwargs)
+ rpk = self.kwargs.get('pk')
+ ra = models.RiskAssessment.objects.get(pk=rpk)
+ context['event'] = ra.event
+ context['edit'] = True
+ context['page_title'] = 'Edit Risk Assessment for Event {}'.format(ra.event.display_id)
+ return context
+
+
+class EventRiskAssessmentDetail(generic.DetailView):
+ model = models.RiskAssessment
+ template_name = 'risk_assessment_detail.html'
+
+ def get_context_data(self, **kwargs):
+ context = super(EventRiskAssessmentDetail, self).get_context_data(**kwargs)
+ context['page_title'] = "Risk Assessment for Event {} {}".format(self.object.event.get_absolute_url(), self.object.event.display_id, self.object.event.name)
+ return context
+
+
+class EventRiskAssessmentList(generic.ListView):
+ paginate_by = 20
+ model = models.RiskAssessment
+ template_name = 'hs_object_list.html'
+
+ def get_queryset(self):
+ return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
+
+ def get_context_data(self, **kwargs):
+ context = super(EventRiskAssessmentList, self).get_context_data(**kwargs)
+ context['title'] = 'Risk Assessment'
+ context['view'] = 'ra_detail'
+ context['edit'] = 'ra_edit'
+ context['review'] = 'ra_review'
+ context['perm'] = 'perms.RIGS.review_riskassessment'
+ return context
+
+
+class EventRiskAssessmentReview(generic.View):
+ def get(self, *args, **kwargs):
+ rpk = kwargs.get('pk')
+ ra = models.RiskAssessment.objects.get(pk=rpk)
+ with reversion.create_revision():
+ reversion.set_user(self.request.user)
+ ra.reviewed_by = self.request.user
+ ra.reviewed_at = timezone.now()
+ ra.save()
+ return HttpResponseRedirect(reverse_lazy('ra_list'))
+
+
+class EventChecklistDetail(generic.DetailView):
+ model = models.EventChecklist
+ template_name = 'event_checklist_detail.html'
+
+ def get_context_data(self, **kwargs):
+ context = super(EventChecklistDetail, self).get_context_data(**kwargs)
+ context['page_title'] = "Event Checklist for Event {} {}".format(self.object.event.get_absolute_url(), self.object.event.display_id, self.object.event.name)
+ return context
+
+
+class EventChecklistEdit(generic.UpdateView):
+ model = models.EventChecklist
+ template_name = 'event_checklist_form.html'
+ form_class = forms.EventChecklistForm
+
+ def get_success_url(self):
+ ec = self.get_object()
+ ec.reviewed_by = None
+ ec.reviewed_at = None
+ ec.save()
+ return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
+
+ def get_context_data(self, **kwargs):
+ context = super(EventChecklistEdit, self).get_context_data(**kwargs)
+ pk = self.kwargs.get('pk')
+ ec = models.EventChecklist.objects.get(pk=pk)
+ context['event'] = ec.event
+ context['edit'] = True
+ context['page_title'] = 'Edit Event Checklist for Event {}'.format(ec.event.display_id)
+ form = context['form']
+ # Get some other objects to include in the form. Used when there are errors but also nice and quick.
+ for field, model in form.related_models.items():
+ value = form[field].value()
+ if value is not None and value != '':
+ context[field] = model.objects.get(pk=value)
+ return context
+
+
+class EventChecklistCreate(generic.CreateView):
+ model = models.EventChecklist
+ template_name = 'event_checklist_form.html'
+ form_class = forms.EventChecklistForm
+
+ # From both business logic and programming POVs, RAs must exist before ECs!
+ def get(self, *args, **kwargs):
+ epk = kwargs.get('pk')
+ event = models.Event.objects.get(pk=epk)
+
+ # Check if RA exists
+ ra = models.RiskAssessment.objects.filter(event=event).first()
+
+ if ra is None:
+ messages.error(self.request, 'A Risk Assessment must exist prior to creating any Event Checklists for {}! Please create one now.'.format(event))
+ return HttpResponseRedirect(reverse_lazy('event_ra', kwargs={'pk': epk}))
+
+ return super(EventChecklistCreate, self).get(self)
+
+ def get_form(self, **kwargs):
+ form = super(EventChecklistCreate, self).get_form(**kwargs)
+ epk = self.kwargs.get('pk')
+ event = models.Event.objects.get(pk=epk)
+ form.instance.event = event
+ return form
+
+ def get_context_data(self, **kwargs):
+ context = super(EventChecklistCreate, self).get_context_data(**kwargs)
+ epk = self.kwargs.get('pk')
+ event = models.Event.objects.get(pk=epk)
+ context['event'] = event
+ context['page_title'] = 'Create Event Checklist for Event {}'.format(event.display_id)
+ return context
+
+ def get_success_url(self):
+ return reverse_lazy('ec_detail', kwargs={'pk': self.object.pk})
+
+
+class EventChecklistList(generic.ListView):
+ paginate_by = 20
+ model = models.EventChecklist
+ template_name = 'hs_object_list.html'
+
+ def get_queryset(self):
+ return self.model.objects.exclude(event__status=models.Event.CANCELLED).order_by('reviewed_at').select_related('event')
+
+ def get_context_data(self, **kwargs):
+ context = super(EventChecklistList, self).get_context_data(**kwargs)
+ context['title'] = 'Event Checklist'
+ context['view'] = 'ec_detail'
+ context['edit'] = 'ec_edit'
+ context['review'] = 'ec_review'
+ context['perm'] = 'perms.RIGS.review_eventchecklist'
+ return context
+
+
+class EventChecklistReview(generic.View):
+ def get(self, *args, **kwargs):
+ rpk = kwargs.get('pk')
+ ec = models.EventChecklist.objects.get(pk=rpk)
+ with reversion.create_revision():
+ reversion.set_user(self.request.user)
+ ec.reviewed_by = self.request.user
+ ec.reviewed_at = timezone.now()
+ ec.save()
+ return HttpResponseRedirect(reverse_lazy('ec_list'))
+
+
+class HSList(generic.ListView):
+ paginate_by = 20
+ model = models.Event
+ template_name = 'hs_list.html'
+
+ def get_queryset(self):
+ return models.Event.objects.all().exclude(status=models.Event.CANCELLED).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists')
+
+ def get_context_data(self, **kwargs):
+ context = super(HSList, self).get_context_data(**kwargs)
+ context['page_title'] = 'H&S Overview'
+ return context
diff --git a/RIGS/ical.py b/RIGS/ical.py
index 6317f1fb..c9834b25 100644
--- a/RIGS/ical.py
+++ b/RIGS/ical.py
@@ -1,12 +1,11 @@
-from RIGS import models, forms
-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
-
import datetime
+
import pytz
+from django.conf import settings
+from django.db.models import Q
+from django_ical.views import ICalFeed
+
+from RIGS import models
class CalendarICS(ICalFeed):
@@ -40,8 +39,10 @@ class CalendarICS(ICalFeed):
return params
def description(self, params):
- desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + ('Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
- desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + ('Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
+ desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + (
+ 'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
+ desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
+ 'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
return desc
@@ -72,7 +73,8 @@ class CalendarICS(ICalFeed):
filter = filter & typeFilters & statusFilters
- return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
+ return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
+ 'venue', 'mic')
def item_title(self, item):
title = ''
@@ -99,7 +101,7 @@ class CalendarICS(ICalFeed):
return item.earliest_time
def item_end_datetime(self, item):
- if type(item.latest_time) == datetime.date: # Ical end_datetime is non-inclusive, so add a day
+ if isinstance(item.latest_time, datetime.date): # Ical end_datetime is non-inclusive, so add a day
return item.latest_time + datetime.timedelta(days=1)
return item.latest_time
@@ -117,19 +119,24 @@ class CalendarICS(ICalFeed):
desc += 'Event = ' + item.name + '\n'
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
if item.is_rig and item.person:
- desc += 'Client = ' + item.person.name + ((' for ' + item.organisation.name) if item.organisation else '') + '\n'
+ desc += 'Client = ' + item.person.name + (
+ (' for ' + item.organisation.name) if item.organisation else '') + '\n'
desc += 'Status = ' + str(item.get_status_display()) + '\n'
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
desc += '\n'
if item.meet_at:
- desc += 'Crew Meet = ' + (item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
+ desc += 'Crew Meet = ' + (
+ item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
if item.access_at:
- desc += 'Access At = ' + (item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
+ desc += 'Access At = ' + (
+ item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
if item.start_date:
- desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + ((' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
+ desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + (
+ (' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
if item.end_date:
- desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + ((' ' + item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n'
+ desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + (
+ (' ' + item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n'
desc += '\n'
if item.description:
diff --git a/RIGS/importer_tests.py b/RIGS/importer_tests.py
deleted file mode 100644
index 36485f25..00000000
--- a/RIGS/importer_tests.py
+++ /dev/null
@@ -1,28 +0,0 @@
-__author__ = 'ghost'
-
-import unittest
-from importer import fix_email
-
-
-class EmailFixerTest(unittest.TestCase):
- def test_correct(self):
- e = fix_email("tom@ghost.uk.net")
- self.assertEqual(e, "tom@ghost.uk.net")
-
- def test_partial(self):
- e = fix_email("psytp")
- self.assertEqual(e, "psytp@nottingham.ac.uk")
-
- def test_none(self):
- old = None
- new = fix_email(old)
- self.assertEqual(old, new)
-
- def test_empty(self):
- old = ""
- new = fix_email(old)
- self.assertEqual(old, new)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/RIGS/management/commands/deleteSampleData.py b/RIGS/management/commands/deleteSampleData.py
new file mode 100644
index 00000000..4abc2f76
--- /dev/null
+++ b/RIGS/management/commands/deleteSampleData.py
@@ -0,0 +1,37 @@
+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
+
+
+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)
+
+ def delete_objects(self, model):
+ for obj in model.objects.all():
+ obj.delete()
diff --git a/RIGS/management/commands/generateSampleData.py b/RIGS/management/commands/generateSampleData.py
index e19c569d..88c33d46 100644
--- a/RIGS/management/commands/generateSampleData.py
+++ b/RIGS/management/commands/generateSampleData.py
@@ -1,5 +1,7 @@
-from django.core.management.base import BaseCommand, CommandError
from django.core.management import call_command
+from django.core.management.base import BaseCommand
+
+from RIGS import models
class Command(BaseCommand):
@@ -7,5 +9,6 @@ class Command(BaseCommand):
can_import_settings = True
def handle(self, *args, **options):
+ call_command('generateSampleUserData')
call_command('generateSampleRIGSData')
call_command('generateSampleAssetsData')
diff --git a/RIGS/management/commands/generateSampleRIGSData.py b/RIGS/management/commands/generateSampleRIGSData.py
index 6b543971..01486262 100644
--- a/RIGS/management/commands/generateSampleRIGSData.py
+++ b/RIGS/management/commands/generateSampleRIGSData.py
@@ -1,11 +1,12 @@
-from django.core.management.base import BaseCommand, CommandError
-from django.contrib.auth.models import Group, Permission
-from django.db import transaction
-from reversion import revisions as reversion
-
import datetime
import 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
@@ -16,10 +17,8 @@ class Command(BaseCommand):
people = []
organisations = []
venues = []
- profiles = []
-
- keyholder_group = None
- finance_group = None
+ events = []
+ profiles = models.Profile.objects.all()
def handle(self, *args, **options):
from django.conf import settings
@@ -27,170 +26,164 @@ class Command(BaseCommand):
if not (settings.DEBUG or settings.STAGING):
raise CommandError('You cannot run this command in production')
- random.seed('Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests
+ random.seed(
+ 'Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests
with transaction.atomic():
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
+ self.setup_people()
+ self.setup_organisations()
+ self.setup_venues()
+ self.setup_events()
- self.setupGenericProfiles()
-
- self.setupPeople()
- self.setupOrganisations()
- self.setupVenues()
-
- self.setupGroups()
-
- self.setupEvents()
-
- self.setupUsefulProfiles()
-
- def setupPeople(self):
- names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe", "Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore", "Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan", "Nicolas Flamel", "Cornelius Fudge", "Goyle", "Gregory Goyle", "Hermione Granger", "Rubeus Hagrid", "Igor Karkaroff", "Viktor Krum", "Bellatrix Lestrange", "Alice Longbottom", "Frank Longbottom", "Neville Longbottom", "Luna Lovegood", "Xenophilius Lovegood", # 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_people(self):
+ names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe",
+ "Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore",
+ "Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan",
+ "Nicolas Flamel", "Cornelius Fudge", "Goyle", "Gregory Goyle", "Hermione Granger", "Rubeus Hagrid",
+ "Igor Karkaroff", "Viktor Krum", "Bellatrix Lestrange", "Alice Longbottom", "Frank Longbottom",
+ "Neville Longbottom", "Luna Lovegood", "Xenophilius Lovegood", # noqa
+ "Remus Lupin", "Draco Malfoy", "Lucius Malfoy", "Narcissa Malfoy", "Olympe Maxime",
+ "Minerva McGonagall", "Mad-Eye Moody", "Peter Pettigrew", "Harry Potter", "James Potter",
+ "Lily Potter", "Quirinus Quirrell", "Tom Riddle", "Mary Riddle", "Lord Voldemort", "Rita Skeeter",
+ "Severus Snape", "Nymphadora Tonks", "Dolores Janes Umbridge", "Arthur Weasley", "Bill Weasley",
+ "Charlie Weasley", "Fred Weasley", "George Weasley", "Ginny Weasley", "Molly Weasley", "Percy Weasley",
+ "Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
- reversion.set_user(random.choice(self.profiles))
+ reversion.set_user(random.choice(models.Profile.objects.all()))
+ person = models.Person.objects.create(name=name)
- newPerson = models.Person.objects.create(name=name)
if i % 3 == 0:
- newPerson.email = "address@person.com"
+ person.email = "address@person.com"
if i % 5 == 0:
- newPerson.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
+ 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:
- newPerson.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
+ person.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
- newPerson.phone = "01234 567894"
+ person.phone = "01234 567894"
- newPerson.save()
- self.people.append(newPerson)
+ person.save()
+ self.people.append(person)
- def setupOrganisations(self):
- names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # 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
+ 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(self.profiles))
- newOrganisation = models.Organisation.objects.create(name=name)
+ reversion.set_user(random.choice(models.Profile.objects.all()))
+ new_organisation = models.Organisation.objects.create(name=name)
+
if i % 2 == 0:
- newOrganisation.has_su_account = True
+ new_organisation.has_su_account = True
if i % 3 == 0:
- newOrganisation.email = "address@organisation.com"
+ new_organisation.email = "address@organisation.com"
if i % 5 == 0:
- newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
+ 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:
- newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
+ new_organisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
- newOrganisation.phone = "01234 567894"
+ new_organisation.phone = "01234 567894"
- newOrganisation.save()
- self.organisations.append(newOrganisation)
+ new_organisation.save()
+ self.organisations.append(new_organisation)
- 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
+ def setup_venues(self):
+ names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch",
+ "The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands",
+ "The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins",
+ "The Vale of Arryn", "The Whispering Wood", "White Harbor", "Winterfell", "The Arbor", "Ashemark",
+ "Brightwater Keep", "Casterly Rock", "Clegane's Keep", "Dragonstone", "Dorne", "God's Eye",
+ "The Golden Tooth", # noqa
+ "Harrenhal", "Highgarden", "Horn Hill", "Fingers", "King's Landing", "Lannisport", "Oldtown",
+ "Rainswood", "Storm's End", "Summerhall", "Sunspear", "Tarth", "Castle Black", "Craster's Keep",
+ "Fist of the First Men", "The Frostfangs", "The Gift", "The Skirling Pass", "The Wall", "Asshai",
+ "Astapor", "Braavos", "The Dothraki Sea", "Lys", "Meereen", "Myr", "Norvos", "Pentos", "Qarth",
+ "Qohor", "The Red Waste", "Tyrosh", "Vaes Dothrak", "Valyria", "Village of the Lhazareen", "Volantis",
+ "Yunkai"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
- newVenue = models.Venue.objects.create(name=name)
+ new_venue = models.Venue.objects.create(name=name)
+
if i % 2 == 0:
- newVenue.three_phase_available = True
+ new_venue.three_phase_available = True
if i % 3 == 0:
- newVenue.email = "address@venue.com"
+ new_venue.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"
+ new_venue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
- newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
+ new_venue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
- newVenue.phone = "01234 567894"
+ new_venue.phone = "01234 567894"
- newVenue.save()
- self.venues.append(newVenue)
+ new_venue.save()
+ self.venues.append(new_venue)
- def setupGroups(self):
- self.keyholder_group = Group.objects.create(name='Keyholders')
- self.finance_group = Group.objects.create(name='Finance')
+ def setup_events(self):
+ names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball",
+ "Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
+ "End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show",
+ "Awards Ceremony", "Debating Event", "Club Night", "DJ Evening", "Building Projection",
+ "Choir Concert"]
+ descriptions = ["A brief description of the event", "This event is boring", "Probably wont happen",
+ "Warning: this has lots of kit"]
+ notes = ["The client came into the office at some point", "Who knows if this will happen",
+ "Probably should check this event", "Maybe not happening", "Run away!"]
- 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"]
+ item_options = [
+ {'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2,
+ 'cost': 200.00},
+ {'name': 'Projector',
+ 'description': 'Some kind of video thinamejig, probably with unnecessary processing for free',
+ 'quantity': 1, 'cost': 500.00},
+ {'name': 'Lighting Desk', 'description': 'Cannot provide guarentee that it will work', 'quantity': 1,
+ 'cost': 200.52},
+ {'name': 'Moving lights', 'description': 'Flashy lights, with the copper', 'quantity': 8, 'cost': 50.00},
+ {'name': 'Microphones', 'description': 'Make loud noise \n you will want speakers with this', 'quantity': 5,
+ 'cost': 0.50},
+ {'name': 'Sound Mixer Thing', 'description': 'Might be analogue, might be digital', 'quantity': 1,
+ 'cost': 100.00},
+ {'name': 'Electricity', 'description': 'You need this', 'quantity': 1, 'cost': 200.00},
+ {'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
+ {'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
- 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
+ day_delta = -120 # start adding events from 4 months ago
for i in range(150): # Let's add 100 events
with reversion.create_revision():
@@ -198,63 +191,100 @@ class Command(BaseCommand):
name = names[i % len(names)]
- startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
- dayDelta = dayDelta + random.randint(0, 3)
+ start_date = datetime.date.today() + datetime.timedelta(days=day_delta)
+ day_delta = day_delta + random.randint(0, 3)
- newEvent = models.Event.objects.create(name=name, start_date=startDate)
+ new_event = models.Event.objects.create(name=name, start_date=start_date)
if random.randint(0, 2) > 1: # 1 in 3 have a start time
- newEvent.start_time = datetime.time(random.randint(15, 20))
+ new_event.start_time = datetime.time(random.randint(15, 20))
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
- newEvent.end_time = datetime.time(random.randint(21, 23))
+ new_event.end_time = datetime.time(random.randint(21, 23))
elif random.randint(0, 1) > 0: # half of the others finish early the next day
- newEvent.end_date = newEvent.start_date + datetime.timedelta(days=1)
- newEvent.end_time = datetime.time(random.randint(0, 5))
+ new_event.end_date = new_event.start_date + datetime.timedelta(days=1)
+ new_event.end_time = datetime.time(random.randint(0, 5))
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
- newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1, 4))
+ new_event.end_date = new_event.start_date + datetime.timedelta(days=random.randint(1, 4))
if random.randint(0, 6) > 0: # 5 in 6 have MIC
- newEvent.mic = random.choice(self.profiles)
+ new_event.mic = random.choice(self.profiles)
if random.randint(0, 6) > 0: # 5 in 6 have organisation
- newEvent.organisation = random.choice(self.organisations)
+ new_event.organisation = random.choice(self.organisations)
if random.randint(0, 6) > 0: # 5 in 6 have person
- newEvent.person = random.choice(self.people)
+ new_event.person = random.choice(self.people)
if random.randint(0, 6) > 0: # 5 in 6 have venue
- newEvent.venue = random.choice(self.venues)
+ new_event.venue = random.choice(self.venues)
# Could have any status, equally weighted
- newEvent.status = random.choice([models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
+ new_event.status = random.choice(
+ [models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
- newEvent.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
+ new_event.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
if random.randint(0, 1) > 0: # 1 in 2 have description
- newEvent.description = random.choice(descriptions)
+ new_event.description = random.choice(descriptions)
if random.randint(0, 1) > 0: # 1 in 2 have notes
- newEvent.notes = random.choice(notes)
+ new_event.notes = random.choice(notes)
- newEvent.save()
+ new_event.save()
# Now add some items
for j in range(random.randint(1, 5)):
- itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
- newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
- newItem.save()
+ item_data = item_options[random.randint(0, len(item_options) - 1)]
+ new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
+ new_item.save()
- while newEvent.sum_total < 0:
- itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
- newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
- newItem.save()
+ while new_event.sum_total < 0:
+ item_data = item_options[random.randint(0, len(item_options) - 1)]
+ new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
+ new_item.save()
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
- if newEvent.start_date < datetime.date.today(): # think about adding an invoice
+ if new_event.start_date < datetime.date.today(): # think about adding an invoice
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
- newInvoice = models.Invoice.objects.create(event=newEvent)
- if newEvent.status is models.Event.CANCELLED: # void cancelled events
- newInvoice.void = True
+ new_invoice = models.Invoice.objects.create(event=new_event)
+ if new_event.status is models.Event.CANCELLED: # void cancelled events
+ new_invoice.void = True
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
- models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance, date=datetime.date.today())
+ models.Payment.objects.create(invoice=new_invoice, amount=new_invoice.balance,
+ date=datetime.date.today())
+ if i == 1 or random.randint(0, 5) > 0: # Event 1 and 1 in 5 have a RA
+ models.RiskAssessment.objects.create(event=new_event, supervisor_consulted=bool(random.getrandbits(1)),
+ nonstandard_equipment=bool(random.getrandbits(1)),
+ nonstandard_use=bool(random.getrandbits(1)),
+ contractors=bool(random.getrandbits(1)),
+ other_companies=bool(random.getrandbits(1)),
+ crew_fatigue=bool(random.getrandbits(1)),
+ big_power=bool(random.getrandbits(1)),
+ generators=bool(random.getrandbits(1)),
+ other_companies_power=bool(random.getrandbits(1)),
+ nonstandard_equipment_power=bool(random.getrandbits(1)),
+ multiple_electrical_environments=bool(random.getrandbits(1)),
+ noise_monitoring=bool(random.getrandbits(1)),
+ known_venue=bool(random.getrandbits(1)),
+ safe_loading=bool(random.getrandbits(1)),
+ safe_storage=bool(random.getrandbits(1)),
+ area_outside_of_control=bool(random.getrandbits(1)),
+ barrier_required=bool(random.getrandbits(1)),
+ nonstandard_emergency_procedure=bool(random.getrandbits(1)),
+ special_structures=bool(random.getrandbits(1)),
+ suspended_structures=bool(random.getrandbits(1)),
+ outside=bool(random.getrandbits(1)))
+ if i == 0 or random.randint(0, 1) > 0: # Event 1 and 1 in 10 have a Checklist
+ models.EventChecklist.objects.create(event=new_event, power_mic=random.choice(self.profiles),
+ safe_parking=bool(random.getrandbits(1)),
+ safe_packing=bool(random.getrandbits(1)),
+ exits=bool(random.getrandbits(1)),
+ trip_hazard=bool(random.getrandbits(1)),
+ warning_signs=bool(random.getrandbits(1)),
+ ear_plugs=bool(random.getrandbits(1)),
+ hs_location="Locked away safely",
+ extinguishers_location="Somewhere, I forgot",
+ earthing=bool(random.getrandbits(1)),
+ pat=bool(random.getrandbits(1)),
+ date=timezone.now(), venue=random.choice(self.venues))
diff --git a/RIGS/migrations/0036_profile_is_approved.py b/RIGS/migrations/0036_profile_is_approved.py
new file mode 100644
index 00000000..3fdb5af7
--- /dev/null
+++ b/RIGS/migrations/0036_profile_is_approved.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.0.13 on 2020-01-10 14:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('RIGS', '0035_auto_20191124_1319'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='profile',
+ name='is_approved',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='profile',
+ name='last_emailed',
+ field=models.DateTimeField(blank=True, null=True),
+ ),
+ ]
diff --git a/RIGS/migrations/0037_approve_legacy.py b/RIGS/migrations/0037_approve_legacy.py
new file mode 100644
index 00000000..d38c1b66
--- /dev/null
+++ b/RIGS/migrations/0037_approve_legacy.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.0.13 on 2020-01-11 18:29
+# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
+from django.db import migrations
+
+def approve_legacy(apps, schema_editor):
+ Profile = apps.get_model('RIGS', 'Profile')
+ for person in Profile.objects.all():
+ person.is_approved = True
+ person.save()
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('RIGS', '0036_profile_is_approved'),
+ ]
+
+ operations = [
+ migrations.RunPython(approve_legacy, migrations.RunPython.noop)
+ ]
diff --git a/RIGS/migrations/0038_auto_20200306_2000.py b/RIGS/migrations/0038_auto_20200306_2000.py
new file mode 100644
index 00000000..f1f893e0
--- /dev/null
+++ b/RIGS/migrations/0038_auto_20200306_2000.py
@@ -0,0 +1,37 @@
+# Generated by Django 2.0.13 on 2020-03-06 20:00
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('RIGS', '0037_approve_legacy'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='event',
+ options={},
+ ),
+ migrations.AlterModelOptions(
+ name='invoice',
+ options={'ordering': ['-invoice_date']},
+ ),
+ migrations.AlterModelOptions(
+ name='organisation',
+ options={},
+ ),
+ migrations.AlterModelOptions(
+ name='person',
+ options={},
+ ),
+ migrations.AlterModelOptions(
+ name='profile',
+ options={'verbose_name': 'user', 'verbose_name_plural': 'users'},
+ ),
+ migrations.AlterModelOptions(
+ name='venue',
+ options={},
+ ),
+ ]
diff --git a/RIGS/migrations/0039_auto_20210123_1910.py b/RIGS/migrations/0039_auto_20210123_1910.py
new file mode 100644
index 00000000..0b995e30
--- /dev/null
+++ b/RIGS/migrations/0039_auto_20210123_1910.py
@@ -0,0 +1,190 @@
+# 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
+
+
+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? (does not obstruct venue access)', null=True)),
+ ('safe_packing', models.BooleanField(blank=True, help_text='Equipment packed away safely? (including flightcases)', 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? (strobe, smoke, power etc.)')),
+ ('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? (using socket tester)', null=True)),
+ ('earthing', models.BooleanField(blank=True, help_text='Equipment appropriately earthed? (truss, stage, generators etc)', 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? (if cable is more than 3m long) ', 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 (if required)', null=True, verbose_name='Phase Rotation')),
+ ('fd_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (ZS)', 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 (ZS)', 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 (ZS)', 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 (ZS)', null=True)),
+ ('all_rcds_tested', models.BooleanField(blank=True, help_text='All circuit RCDs tested? (using test button)', null=True)),
+ ('public_sockets_tested', models.BooleanField(blank=True, help_text='Public/Performer accessible circuits tested? (using socket tester)', null=True)),
+ ('reviewed_at', models.DateTimeField(null=True)),
+ ],
+ options={
+ 'ordering': ['event'],
+ 'permissions': [('review_eventchecklist', 'Can review Event Checklists')],
+ },
+ bases=(models.Model, RIGS.models.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, RIGS.models.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, RIGS.models.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 TEC's standard risk assessments and method statements?")),
+ ('nonstandard_use', models.BooleanField(help_text='Are TEC using their equipment in a way that is abnormal? i.e. Not covered by TECs standard health and safety documentation')),
+ ('contractors', models.BooleanField(help_text='Are you using any external contractors? i.e. Freelancers/Crewing Companies')),
+ ('other_companies', models.BooleanField(help_text='Are TEC working with any other companies on site? e.g. TEC is providing the lighting while another company does sound')),
+ ('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 Sharepoint 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 Sharepoint 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, RIGS.models.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 must 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'),
+ ),
+ ]
diff --git a/RIGS/migrations/0040_auto_20210302_1148.py b/RIGS/migrations/0040_auto_20210302_1148.py
new file mode 100644
index 00000000..fd923180
--- /dev/null
+++ b/RIGS/migrations/0040_auto_20210302_1148.py
@@ -0,0 +1,67 @@
+# Generated by Django 3.1.7 on 2021-03-02 11:48
+
+from django.db import migrations
+
+
+def postgres_migration_prep(apps, schema_editor):
+ model = apps.get_model("RIGS", "Event")
+ for field in ["auth_request_to", "collector", "description", "notes", "purchase_order"]:
+ filter_param = {"{}__isnull".format(field): True}
+ update_param = {field: ""}
+ model.objects.filter(**filter_param).update(**update_param)
+ model = apps.get_model("RIGS", "EventAuthorisation")
+ for field in ["account_code", "uni_id"]:
+ filter_param = {"{}__isnull".format(field): True}
+ update_param = {field: ""}
+ model.objects.filter(**filter_param).update(**update_param)
+ model = apps.get_model("RIGS", "EventChecklist")
+ for field in ["extinguishers_location", "hs_location", "w1_description", "w2_description", "w3_description"]:
+ filter_param = {"{}__isnull".format(field): True}
+ update_param = {field: ""}
+ model.objects.filter(**filter_param).update(**update_param)
+ model = apps.get_model("RIGS", "EventItem")
+ for field in ["description"]:
+ filter_param = {"{}__isnull".format(field): True}
+ update_param = {field: ""}
+ model.objects.filter(**filter_param).update(**update_param)
+ model = apps.get_model("RIGS", "Organisation")
+ for field in ["address", "email", "notes", "phone"]:
+ filter_param = {"{}__isnull".format(field): True}
+ update_param = {field: ""}
+ model.objects.filter(**filter_param).update(**update_param)
+ model = apps.get_model("RIGS", "Payment")
+ for field in ["method"]:
+ filter_param = {"{}__isnull".format(field): True}
+ update_param = {field: ""}
+ model.objects.filter(**filter_param).update(**update_param)
+ model = apps.get_model("RIGS", "Person")
+ for field in ["address", "email", "notes", "phone"]:
+ filter_param = {"{}__isnull".format(field): True}
+ update_param = {field: ""}
+ model.objects.filter(**filter_param).update(**update_param)
+ model = apps.get_model("RIGS", "Profile")
+ for field in ["phone"]:
+ filter_param = {"{}__isnull".format(field): True}
+ update_param = {field: ""}
+ model.objects.filter(**filter_param).update(**update_param)
+ model = apps.get_model("RIGS", "RiskAssessment")
+ for field in ["general_notes", "persons_responsible_structures", "power_notes", "rigging_plan", "sound_notes"]:
+ filter_param = {"{}__isnull".format(field): True}
+ update_param = {field: ""}
+ model.objects.filter(**filter_param).update(**update_param)
+ model = apps.get_model("RIGS", "Venue")
+ for field in ["address", "email", "notes", "phone"]:
+ filter_param = {"{}__isnull".format(field): True}
+ update_param = {field: ""}
+ model.objects.filter(**filter_param).update(**update_param)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('RIGS', '0039_auto_20210123_1910'),
+ ]
+
+ operations = [
+ migrations.RunPython(postgres_migration_prep, migrations.RunPython.noop)
+ ]
diff --git a/RIGS/migrations/0041_auto_20210302_1204.py b/RIGS/migrations/0041_auto_20210302_1204.py
new file mode 100644
index 00000000..8dfc0efb
--- /dev/null
+++ b/RIGS/migrations/0041_auto_20210302_1204.py
@@ -0,0 +1,201 @@
+# Generated by Django 3.1.7 on 2021-03-02 12:04
+
+import RIGS.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('RIGS', '0040_auto_20210302_1148'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='event',
+ name='meet_info',
+ ),
+ migrations.RemoveField(
+ model_name='event',
+ name='payment_method',
+ ),
+ migrations.RemoveField(
+ model_name='event',
+ name='payment_received',
+ ),
+ migrations.AddField(
+ model_name='profile',
+ name='dark_theme',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AlterField(
+ model_name='event',
+ name='auth_request_to',
+ field=models.EmailField(blank=True, default='', max_length=254),
+ ),
+ migrations.AlterField(
+ model_name='event',
+ name='collector',
+ field=models.CharField(blank=True, default='', max_length=255, verbose_name='collected by'),
+ ),
+ migrations.AlterField(
+ model_name='event',
+ name='description',
+ field=models.TextField(blank=True, default=''),
+ ),
+ migrations.AlterField(
+ model_name='event',
+ name='notes',
+ field=models.TextField(blank=True, default=''),
+ ),
+ migrations.AlterField(
+ model_name='event',
+ name='purchase_order',
+ field=models.CharField(blank=True, default='', max_length=255, verbose_name='PO'),
+ ),
+ migrations.AlterField(
+ model_name='eventauthorisation',
+ name='account_code',
+ field=models.CharField(blank=True, default='', max_length=50),
+ ),
+ migrations.AlterField(
+ model_name='eventauthorisation',
+ name='uni_id',
+ field=models.CharField(blank=True, default='', max_length=10, verbose_name='University ID'),
+ ),
+ migrations.AlterField(
+ model_name='eventchecklist',
+ name='extinguishers_location',
+ field=models.CharField(blank=True, default='', help_text='Location of fire extinguishers', max_length=255),
+ ),
+ migrations.AlterField(
+ model_name='eventchecklist',
+ name='hs_location',
+ field=models.CharField(blank=True, default='', help_text='Location of Safety Bag/Box', max_length=255),
+ ),
+ migrations.AlterField(
+ model_name='eventchecklist',
+ name='w1_description',
+ field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
+ ),
+ migrations.AlterField(
+ model_name='eventchecklist',
+ name='w2_description',
+ field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
+ ),
+ migrations.AlterField(
+ model_name='eventchecklist',
+ name='w3_description',
+ field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
+ ),
+ migrations.AlterField(
+ model_name='eventitem',
+ name='description',
+ field=models.TextField(blank=True, default=''),
+ ),
+ migrations.AlterField(
+ model_name='organisation',
+ name='address',
+ field=models.TextField(blank=True, default=''),
+ ),
+ migrations.AlterField(
+ model_name='organisation',
+ name='email',
+ field=models.EmailField(blank=True, default='', max_length=254),
+ ),
+ migrations.AlterField(
+ model_name='organisation',
+ name='notes',
+ field=models.TextField(blank=True, default=''),
+ ),
+ migrations.AlterField(
+ model_name='organisation',
+ name='phone',
+ field=models.CharField(blank=True, default='', max_length=15),
+ ),
+ migrations.AlterField(
+ model_name='payment',
+ name='method',
+ field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('SU', 'SU Core'), ('T', 'TEC Adjustment')], default='', max_length=2),
+ ),
+ migrations.AlterField(
+ model_name='person',
+ name='address',
+ field=models.TextField(blank=True, default=''),
+ ),
+ migrations.AlterField(
+ model_name='person',
+ name='email',
+ field=models.EmailField(blank=True, default='', max_length=254),
+ ),
+ migrations.AlterField(
+ model_name='person',
+ name='notes',
+ field=models.TextField(blank=True, default=''),
+ ),
+ migrations.AlterField(
+ model_name='person',
+ name='phone',
+ field=models.CharField(blank=True, default='', max_length=15),
+ ),
+ migrations.AlterField(
+ model_name='profile',
+ name='api_key',
+ field=models.CharField(blank=True, default='', editable=False, max_length=40),
+ ),
+ migrations.AlterField(
+ model_name='profile',
+ name='phone',
+ field=models.CharField(blank=True, default='', max_length=13),
+ ),
+ migrations.AlterField(
+ model_name='riskassessment',
+ name='general_notes',
+ field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
+ ),
+ migrations.AlterField(
+ model_name='riskassessment',
+ name='persons_responsible_structures',
+ field=models.TextField(blank=True, default='', help_text='Who are the persons on site responsible for their use?'),
+ ),
+ migrations.AlterField(
+ model_name='riskassessment',
+ name='power_notes',
+ field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
+ ),
+ migrations.AlterField(
+ model_name='riskassessment',
+ name='power_plan',
+ field=models.URLField(blank=True, default='', help_text="Upload your power plan to the Sharepoint 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 Sharepoint 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),
+ ),
+ ]
diff --git a/RIGS/migrations/0042_auto_20211007_2338.py b/RIGS/migrations/0042_auto_20211007_2338.py
new file mode 100644
index 00000000..20343cdf
--- /dev/null
+++ b/RIGS/migrations/0042_auto_20211007_2338.py
@@ -0,0 +1,34 @@
+# Generated by Django 3.1.13 on 2021-10-07 22:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('RIGS', '0041_auto_20210302_1204'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='eventchecklist',
+ name='fd_earth_fault',
+ field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (ZS)', 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 (ZS)', 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 (ZS)', 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 (ZS)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
+ ),
+ ]
+
diff --git a/RIGS/models.py b/RIGS/models.py
index 937d5354..c45a9134 100644
--- a/RIGS/models.py
+++ b/RIGS/models.py
@@ -1,32 +1,32 @@
import datetime
import hashlib
-import datetime
-import pytz
-
-from django.db import models
-from django.contrib.auth.models import AbstractUser
-from django.conf import settings
-from django.utils import timezone
-from django.utils.functional import cached_property
-from django.utils.encoding import python_2_unicode_compatible
-from reversion import revisions as reversion
-from reversion.models import Version
-import string
-
import random
+import string
from collections import Counter
from decimal import Decimal
+from urllib.parse import urlparse
+import pytz
+from django import forms
+from django.conf import settings
+from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
-from django.urls import reverse_lazy
+from django.db import models
+from django.urls import reverse
+from django.utils import timezone
+from django.utils.functional import cached_property
+from reversion import revisions as reversion
+from reversion.models import Version
-# Create your models here.
-@python_2_unicode_compatible
class Profile(AbstractUser):
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
- phone = models.CharField(max_length=13, null=True, blank=True)
- api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
+ phone = models.CharField(max_length=13, blank=True, default='')
+ api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
+ is_approved = models.BooleanField(default=False)
+ # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
+ last_emailed = models.DateTimeField(blank=True, null=True)
+ dark_theme = models.BooleanField(default=False)
@classmethod
def make_api_key(cls):
@@ -39,7 +39,8 @@ class Profile(AbstractUser):
def profile_picture(self):
url = ""
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
- url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email.encode('utf-8')).hexdigest() + "?d=wavatar&s=500"
+ url = "https://www.gravatar.com/avatar/" + hashlib.md5(
+ self.email.encode('utf-8')).hexdigest() + "?d=wavatar&s=500"
return url
@property
@@ -51,18 +52,28 @@ class Profile(AbstractUser):
@property
def latest_events(self):
- return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
+ return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
+
+ @classmethod
+ def admins(cls):
+ return Profile.objects.filter(email__in=[y for x in settings.ADMINS for y in x])
+
+ @classmethod
+ def users_awaiting_approval_count(cls):
+ return Profile.objects.filter(models.Q(is_approved=False)).count()
def __str__(self):
return self.name
- class Meta:
- permissions = (
- ('view_profile', 'Can view Profile'),
- )
+# TODO move to versioning - currently get import errors with that
class RevisionMixin(object):
+ @property
+ def is_first_version(self):
+ versions = Version.objects.get_for_object(self)
+ return len(versions) == 1
+
@property
def current_version(self):
version = Version.objects.get_for_object(self).select_related('revision').first()
@@ -90,16 +101,14 @@ class RevisionMixin(object):
return "V{0} | R{1}".format(version.pk, version.revision.pk)
-@reversion.register
-@python_2_unicode_compatible
class Person(models.Model, RevisionMixin):
name = models.CharField(max_length=50)
- phone = models.CharField(max_length=15, blank=True, null=True)
- email = models.EmailField(blank=True, null=True)
+ phone = models.CharField(max_length=15, blank=True, default='')
+ email = models.EmailField(blank=True, default='')
- address = models.TextField(blank=True, null=True)
+ address = models.TextField(blank=True, default='')
- notes = models.TextField(blank=True, null=True)
+ notes = models.TextField(blank=True, default='')
def __str__(self):
string = self.name
@@ -125,24 +134,17 @@ class Person(models.Model, RevisionMixin):
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def get_absolute_url(self):
- return reverse_lazy('person_detail', kwargs={'pk': self.pk})
-
- class Meta:
- permissions = (
- ('view_person', 'Can view Persons'),
- )
+ return reverse('person_detail', kwargs={'pk': self.pk})
-@reversion.register
-@python_2_unicode_compatible
class Organisation(models.Model, RevisionMixin):
name = models.CharField(max_length=50)
- phone = models.CharField(max_length=15, blank=True, null=True)
- email = models.EmailField(blank=True, null=True)
+ phone = models.CharField(max_length=15, blank=True, default='')
+ email = models.EmailField(blank=True, default='')
- address = models.TextField(blank=True, null=True)
+ address = models.TextField(blank=True, default='')
- notes = models.TextField(blank=True, null=True)
+ notes = models.TextField(blank=True, default='')
union_account = models.BooleanField(default=False)
def __str__(self):
@@ -169,12 +171,7 @@ class Organisation(models.Model, RevisionMixin):
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def get_absolute_url(self):
- return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
-
- class Meta:
- permissions = (
- ('view_organisation', 'Can view Organisations'),
- )
+ return reverse('organisation_detail', kwargs={'pk': self.pk})
class VatManager(models.Manager):
@@ -182,7 +179,6 @@ class VatManager(models.Manager):
return self.find_rate(timezone.now())
def find_rate(self, date):
- # return self.filter(startAt__lte=date).latest()
try:
return self.filter(start_at__lte=date).latest()
except VatRate.DoesNotExist:
@@ -192,7 +188,6 @@ class VatManager(models.Manager):
@reversion.register
-@python_2_unicode_compatible
class VatRate(models.Model, RevisionMixin):
start_at = models.DateField()
rate = models.DecimalField(max_digits=6, decimal_places=6)
@@ -200,6 +195,8 @@ class VatRate(models.Model, RevisionMixin):
objects = VatManager()
+ reversion_hide = True
+
@property
def as_percent(self):
return self.rate * 100
@@ -212,16 +209,14 @@ class VatRate(models.Model, RevisionMixin):
return self.comment + " " + str(self.start_at) + " @ " + str(self.as_percent) + "%"
-@reversion.register
-@python_2_unicode_compatible
class Venue(models.Model, RevisionMixin):
name = models.CharField(max_length=255)
- phone = models.CharField(max_length=15, blank=True, null=True)
- email = models.EmailField(blank=True, null=True)
+ phone = models.CharField(max_length=15, blank=True, default='')
+ email = models.EmailField(blank=True, default='')
three_phase_available = models.BooleanField(default=False)
- notes = models.TextField(blank=True, null=True)
+ notes = models.TextField(blank=True, default='')
- address = models.TextField(blank=True, null=True)
+ address = models.TextField(blank=True, default='')
def __str__(self):
string = self.name
@@ -234,23 +229,23 @@ class Venue(models.Model, RevisionMixin):
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def get_absolute_url(self):
- return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
-
- class Meta:
- permissions = (
- ('view_venue', 'Can view Venues'),
- )
+ return reverse('venue_detail', kwargs={'pk': self.pk})
class EventManager(models.Manager):
def current_events(self):
events = self.filter(
- (models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Starts after with no end
- (models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Ends after
- (models.Q(dry_hire=True, start_date__gte=timezone.now().date()) & ~models.Q(status=Event.CANCELLED)) | # Active dry hire
- (models.Q(dry_hire=True, checked_in_by__isnull=True) & (models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
- models.Q(status=Event.CANCELLED, start_date__gte=timezone.now().date()) # Canceled but not started
+ (models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q(
+ status=Event.CANCELLED)) | # Starts after with no end
+ (models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
+ status=Event.CANCELLED)) | # Ends after
+ (models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(
+ status=Event.CANCELLED)) | # Active dry hire
+ (models.Q(dry_hire=True, checked_in_by__isnull=True) & (
+ models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
+ models.Q(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
+
return events
def events_in_bounds(self, start, end):
@@ -273,21 +268,31 @@ class EventManager(models.Manager):
def rig_count(self):
event_count = self.filter(
- (models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False,
+ (models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end
- (models.Q(end_date__gte=timezone.now().date(), dry_hire=False, is_rig=True) & ~models.Q(
+ (models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Ends after
- (models.Q(dry_hire=True, start_date__gte=timezone.now().date(), is_rig=True) & ~models.Q(
- status=Event.CANCELLED)) | # Active dry hire
- (models.Q(dry_hire=True, checked_in_by__isnull=True, is_rig=True) & (
- models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) # Active dry hire GT
+ (models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q(
+ status=Event.CANCELLED)) # Active dry hire
).count()
return event_count
+ def waiting_invoices(self):
+ events = self.filter(
+ (
+ models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
+ models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before
+ ) & models.Q(invoice__isnull=True) & # Has not already been invoiced
+ models.Q(is_rig=True) # Is a rig (not non-rig)
+ ).order_by('start_date') \
+ .select_related('person', 'organisation', 'venue', 'mic') \
+ .prefetch_related('items')
+
+ return events
+
@reversion.register(follow=['items'])
-@python_2_unicode_compatible
class Event(models.Model, RevisionMixin):
# Done to make it much nicer on the database
PROVISIONAL = 0
@@ -305,8 +310,8 @@ class Event(models.Model, RevisionMixin):
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
- description = models.TextField(blank=True, null=True)
- notes = models.TextField(blank=True, null=True)
+ description = models.TextField(blank=True, default='')
+ notes = models.TextField(blank=True, default='')
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
dry_hire = models.BooleanField(default=False)
is_rig = models.BooleanField(default=True)
@@ -320,26 +325,31 @@ class Event(models.Model, RevisionMixin):
end_time = models.TimeField(blank=True, null=True)
access_at = models.DateTimeField(blank=True, null=True)
meet_at = models.DateTimeField(blank=True, null=True)
- meet_info = models.CharField(max_length=255, blank=True, null=True)
# Crew management
- checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True, on_delete=models.CASCADE)
+ checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
+ on_delete=models.CASCADE)
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
verbose_name="MIC", on_delete=models.CASCADE)
# Monies
- payment_method = models.CharField(max_length=255, blank=True, null=True)
- payment_received = models.CharField(max_length=255, blank=True, null=True)
- purchase_order = models.CharField(max_length=255, blank=True, null=True, verbose_name='PO')
- collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='collected by')
+ purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
+ collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
# Authorisation request details
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
auth_request_at = models.DateTimeField(null=True, blank=True)
- auth_request_to = models.EmailField(null=True, blank=True)
+ auth_request_to = models.EmailField(blank=True, default='')
- # Risk assessment info
- risk_assessment_edit_url = models.CharField(verbose_name="risk assessment", max_length=255, blank=True, null=True)
+ @property
+ def display_id(self):
+ if self.pk:
+ if self.is_rig:
+ return str("N%05d" % self.pk)
+ else:
+ return self.pk
+ else:
+ return "????"
# Calculated values
"""
@@ -348,18 +358,7 @@ class Event(models.Model, RevisionMixin):
@property
def sum_total(self):
- # Manual querying is required for efficiency whilst maintaining floating point arithmetic
- # if connection.vendor == 'postgresql':
- # sql = "SELECT SUM(quantity * cost) AS sum_total FROM \"RIGS_eventitem\" WHERE event_id=%i" % self.id
- # else:
- # sql = "SELECT id, SUM(quantity * cost) AS sum_total FROM RIGS_eventitem WHERE event_id=%i" % self.id
- # total = self.items.raw(sql)[0]
- # if total.sum_total:
- # return total.sum_total
- # total = 0.0
- # for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"):
- # total += item.sum
- total = EventItem.objects.filter(event=self).aggregate(
+ total = self.items.aggregate(
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
output_field=models.DecimalField(max_digits=10, decimal_places=2))
)['sum_total']
@@ -373,6 +372,9 @@ class Event(models.Model, RevisionMixin):
@property
def vat(self):
+ # No VAT is owed on internal transfers
+ if self.internal:
+ return 0
return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
"""
@@ -392,8 +394,8 @@ class Event(models.Model, RevisionMixin):
return (self.status == self.BOOKED or self.status == self.CONFIRMED)
@property
- def authorised(self):
- return not self.internal and self.purchase_order or self.authorisation.amount == self.total
+ def hs_done(self):
+ return self.riskassessment is not None and len(self.checklists.all()) > 0
@property
def has_start_time(self):
@@ -457,44 +459,59 @@ class Event(models.Model, RevisionMixin):
@property
def internal(self):
- return self.organisation and self.organisation.union_account
+ return bool(self.organisation and self.organisation.union_account)
+
+ @property
+ def authorised(self):
+ if self.internal:
+ return self.authorisation.amount == self.total
+ else:
+ return bool(self.purchase_order)
objects = EventManager()
def get_absolute_url(self):
- return reverse_lazy('event_detail', kwargs={'pk': self.pk})
+ return reverse('event_detail', kwargs={'pk': self.pk})
def __str__(self):
- return str(self.pk) + ": " + self.name
+ return "{}: {}".format(self.display_id, self.name)
def clean(self):
+ errdict = {}
if self.end_date and self.start_date > self.end_date:
- raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.')
+ errdict['end_date'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
startEndSameDay = not self.end_date or self.end_date == self.start_date
hasStartAndEnd = self.has_start_time and self.has_end_time
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
- raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.')
+ errdict['end_time'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
+
+ if self.access_at is not None:
+ if self.access_at.date() > self.start_date:
+ errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
+ elif self.start_time is not None and self.start_date == self.access_at.date() and self.access_at.time() > self.start_time:
+ errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
+
+ if errdict != {}: # If there was an error when validation
+ raise ValidationError(errdict)
def save(self, *args, **kwargs):
"""Call :meth:`full_clean` before saving."""
self.full_clean()
super(Event, self).save(*args, **kwargs)
- class Meta:
- permissions = (
- ('view_event', 'Can view Events'),
- )
-
-class EventItem(models.Model):
+@reversion.register
+class EventItem(models.Model, RevisionMixin):
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
- description = models.TextField(blank=True, null=True)
+ description = models.TextField(blank=True, default='')
quantity = models.IntegerField()
cost = models.DecimalField(max_digits=10, decimal_places=2)
order = models.IntegerField()
+ reversion_hide = True
+
@property
def total_cost(self):
return self.cost * self.quantity
@@ -503,16 +520,11 @@ class EventItem(models.Model):
ordering = ['order']
def __str__(self):
- return str(self.event.pk) + "." + str(self.order) + ": " + self.event.name + " | " + self.name
+ return "{}.{}: {} | {}".format(self.event_id, self.order, self.event.name, self.name)
-
-class EventCrew(models.Model):
- event = models.ForeignKey('Event', related_name='crew', on_delete=models.CASCADE)
- user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
- rig = models.BooleanField(default=False)
- run = models.BooleanField(default=False)
- derig = models.BooleanField(default=False)
- notes = models.TextField(blank=True, null=True)
+ @property
+ def activity_feed_string(self):
+ return str("item {}".format(self.name))
@reversion.register
@@ -520,25 +532,46 @@ class EventAuthorisation(models.Model, RevisionMixin):
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
email = models.EmailField()
name = models.CharField(max_length=255)
- uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID")
- account_code = models.CharField(max_length=50, blank=True, null=True)
+ uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID")
+ account_code = models.CharField(max_length=50, default='', blank=True)
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
- sent_by = models.ForeignKey('RIGS.Profile', on_delete=models.CASCADE)
+ sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
def get_absolute_url(self):
- return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
+ return reverse('event_detail', kwargs={'pk': self.event_id})
@property
def activity_feed_string(self):
- return str("N%05d" % self.event.pk + ' (requested by ' + self.sent_by.initials + ')')
+ return "{} (requested by {})".format(self.event.display_id, self.sent_by.initials)
-@python_2_unicode_compatible
-class Invoice(models.Model):
+class InvoiceManager(models.Manager):
+ def outstanding_invoices(self):
+ # Manual query is the only way I have found to do this efficiently. Not ideal but needs must
+ sql = "SELECT * FROM " \
+ "(SELECT " \
+ "(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
+ "(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
+ "(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
+ "\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
+ "AS sub " \
+ "WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
+ "ORDER BY invoice_date"
+
+ query = self.raw(sql)
+ return query
+
+
+@reversion.register(follow=['payment_set'])
+class Invoice(models.Model, RevisionMixin):
event = models.OneToOneField('Event', on_delete=models.CASCADE)
invoice_date = models.DateField(auto_now_add=True)
void = models.BooleanField(default=False)
+ reversion_perm = 'RIGS.view_invoice'
+
+ objects = InvoiceManager()
+
@property
def sum_total(self):
return self.event.sum_total
@@ -562,18 +595,26 @@ class Invoice(models.Model):
def is_closed(self):
return self.balance == 0 or self.void
+ def get_absolute_url(self):
+ return reverse('invoice_detail', kwargs={'pk': self.pk})
+
+ @property
+ def activity_feed_string(self):
+ return "#{} for Event {}".format(self.display_id, self.event.display_id)
+
def __str__(self):
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
+ @property
+ def display_id(self):
+ return "{:05d}".format(self.pk)
+
class Meta:
- permissions = (
- ('view_invoice', 'Can view Invoices'),
- )
ordering = ['-invoice_date']
-@python_2_unicode_compatible
-class Payment(models.Model):
+@reversion.register
+class Payment(models.Model, RevisionMixin):
CASH = 'C'
INTERNAL = 'I'
EXTERNAL = 'E'
@@ -590,7 +631,248 @@ class Payment(models.Model):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
date = models.DateField()
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
- method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True)
+ method = models.CharField(max_length=2, choices=METHODS, default='', blank=True)
+
+ reversion_hide = True
def __str__(self):
return "%s: %d" % (self.get_method_display(), self.amount)
+
+ @property
+ def activity_feed_string(self):
+ return str("payment of £{}".format(self.amount))
+
+
+def validate_url(value):
+ if not value:
+ return # Required error is done the field
+ obj = urlparse(value)
+ if obj.hostname not in ('nottinghamtec.sharepoint.com'):
+ raise ValidationError('URL must point to a location on the TEC Sharepoint')
+
+
+@reversion.register
+class RiskAssessment(models.Model, RevisionMixin):
+ SMALL = (0, 'Small')
+ MEDIUM = (1, 'Medium')
+ LARGE = (2, 'Large')
+ SIZES = (SMALL, MEDIUM, LARGE)
+
+ event = models.OneToOneField('Event', on_delete=models.CASCADE)
+ # General
+ nonstandard_equipment = models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by "
+ "TEC's standard risk assessments and method statements?")
+ nonstandard_use = models.BooleanField(help_text="Are TEC using their equipment in a way that is abnormal? i.e. Not covered by TECs standard health and safety documentation")
+ contractors = models.BooleanField(help_text="Are you using any external contractors? i.e. Freelancers/Crewing Companies")
+ other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site? e.g. TEC is providing the lighting while another company does sound")
+ crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?")
+ general_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
+
+ # Power
+ big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
+ # If yes to the above two, you must answer...
+ power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
+ verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person must be a Power Technician or Power Supervisor)")
+ outside = models.BooleanField(help_text="Is the event outdoors?")
+ generators = models.BooleanField(help_text="Will generators be used?")
+ other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?")
+ nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?")
+ multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?")
+ power_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
+ power_plan = models.URLField(blank=True, default='', help_text="Upload your power plan to the Sharepoint and submit a link", validators=[validate_url])
+
+ # Sound
+ noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?")
+ sound_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
+
+ # Site
+ known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
+ safe_loading = models.BooleanField(help_text="Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)")
+ safe_storage = models.BooleanField(help_text="Are there any problems with safe and secure equipment storage?")
+ area_outside_of_control = models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")
+ barrier_required = models.BooleanField(help_text="Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?")
+ nonstandard_emergency_procedure = models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")
+
+ # Structures
+ special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?")
+ suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")
+ persons_responsible_structures = models.TextField(blank=True, default='', help_text="Who are the persons on site responsible for their use?")
+ rigging_plan = models.URLField(blank=True, default='', help_text="Upload your rigging plan to the Sharepoint and submit a link", validators=[validate_url])
+
+ # Blimey that was a lot of options
+
+ reviewed_at = models.DateTimeField(null=True)
+ reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
+ verbose_name="Reviewer", on_delete=models.CASCADE)
+
+ supervisor_consulted = models.BooleanField(null=True)
+
+ expected_values = {
+ 'nonstandard_equipment': False,
+ 'nonstandard_use': False,
+ 'contractors': False,
+ 'other_companies': False,
+ 'crew_fatigue': False,
+ 'big_power': False,
+ 'generators': False,
+ 'other_companies_power': False,
+ 'nonstandard_equipment_power': False,
+ 'multiple_electrical_environments': False,
+ 'noise_monitoring': False,
+ 'known_venue': False,
+ 'safe_loading': False,
+ 'safe_storage': False,
+ 'area_outside_of_control': False,
+ 'barrier_required': False,
+ 'nonstandard_emergency_procedure': False,
+ 'special_structures': False,
+ 'suspended_structures': False,
+ }
+ inverted_fields = {key: value for (key, value) in expected_values.items() if not value}.keys()
+
+ def clean(self):
+ # Check for idiots
+ if not self.outside and self.generators:
+ raise forms.ValidationError("Engage brain, please. No generators indoors!(!)")
+
+ class Meta:
+ ordering = ['event']
+ permissions = [
+ ('review_riskassessment', 'Can review Risk Assessments')
+ ]
+
+ @cached_property
+ def fieldz(self):
+ return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
+
+ @property
+ def event_size(self):
+ # Confirm event size. Check all except generators, since generators entails outside
+ if self.outside or self.other_companies_power or self.nonstandard_equipment_power or self.multiple_electrical_environments:
+ return self.LARGE[0]
+ elif self.big_power:
+ return self.MEDIUM[0]
+ else:
+ return self.SMALL[0]
+
+ @property
+ def activity_feed_string(self):
+ return str(self.event)
+
+ def get_absolute_url(self):
+ return reverse('ra_detail', kwargs={'pk': self.pk})
+
+ def __str__(self):
+ return "%i - %s" % (self.pk, self.event)
+
+
+@reversion.register(follow=['vehicles', 'crew'])
+class EventChecklist(models.Model, RevisionMixin):
+ event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
+
+ # General
+ power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists',
+ verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?")
+ venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
+ date = models.DateField()
+
+ # Safety Checks
+ safe_parking = models.BooleanField(blank=True, null=True, help_text="Vehicles parked safely? (does not obstruct venue access)")
+ safe_packing = models.BooleanField(blank=True, null=True, help_text="Equipment packed away safely? (including flightcases)")
+ exits = models.BooleanField(blank=True, null=True, help_text="Emergency exits clear?")
+ trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?")
+ warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place? (strobe, smoke, power etc.)")
+ ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?")
+ hs_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of Safety Bag/Box")
+ extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers")
+
+ # Small Electrical Checks
+ rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
+ supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested? (using socket tester)")
+
+ # Shared electrical checks
+ earthing = models.BooleanField(blank=True, null=True, help_text="Equipment appropriately earthed? (truss, stage, generators etc)")
+ pat = models.BooleanField(blank=True, null=True, help_text="All equipment in PAT period?")
+
+ # Medium Electrical Checks
+ source_rcd = models.BooleanField(blank=True, null=True, help_text="Source RCD protected? (if cable is more than 3m long) ")
+ labelling = models.BooleanField(blank=True, null=True, help_text="Appropriate and clear labelling on distribution and cabling?")
+ # First Distro
+ fd_voltage_l1 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L1-N", help_text="L1 - N")
+ fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N")
+ fd_voltage_l3 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N")
+ fd_phase_rotation = models.BooleanField(blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation (if required)")
+ fd_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (ZS)")
+ fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
+ # Worst case points
+ w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
+ w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
+ w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
+ w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (ZS)")
+ w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
+ w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
+ w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
+ w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (ZS)")
+ w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
+ w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
+ w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
+ w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=5, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (ZS)")
+
+ all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested? (using test button)")
+ public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested? (using socket tester)")
+
+ reviewed_at = models.DateTimeField(null=True)
+ reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
+ verbose_name="Reviewer", on_delete=models.CASCADE)
+
+ inverted_fields = []
+
+ class Meta:
+ ordering = ['event']
+ permissions = [
+ ('review_eventchecklist', 'Can review Event Checklists')
+ ]
+
+ @cached_property
+ def fieldz(self):
+ return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
+
+ @property
+ def activity_feed_string(self):
+ return str(self.event)
+
+ def get_absolute_url(self):
+ return reverse('ec_detail', kwargs={'pk': self.pk})
+
+ def __str__(self):
+ return "%i - %s" % (self.pk, self.event)
+
+
+@reversion.register
+class EventChecklistVehicle(models.Model, RevisionMixin):
+ checklist = models.ForeignKey('EventChecklist', related_name='vehicles', blank=True, on_delete=models.CASCADE)
+ vehicle = models.CharField(max_length=255)
+ driver = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='vehicles', on_delete=models.CASCADE)
+
+ reversion_hide = True
+
+ def __str__(self):
+ return "{} driven by {}".format(self.vehicle, str(self.driver))
+
+
+@reversion.register
+class EventChecklistCrew(models.Model, RevisionMixin):
+ checklist = models.ForeignKey('EventChecklist', related_name='crew', blank=True, on_delete=models.CASCADE)
+ crewmember = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='crewed', on_delete=models.CASCADE)
+ role = models.CharField(max_length=255)
+ start = models.DateTimeField()
+ end = models.DateTimeField()
+
+ reversion_hide = True
+
+ def clean(self):
+ if self.start > self.end:
+ raise ValidationError('Unless you\'ve invented time travel, crew can\'t finish before they have started.')
+
+ def __str__(self):
+ return "{} ({})".format(str(self.crewmember), self.role)
diff --git a/RIGS/rigboard.py b/RIGS/rigboard.py
index abb0ed9e..9465a2a2 100644
--- a/RIGS/rigboard.py
+++ b/RIGS/rigboard.py
@@ -1,52 +1,53 @@
-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 copy
import datetime
import re
-import copy
+import urllib.error
+import urllib.parse
+import urllib.request
+from io import BytesIO
+
+import premailer
+import simplejson
+from PyPDF2 import PdfFileMerger, PdfFileReader
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.staticfiles import finders
+from django.core import signing
+from django.core.exceptions import SuspiciousOperation
+from django.core.mail import EmailMultiAlternatives
+from django.db.models import Q
+from django.http import HttpResponse
+from django.shortcuts import get_object_or_404
+from django.template.loader import get_template
+from django.urls import reverse
+from django.urls import reverse_lazy
+from django.utils import timezone
+from django.utils.decorators import method_decorator
+from django.views import generic
+from z3c.rml import rml2pdf
+
+from PyRIGS import decorators
+from PyRIGS.views import OEmbedView, is_ajax
+from RIGS import models, forms
__author__ = 'ghost'
class RigboardIndex(generic.TemplateView):
- template_name = 'RIGS/rigboard.html'
+ template_name = 'rigboard.html'
def get_context_data(self, **kwargs):
# get super context
context = super(RigboardIndex, self).get_context_data(**kwargs)
# call out method to get current events
- context['events'] = models.Event.objects.current_events()
+ context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists')
+ context['page_title'] = "Rigboard"
return context
class WebCalendar(generic.TemplateView):
- template_name = 'RIGS/calendar.html'
+ template_name = 'calendar.html'
def get_context_data(self, **kwargs):
context = super(WebCalendar, self).get_context_data(**kwargs)
@@ -56,61 +57,40 @@ class WebCalendar(generic.TemplateView):
class EventDetail(generic.DetailView):
+ template_name = 'event_detail.html'
model = models.Event
-
-class EventOembed(generic.View):
- model = models.Event
-
- def get(self, request, pk=None):
- embed_url = reverse('event_embed', args=[pk])
- full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
-
- data = {
- 'html': ''.format(full_url),
- 'version': '1.0',
- 'type': 'rich',
- 'height': '250'
- }
-
- json = simplejson.JSONEncoderForHTML().encode(data)
- return HttpResponse(json, content_type="application/json")
+ def get_context_data(self, **kwargs):
+ context = super(EventDetail, self).get_context_data(**kwargs)
+ title = "{} | {}".format(self.object.display_id, self.object.name)
+ if self.object.dry_hire:
+ title += " Dry Hire"
+ context['page_title'] = title
+ return context
class EventEmbed(EventDetail):
- template_name = 'RIGS/event_embed.html'
+ template_name = 'event_embed.html'
-class EventRA(generic.base.RedirectView):
- permanent = False
-
- 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 EventOEmbed(OEmbedView):
+ model = models.Event
+ url_name = 'event_embed'
class EventCreate(generic.CreateView):
model = models.Event
form_class = forms.EventForm
+ template_name = 'event_form.html'
def get_context_data(self, **kwargs):
context = super(EventCreate, self).get_context_data(**kwargs)
+ context['page_title'] = "New Event"
context['edit'] = True
context['currentVAT'] = models.VatRate.objects.current_rate()
form = context['form']
- if re.search('"-\d+"', form['items_json'].value()):
+ if hasattr(form, 'items_json') and re.search(r'"-\d+"', form['items_json'].value()):
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
@@ -127,9 +107,11 @@ class EventCreate(generic.CreateView):
class EventUpdate(generic.UpdateView):
model = models.Event
form_class = forms.EventForm
+ template_name = 'event_form.html'
def get_context_data(self, **kwargs):
context = super(EventUpdate, self).get_context_data(**kwargs)
+ context['page_title'] = "Event {}".format(self.object.display_id)
context['edit'] = True
form = context['form']
@@ -143,13 +125,15 @@ class EventUpdate(generic.UpdateView):
return context
def render_to_response(self, context, **response_kwargs):
- if not hasattr(context, 'duplicate'):
+ if hasattr(context, 'duplicate') and not context['duplicate']:
# If this event has already been emailed to a client, show a warning
if self.object.auth_request_at is not None:
- messages.info(self.request, 'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.')
+ messages.info(self.request,
+ 'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.')
if hasattr(self.object, 'authorised'):
- messages.warning(self.request, 'This event has already been authorised by client, any changes to price will require reauthorisation.')
+ messages.warning(self.request,
+ '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)
def get_success_url(self):
@@ -162,13 +146,15 @@ class EventDuplicate(EventUpdate):
new = copy.copy(old) # Make a copy of the object in memory
new.based_on = old # Make the new event based on the old event
new.purchase_order = None # Remove old PO
+ new.status = new.PROVISIONAL # Return status to provisional
# Clear checked in by if it's a dry hire
if new.dry_hire is True:
new.checked_in_by = None
+ new.collector = None
# Remove all the authorisation information from the new event
- new.auth_request_to = None
+ new.auth_request_to = ''
new.auth_request_by = None
new.auth_request_at = None
@@ -182,6 +168,7 @@ class EventDuplicate(EventUpdate):
def get_context_data(self, **kwargs):
context = super(EventDuplicate, self).get_context_data(**kwargs)
+ context['page_title'] = "Duplicate of Event {}".format(self.object.display_id)
context["duplicate"] = True
return context
@@ -189,24 +176,18 @@ class EventDuplicate(EventUpdate):
class EventPrint(generic.View):
def get(self, request, pk):
object = get_object_or_404(models.Event, pk=pk)
- template = get_template('RIGS/event_print.xml')
+ template = get_template('event_print.xml')
merger = PdfFileMerger()
context = {
'object': object,
- 'fonts': {
- 'opensans': {
- 'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
- 'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
- }
- },
'quote': True,
'current_user': request.user,
+ 'filename': 'Event_{}_{}_{}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
}
rml = template.render(context)
-
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
@@ -218,19 +199,26 @@ class EventPrint(generic.View):
merger.write(merged)
response = HttpResponse(content_type='application/pdf')
-
- escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
-
- response['Content-Disposition'] = "filename=N%05d | %s.pdf" % (object.pk, escapedEventName)
+ response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
response.write(merged.getvalue())
return response
-class EventArchive(generic.ArchiveIndexView):
+class EventArchive(generic.ListView):
+ template_name = "event_archive.html"
model = models.Event
- date_field = "start_date"
paginate_by = 25
+ def get_context_data(self, **kwargs):
+ # get super context
+ context = super(EventArchive, self).get_context_data(**kwargs)
+
+ context['start'] = self.request.GET.get('start', None)
+ context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d'))
+ context['statuses'] = models.Event.EVENT_STATUS_CHOICES
+ context['page_title'] = 'Event Archive'
+ return context
+
def get_queryset(self):
start = self.request.GET.get('start', None)
end = self.request.GET.get('end', datetime.date.today())
@@ -241,19 +229,39 @@ class EventArchive(generic.ArchiveIndexView):
"Muppet! Check the dates, it has been fixed for you.")
start, end = end, start # Stop the impending fail
- filter = False
+ filter = Q()
if end != "":
- filter = Q(start_date__lte=end)
+ filter &= Q(start_date__lte=end)
if start:
- if filter:
- filter = filter & Q(start_date__gte=start)
- else:
- filter = Q(start_date__gte=start)
+ filter &= Q(start_date__gte=start)
- if filter:
- qs = self.model.objects.filter(filter).order_by('-start_date')
- else:
- qs = self.model.objects.all().order_by('-start_date')
+ q = self.request.GET.get('q', "")
+
+ if q != "":
+ qfilter = Q(name__icontains=q) | Q(description__icontains=q) | Q(notes__icontains=q)
+
+ # try and parse an int
+ try:
+ val = int(q)
+ qfilter = qfilter | Q(pk=val)
+ except: # noqa not an integer
+ pass
+
+ try:
+ if q[0] == "N":
+ val = int(q[1:])
+ qfilter = Q(pk=val) # If string is N###### then do a simple PK filter
+ except: # noqa
+ pass
+
+ filter &= qfilter
+
+ status = self.request.GET.getlist('status', "")
+
+ if len(status) > 0:
+ filter &= Q(status__in=status)
+
+ qs = self.model.objects.filter(filter).order_by('-start_date')
# Preselect related for efficiency
qs.select_related('person', 'organisation', 'venue', 'mic')
@@ -265,8 +273,9 @@ class EventArchive(generic.ArchiveIndexView):
class EventAuthorise(generic.UpdateView):
- template_name = 'RIGS/eventauthorisation_form.html'
- success_template = 'RIGS/eventauthorisation_success.html'
+ template_name = 'eventauthorisation_form.html'
+ success_template = 'eventauthorisation_success.html'
+ preview = False
def form_valid(self, form):
self.object = form.save()
@@ -274,7 +283,7 @@ class EventAuthorise(generic.UpdateView):
self.template_name = self.success_template
messages.add_message(self.request, messages.SUCCESS,
'Success! Your event has been authorised. ' +
- 'You will also receive email confirmation to %s.' % (self.object.email))
+ 'You will also receive email confirmation to %s.' % self.object.email)
return self.render_to_response(self.get_context_data())
@property
@@ -290,8 +299,11 @@ class EventAuthorise(generic.UpdateView):
def get_context_data(self, **kwargs):
context = super(EventAuthorise, self).get_context_data(**kwargs)
context['event'] = self.event
-
context['tos_url'] = settings.TERMS_OF_HIRE_URL
+ context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name)
+ if self.event.dry_hire:
+ context['page_title'] += ' Dry Hire'
+ context['preview'] = self.preview
return context
def get(self, request, *args, **kwargs):
@@ -329,7 +341,7 @@ class EventAuthorise(generic.UpdateView):
class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin):
model = models.Event
form_class = forms.EventAuthorisationRequestForm
- template_name = 'RIGS/eventauthorisation_request.html'
+ template_name = 'eventauthorisation_request.html'
@method_decorator(decorators.nottinghamtec_address_required)
def dispatch(self, *args, **kwargs):
@@ -340,7 +352,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
return self.get_object()
def get_success_url(self):
- if self.request.is_ajax():
+ if is_ajax(self.request):
url = reverse_lazy('closemodal')
messages.info(self.request, "location.reload()")
else:
@@ -354,7 +366,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
email = form.cleaned_data['email']
event = self.object
event.auth_request_by = self.request.user
- event.auth_request_at = datetime.datetime.now()
+ event.auth_request_at = timezone.now()
event.auth_request_to = email
event.save()
@@ -374,12 +386,12 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
msg = EmailMultiAlternatives(
"N%05d | %s - Event Authorisation Request" % (self.object.pk, self.object.name),
- get_template("RIGS/eventauthorisation_client_request.txt").render(context),
+ get_template("eventauthorisation_client_request.txt").render(context),
to=[email],
reply_to=[self.request.user.email],
)
- css = staticfiles_storage.path('css/email.css')
- html = premailer.Premailer(get_template("RIGS/eventauthorisation_client_request.html").render(context),
+ css = finders.find('css/email.css')
+ html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
external_styles=css).transform()
msg.attach_alternative(html, 'text/html')
@@ -389,12 +401,11 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
class EventAuthoriseRequestEmailPreview(generic.DetailView):
- template_name = "RIGS/eventauthorisation_client_request.html"
+ template_name = "eventauthorisation_client_request.html"
model = models.Event
def render_to_response(self, context, **response_kwargs):
- from django.contrib.staticfiles.storage import staticfiles_storage
- css = staticfiles_storage.path('css/email.css')
+ css = finders.find('css/email.css')
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
assert isinstance(response, HttpResponse)
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
@@ -408,28 +419,5 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
'sent_by': self.request.user.pk,
})
context['to_name'] = self.request.GET.get('to_name', None)
+ context['target'] = 'event_authorise_form_preview'
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)
diff --git a/RIGS/signals.py b/RIGS/signals.py
index ea0395d7..e1fc37b0 100644
--- a/RIGS/signals.py
+++ b/RIGS/signals.py
@@ -1,16 +1,21 @@
import re
-import urllib.request
import urllib.error
import urllib.parse
+import urllib.request
from io import BytesIO
-from django.db.models.signals import post_save
from PyPDF2 import PdfFileReader, PdfFileMerger
from django.conf import settings
-from django.contrib.staticfiles.storage import staticfiles_storage
+from django.contrib.staticfiles import finders
+from django.core.cache import cache
from django.core.mail import EmailMessage, EmailMultiAlternatives
+from django.db.models.signals import post_save
from django.template.loader import get_template
+from django.urls import reverse
+from django.utils import timezone
from premailer import Premailer
+from registration.signals import user_activated
+from reversion import revisions as reversion
from z3c.rml import rml2pdf
from RIGS import models
@@ -20,17 +25,11 @@ def send_eventauthorisation_success_email(instance):
# Generate PDF first to prevent context conflicts
context = {
'object': instance.event,
- 'fonts': {
- 'opensans': {
- 'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
- 'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
- }
- },
'receipt': True,
'current_user': False,
}
- template = get_template('RIGS/event_print.xml')
+ template = get_template('event_print.xml')
merger = PdfFileMerger()
rml = template.render(context)
@@ -59,17 +58,17 @@ def send_eventauthorisation_success_email(instance):
client_email = EmailMultiAlternatives(
subject,
- get_template("RIGS/eventauthorisation_client_success.txt").render(context),
+ get_template("eventauthorisation_client_success.txt").render(context),
to=[instance.email],
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
)
- css = staticfiles_storage.path('css/email.css')
- html = Premailer(get_template("RIGS/eventauthorisation_client_success.html").render(context),
+ css = finders.find('css/email.css')
+ html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
external_styles=css).transform()
client_email.attach_alternative(html, 'text/html')
- escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', instance.event.name)
+ escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
client_email.attach('N%05d - %s - CONFIRMATION.pdf' % (instance.event.pk, escapedEventName),
merged.getvalue(),
@@ -83,7 +82,7 @@ def send_eventauthorisation_success_email(instance):
mic_email = EmailMessage(
subject,
- get_template("RIGS/eventauthorisation_mic_success.txt").render(context),
+ get_template("eventauthorisation_mic_success.txt").render(context),
to=[mic_email_address]
)
@@ -102,3 +101,43 @@ def on_revision_commit(sender, instance, created, **kwargs):
post_save.connect(on_revision_commit, sender=models.EventAuthorisation)
+
+
+def send_admin_awaiting_approval_email(user, request, **kwargs):
+ # Bit more controlled than just emailing all superusers
+ for admin in models.Profile.admins():
+ # Check we've ever emailed them before and if so, if cooldown has passed.
+ if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now():
+ context = {
+ 'request': request,
+ 'link_suffix': reverse("admin:RIGS_profile_changelist") + '?is_approved__exact=0',
+ 'number_of_users': models.Profile.users_awaiting_approval_count(),
+ 'to_name': admin.first_name
+ }
+
+ email = EmailMultiAlternatives(
+ "%s new users awaiting approval on RIGS" % (context['number_of_users']),
+ get_template("admin_awaiting_approval.txt").render(context),
+ to=[admin.email],
+ reply_to=[user.email],
+ )
+ css = finders.find('css/email.css')
+ html = Premailer(get_template("admin_awaiting_approval.html").render(context),
+ external_styles=css).transform()
+ email.attach_alternative(html, 'text/html')
+ email.send()
+
+ # Update last sent
+ admin.last_emailed = timezone.now()
+ admin.save()
+
+
+user_activated.connect(send_admin_awaiting_approval_email)
+
+
+def update_cache(sender, instance, created, **kwargs):
+ cache.clear()
+
+
+for model in reversion.get_registered_models():
+ post_save.connect(update_cache, sender=model)
diff --git a/RIGS/static/config.rb b/RIGS/static/config.rb
deleted file mode 100644
index 7c3e52b4..00000000
--- a/RIGS/static/config.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# Require any additional compass plugins here.
-require 'bootstrap-sass'
-
-# Set this to the root of your project when deployed:
-http_path = "/static/"
-css_dir = "css"
-sass_dir = "scss"
-images_dir = "img"
-javascripts_dir = "js"
-fonts_dir = "fonts"
-
-# You can select your preferred output style here (can be overridden via the command line):
-# output_style = :expanded or :nested or :compact or :compressed
-output_style = :compressed
-
-# To enable relative paths to assets via compass helper functions. Uncomment:
-# relative_assets = true
-
-# To disable debugging comments that display the original location of your selectors. Uncomment:
-# line_comments = false
-
-
-# If you prefer the indented syntax, you might want to regenerate this
-# project again passing --syntax sass, or you can uncomment this:
-# preferred_syntax = :sass
-# and then run:
-# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass
diff --git a/RIGS/static/css/ajax-bootstrap-select.css b/RIGS/static/css/ajax-bootstrap-select.css
deleted file mode 100755
index a7c010b2..00000000
--- a/RIGS/static/css/ajax-bootstrap-select.css
+++ /dev/null
@@ -1,27 +0,0 @@
-/*!
- * Ajax Bootstrap Select
- *
- * Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
- *
- * @version 1.3.1
- * @author Adam Heim - https://github.com/truckingsim
- * @link https://github.com/truckingsim/Ajax-Bootstrap-Select
- * @copyright 2015 Adam Heim
- * @license Released under the MIT license.
- *
- * Contributors:
- * Mark Carver - https://github.com/markcarver
- *
- * Last build: 2015-01-06 8:43:11 PM EST
- */
-.bootstrap-select .status {
- background: #f0f0f0;
- clear: both;
- color: #999;
- font-size: 11px;
- font-style: italic;
- font-weight: 500;
- line-height: 1;
- margin-bottom: -5px;
- padding: 10px 20px;
-}
diff --git a/RIGS/static/css/bootstrap-datetimepicker.min.css b/RIGS/static/css/bootstrap-datetimepicker.min.css
deleted file mode 100644
index c7021619..00000000
--- a/RIGS/static/css/bootstrap-datetimepicker.min.css
+++ /dev/null
@@ -1,366 +0,0 @@
-/*!
- * Datetimepicker for Bootstrap 3
- * ! version : 4.7.14
- * https://github.com/Eonasdan/bootstrap-datetimepicker/
- */
-.bootstrap-datetimepicker-widget {
- list-style: none;
-}
-.bootstrap-datetimepicker-widget.dropdown-menu {
- margin: 2px 0;
- padding: 4px;
- width: 19em;
-}
-@media (min-width: 768px) {
- .bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
- width: 38em;
- }
-}
-@media (min-width: 992px) {
- .bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
- width: 38em;
- }
-}
-@media (min-width: 1200px) {
- .bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
- width: 38em;
- }
-}
-.bootstrap-datetimepicker-widget.dropdown-menu:before,
-.bootstrap-datetimepicker-widget.dropdown-menu:after {
- content: '';
- display: inline-block;
- position: absolute;
-}
-.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before {
- border-left: 7px solid transparent;
- border-right: 7px solid transparent;
- border-bottom: 7px solid #cccccc;
- border-bottom-color: rgba(0, 0, 0, 0.2);
- top: -7px;
- left: 7px;
-}
-.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
- border-left: 6px solid transparent;
- border-right: 6px solid transparent;
- border-bottom: 6px solid white;
- top: -6px;
- left: 8px;
-}
-.bootstrap-datetimepicker-widget.dropdown-menu.top:before {
- border-left: 7px solid transparent;
- border-right: 7px solid transparent;
- border-top: 7px solid #cccccc;
- border-top-color: rgba(0, 0, 0, 0.2);
- bottom: -7px;
- left: 6px;
-}
-.bootstrap-datetimepicker-widget.dropdown-menu.top:after {
- border-left: 6px solid transparent;
- border-right: 6px solid transparent;
- border-top: 6px solid white;
- bottom: -6px;
- left: 7px;
-}
-.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before {
- left: auto;
- right: 6px;
-}
-.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after {
- left: auto;
- right: 7px;
-}
-.bootstrap-datetimepicker-widget .list-unstyled {
- margin: 0;
-}
-.bootstrap-datetimepicker-widget a[data-action] {
- padding: 6px 0;
-}
-.bootstrap-datetimepicker-widget a[data-action]:active {
- box-shadow: none;
-}
-.bootstrap-datetimepicker-widget .timepicker-hour,
-.bootstrap-datetimepicker-widget .timepicker-minute,
-.bootstrap-datetimepicker-widget .timepicker-second {
- width: 54px;
- font-weight: bold;
- font-size: 1.2em;
- margin: 0;
-}
-.bootstrap-datetimepicker-widget button[data-action] {
- padding: 6px;
-}
-.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
- content: "Increment Hours";
-}
-.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
- content: "Increment Minutes";
-}
-.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
- content: "Decrement Hours";
-}
-.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
- content: "Decrement Minutes";
-}
-.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
- content: "Show Hours";
-}
-.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
- content: "Show Minutes";
-}
-.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
- content: "Toggle AM/PM";
-}
-.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
- content: "Clear the picker";
-}
-.bootstrap-datetimepicker-widget .btn[data-action="today"]::after {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
- content: "Set the date to today";
-}
-.bootstrap-datetimepicker-widget .picker-switch {
- text-align: center;
-}
-.bootstrap-datetimepicker-widget .picker-switch::after {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
- content: "Toggle Date and Time Screens";
-}
-.bootstrap-datetimepicker-widget .picker-switch td {
- padding: 0;
- margin: 0;
- height: auto;
- width: auto;
- line-height: inherit;
-}
-.bootstrap-datetimepicker-widget .picker-switch td span {
- line-height: 2.5;
- height: 2.5em;
- width: 100%;
-}
-.bootstrap-datetimepicker-widget table {
- width: 100%;
- margin: 0;
-}
-.bootstrap-datetimepicker-widget table td,
-.bootstrap-datetimepicker-widget table th {
- text-align: center;
- border-radius: 4px;
-}
-.bootstrap-datetimepicker-widget table th {
- height: 20px;
- line-height: 20px;
- width: 20px;
-}
-.bootstrap-datetimepicker-widget table th.picker-switch {
- width: 145px;
-}
-.bootstrap-datetimepicker-widget table th.disabled,
-.bootstrap-datetimepicker-widget table th.disabled:hover {
- background: none;
- color: #777777;
- cursor: not-allowed;
-}
-.bootstrap-datetimepicker-widget table th.prev::after {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
- content: "Previous Month";
-}
-.bootstrap-datetimepicker-widget table th.next::after {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
- content: "Next Month";
-}
-.bootstrap-datetimepicker-widget table thead tr:first-child th {
- cursor: pointer;
-}
-.bootstrap-datetimepicker-widget table thead tr:first-child th:hover {
- background: #eeeeee;
-}
-.bootstrap-datetimepicker-widget table td {
- height: 54px;
- line-height: 54px;
- width: 54px;
-}
-.bootstrap-datetimepicker-widget table td.cw {
- font-size: .8em;
- height: 20px;
- line-height: 20px;
- color: #777777;
-}
-.bootstrap-datetimepicker-widget table td.day {
- height: 20px;
- line-height: 20px;
- width: 20px;
-}
-.bootstrap-datetimepicker-widget table td.day:hover,
-.bootstrap-datetimepicker-widget table td.hour:hover,
-.bootstrap-datetimepicker-widget table td.minute:hover,
-.bootstrap-datetimepicker-widget table td.second:hover {
- background: #eeeeee;
- cursor: pointer;
-}
-.bootstrap-datetimepicker-widget table td.old,
-.bootstrap-datetimepicker-widget table td.new {
- color: #777777;
-}
-.bootstrap-datetimepicker-widget table td.today {
- position: relative;
-}
-.bootstrap-datetimepicker-widget table td.today:before {
- content: '';
- display: inline-block;
- border: 0 0 7px 7px solid transparent;
- border-bottom-color: #337ab7;
- border-top-color: rgba(0, 0, 0, 0.2);
- position: absolute;
- bottom: 4px;
- right: 4px;
-}
-.bootstrap-datetimepicker-widget table td.active,
-.bootstrap-datetimepicker-widget table td.active:hover {
- background-color: #337ab7;
- color: #ffffff;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
-}
-.bootstrap-datetimepicker-widget table td.active.today:before {
- border-bottom-color: #fff;
-}
-.bootstrap-datetimepicker-widget table td.disabled,
-.bootstrap-datetimepicker-widget table td.disabled:hover {
- background: none;
- color: #777777;
- cursor: not-allowed;
-}
-.bootstrap-datetimepicker-widget table td span {
- display: inline-block;
- width: 54px;
- height: 54px;
- line-height: 54px;
- margin: 2px 1.5px;
- cursor: pointer;
- border-radius: 4px;
-}
-.bootstrap-datetimepicker-widget table td span:hover {
- background: #eeeeee;
-}
-.bootstrap-datetimepicker-widget table td span.active {
- background-color: #337ab7;
- color: #ffffff;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
-}
-.bootstrap-datetimepicker-widget table td span.old {
- color: #777777;
-}
-.bootstrap-datetimepicker-widget table td span.disabled,
-.bootstrap-datetimepicker-widget table td span.disabled:hover {
- background: none;
- color: #777777;
- cursor: not-allowed;
-}
-.bootstrap-datetimepicker-widget.usetwentyfour td.hour {
- height: 27px;
- line-height: 27px;
-}
-.input-group.date .input-group-addon {
- cursor: pointer;
-}
-.sr-only {
- position: absolute;
- width: 1px;
- height: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- border: 0;
-}
diff --git a/RIGS/static/css/bootstrap-select.min.css b/RIGS/static/css/bootstrap-select.min.css
deleted file mode 100755
index 9d96ebb3..00000000
--- a/RIGS/static/css/bootstrap-select.min.css
+++ /dev/null
@@ -1,6 +0,0 @@
-/*!
- * Bootstrap-select v1.12.4 (http://silviomoreto.github.io/bootstrap-select)
- *
- * Copyright 2013-2017 bootstrap-select
- * Licensed under MIT (https://github.com/silviomoreto/bootstrap-select/blob/master/LICENSE)
- */select.bs-select-hidden,select.selectpicker{display:none!important}.bootstrap-select{width:220px\9}.bootstrap-select>.dropdown-toggle{width:100%;padding-right:25px;z-index:1}.bootstrap-select>.dropdown-toggle.bs-placeholder,.bootstrap-select>.dropdown-toggle.bs-placeholder:active,.bootstrap-select>.dropdown-toggle.bs-placeholder:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder:hover{color:#999}.bootstrap-select>select{position:absolute!important;bottom:0;left:50%;display:block!important;width:.5px!important;height:100%!important;padding:0!important;opacity:0!important;border:none}.bootstrap-select>select.mobile-device{top:0;left:0;display:block!important;width:100%!important;z-index:2}.error .bootstrap-select .dropdown-toggle,.has-error .bootstrap-select .dropdown-toggle{border-color:#b94a48}.bootstrap-select.fit-width{width:auto!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}.bootstrap-select .dropdown-toggle:focus{outline:thin dotted #333!important;outline:5px auto -webkit-focus-ring-color!important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none}.bootstrap-select.form-control:not([class*=col-]){width:100%}.bootstrap-select.form-control.input-group-btn{z-index:auto}.bootstrap-select.form-control.input-group-btn:not(:first-child):not(:last-child)>.btn{border-radius:0}.bootstrap-select.btn-group:not(.input-group-btn),.bootstrap-select.btn-group[class*=col-]{float:none;display:inline-block;margin-left:0}.bootstrap-select.btn-group.dropdown-menu-right,.bootstrap-select.btn-group[class*=col-].dropdown-menu-right,.row .bootstrap-select.btn-group[class*=col-].dropdown-menu-right{float:right}.form-group .bootstrap-select.btn-group,.form-horizontal .bootstrap-select.btn-group,.form-inline .bootstrap-select.btn-group{margin-bottom:0}.form-group-lg .bootstrap-select.btn-group.form-control,.form-group-sm .bootstrap-select.btn-group.form-control{padding:0}.form-group-lg .bootstrap-select.btn-group.form-control .dropdown-toggle,.form-group-sm .bootstrap-select.btn-group.form-control .dropdown-toggle{height:100%;font-size:inherit;line-height:inherit;border-radius:inherit}.form-inline .bootstrap-select.btn-group .form-control{width:100%}.bootstrap-select.btn-group.disabled,.bootstrap-select.btn-group>.disabled{cursor:not-allowed}.bootstrap-select.btn-group.disabled:focus,.bootstrap-select.btn-group>.disabled:focus{outline:0!important}.bootstrap-select.btn-group.bs-container{position:absolute;height:0!important;padding:0!important}.bootstrap-select.btn-group.bs-container .dropdown-menu{z-index:1060}.bootstrap-select.btn-group .dropdown-toggle .filter-option{display:inline-block;overflow:hidden;width:100%;text-align:left}.bootstrap-select.btn-group .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.bootstrap-select.btn-group[class*=col-] .dropdown-toggle{width:100%}.bootstrap-select.btn-group .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select.btn-group .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.bootstrap-select.btn-group .dropdown-menu li{position:relative}.bootstrap-select.btn-group .dropdown-menu li.active small{color:#fff}.bootstrap-select.btn-group .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select.btn-group .dropdown-menu li a{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.bootstrap-select.btn-group .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select.btn-group .dropdown-menu li a span.check-mark{display:none}.bootstrap-select.btn-group .dropdown-menu li a span.text{display:inline-block}.bootstrap-select.btn-group .dropdown-menu li small{padding-left:.5em}.bootstrap-select.btn-group .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);pointer-events:none;opacity:.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select.btn-group .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.btn-group.fit-width .dropdown-toggle .filter-option{position:static}.bootstrap-select.btn-group.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.btn-group.show-tick .dropdown-menu li.selected a span.check-mark{position:absolute;display:inline-block;right:15px;margin-top:5px}.bootstrap-select.btn-group.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle{z-index:1061}.bootstrap-select.show-menu-arrow .dropdown-toggle:before{content:'';border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(204,204,204,.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle:after{content:'';border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle:before{bottom:auto;top:-3px;border-top:7px solid rgba(204,204,204,.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle:after{bottom:auto;top:-3px;border-top:6px solid #fff;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle:after,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle:before{display:block}.bs-actionsbox,.bs-donebutton,.bs-searchbox{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none}
\ No newline at end of file
diff --git a/RIGS/static/css/email.css b/RIGS/static/css/email.css
deleted file mode 100644
index 915b52f3..00000000
--- a/RIGS/static/css/email.css
+++ /dev/null
@@ -1 +0,0 @@
-body{margin:0px}.main-table{width:100%;border-collapse:collapse}.client-header{background-image:url("https://www.nottinghamtec.co.uk/imgs/wof2014-1.jpg");background-color:#222;background-repeat:no-repeat;background-position:center;width:100%;margin-bottom:28px}.client-header .logos{width:100%;max-width:640px}.client-header img{height:110px}.content-container{width:100%}.content-container .content{font-family:"Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;width:100%;max-width:600px;padding:10px;text-align:left}.content-container .content .button-container{width:100%}.content-container .content .button-container .button{padding:6px 12px;background-color:#357ebf;border-radius:4px}.content-container .content .button-container .button a{color:#fff;text-decoration:none}
diff --git a/RIGS/static/css/fullcalendar.css b/RIGS/static/css/fullcalendar.css
deleted file mode 100755
index 382f709e..00000000
--- a/RIGS/static/css/fullcalendar.css
+++ /dev/null
@@ -1,1061 +0,0 @@
-/*!
- * FullCalendar v2.3.1 Stylesheet
- * Docs & License: http://fullcalendar.io/
- * (c) 2015 Adam Shaw
- */
-
-
-.fc {
- direction: ltr;
- text-align: left;
-}
-
-.fc-rtl {
- text-align: right;
-}
-
-body .fc { /* extra precedence to overcome jqui */
- font-size: 1em;
-}
-
-
-/* Colors
---------------------------------------------------------------------------------------------------*/
-
-.fc-unthemed th,
-.fc-unthemed td,
-.fc-unthemed thead,
-.fc-unthemed tbody,
-.fc-unthemed .fc-divider,
-.fc-unthemed .fc-row,
-.fc-unthemed .fc-popover {
- border-color: #ddd;
-}
-
-.fc-unthemed .fc-popover {
- background-color: #fff;
-}
-
-.fc-unthemed .fc-divider,
-.fc-unthemed .fc-popover .fc-header {
- background: #eee;
-}
-
-.fc-unthemed .fc-popover .fc-header .fc-close {
- color: #666;
-}
-
-.fc-unthemed .fc-today {
- background: #fcf8e3;
-}
-
-.fc-highlight { /* when user is selecting cells */
- background: #bce8f1;
- opacity: .3;
- filter: alpha(opacity=30); /* for IE */
-}
-
-.fc-bgevent { /* default look for background events */
- background: rgb(143, 223, 130);
- opacity: .3;
- filter: alpha(opacity=30); /* for IE */
-}
-
-.fc-nonbusiness { /* default look for non-business-hours areas */
- /* will inherit .fc-bgevent's styles */
- background: #d7d7d7;
-}
-
-
-/* Icons (inline elements with styled text that mock arrow icons)
---------------------------------------------------------------------------------------------------*/
-
-.fc-icon {
- display: inline-block;
- width: 1em;
- height: 1em;
- line-height: 1em;
- font-size: 1em;
- text-align: center;
- overflow: hidden;
- font-family: "Open Sans", sans-serif;
-}
-
-/*
-Acceptable font-family overrides for individual icons:
- "Arial", sans-serif
- "Times New Roman", serif
-
-NOTE: use percentage font sizes or else old IE chokes
-*/
-
-.fc-icon:after {
- position: relative;
- margin: 0 -1em; /* ensures character will be centered, regardless of width */
-}
-
-.fc-icon-left-single-arrow:after {
- content: "\02039";
- font-weight: bold;
- font-size: 200%;
- top: -7%;
- left: 3%;
-}
-
-.fc-icon-right-single-arrow:after {
- content: "\0203A";
- font-weight: bold;
- font-size: 200%;
- top: -7%;
- left: -3%;
-}
-
-.fc-icon-left-double-arrow:after {
- content: "\000AB";
- font-size: 160%;
- top: -7%;
-}
-
-.fc-icon-right-double-arrow:after {
- content: "\000BB";
- font-size: 160%;
- top: -7%;
-}
-
-.fc-icon-left-triangle:after {
- content: "\25C4";
- font-size: 125%;
- top: 3%;
- left: -2%;
-}
-
-.fc-icon-right-triangle:after {
- content: "\25BA";
- font-size: 125%;
- top: 3%;
- left: 2%;
-}
-
-.fc-icon-down-triangle:after {
- content: "\25BC";
- font-size: 125%;
- top: 2%;
-}
-
-.fc-icon-x:after {
- content: "\000D7";
- font-size: 200%;
- top: 6%;
-}
-
-
-/* Buttons (styled