mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-08 16:09:40 +00:00
Compare commits
69 Commits
dependabot
...
aa51ce7861
| Author | SHA1 | Date | |
|---|---|---|---|
| aa51ce7861 | |||
| cdc8635e2f | |||
| 5d07a1853d | |||
| 1eed448828 | |||
| e2e83e2cd8 | |||
| 534e1000d4 | |||
| 84119f6685 | |||
| e50cf8d06c | |||
| a3970cf553 | |||
| 4b8639faf4 | |||
| 6a4620a2e5 | |||
| cc233e0223 | |||
| 2351201e13 | |||
| 874b8ef37a | |||
| 6a0b00dc76 | |||
| 1c31608951 | |||
| 37c0890a4b | |||
| c772744a4b | |||
| 5319c2f6e3 | |||
| aa525372a2 | |||
| 2774690b59 | |||
| 81733d32ba | |||
| e6527db6f7 | |||
| d109ee231f | |||
| cde46cc8e4 | |||
| dddf0dc42a | |||
| 19c5f282d2 | |||
| d5d2e9167c | |||
| 3b3ba0b87e | |||
| 1fbe59dfb0 | |||
| 65c5307072 | |||
| 638816535e | |||
| 8346d705ba | |||
| 18eed1a654 | |||
| 5174a442bc | |||
| 5f1fd59dd2 | |||
| 3be2a9f4b5 | |||
| 883ef4ed8b | |||
| 2195a60438 | |||
| 12c4b63947 | |||
| 39ab9df836 | |||
| a99e4b1d1c | |||
| 18a091f8ea | |||
| d64d0f54d4 | |||
| 8f54897b69 | |||
| 97830596b5 | |||
| 68f401097d | |||
| 8c5b2f426d | |||
| e3fc05772e | |||
| e38205b9e7 | |||
| f2b9642772 | |||
| 08939a0e1f | |||
| 15b7f9c7c1 | |||
| c67f7e1e54 | |||
| 4f268b3168 | |||
| 0e7723828a | |||
| c46a2c53f3 | |||
| b3617a74bf | |||
| 5806cbc72d | |||
| ab2ff9f146 | |||
| fd5575a818 | |||
| 402a1dd7f0 | |||
| be7688aa75 | |||
| a472e414f7 | |||
| 618c02aa9c | |||
| 3056b6ef71 | |||
| 548c960996 | |||
| 2cc43dcdb7 | |||
| 6db25fb56b |
@@ -1,32 +1,16 @@
|
||||
---
|
||||
engines:
|
||||
version: 2
|
||||
plugins:
|
||||
csslint:
|
||||
enabled: true
|
||||
duplication:
|
||||
enabled: true
|
||||
config:
|
||||
languages:
|
||||
- ruby
|
||||
- javascript
|
||||
- python
|
||||
- php
|
||||
eslint:
|
||||
enabled: true
|
||||
fixme:
|
||||
enabled: true
|
||||
radon:
|
||||
enabled: true
|
||||
rubocop:
|
||||
enabled: true
|
||||
ratings:
|
||||
paths:
|
||||
- "**.css"
|
||||
- "**.inc"
|
||||
- "**.js"
|
||||
- "**.jsx"
|
||||
- "**.module"
|
||||
- "**.php"
|
||||
- "**.py"
|
||||
- "**.rb"
|
||||
exclude_paths:
|
||||
- config/
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
[run]
|
||||
source =
|
||||
./
|
||||
plugins =
|
||||
django_coverage_plugin
|
||||
|
||||
omit =
|
||||
*/migrations/*
|
||||
plugins = django_coverage_plugin
|
||||
omit = *migrations*, *tests*
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
--exclude-exts=.min.css
|
||||
--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes
|
||||
@@ -1 +0,0 @@
|
||||
**/*{.,-}min.js
|
||||
213
.eslintrc
213
.eslintrc
@@ -1,213 +0,0 @@
|
||||
ecmaFeatures:
|
||||
modules: true
|
||||
jsx: true
|
||||
|
||||
env:
|
||||
amd: true
|
||||
browser: true
|
||||
es6: true
|
||||
jquery: true
|
||||
node: true
|
||||
|
||||
# http://eslint.org/docs/rules/
|
||||
rules:
|
||||
# Possible Errors
|
||||
comma-dangle: [2, never]
|
||||
no-cond-assign: 2
|
||||
no-console: 0
|
||||
no-constant-condition: 2
|
||||
no-control-regex: 2
|
||||
no-debugger: 2
|
||||
no-dupe-args: 2
|
||||
no-dupe-keys: 2
|
||||
no-duplicate-case: 2
|
||||
no-empty: 2
|
||||
no-empty-character-class: 2
|
||||
no-ex-assign: 2
|
||||
no-extra-boolean-cast: 2
|
||||
no-extra-parens: 0
|
||||
no-extra-semi: 2
|
||||
no-func-assign: 2
|
||||
no-inner-declarations: [2, functions]
|
||||
no-invalid-regexp: 2
|
||||
no-irregular-whitespace: 2
|
||||
no-negated-in-lhs: 2
|
||||
no-obj-calls: 2
|
||||
no-regex-spaces: 2
|
||||
no-sparse-arrays: 2
|
||||
no-unexpected-multiline: 2
|
||||
no-unreachable: 2
|
||||
use-isnan: 2
|
||||
valid-jsdoc: 0
|
||||
valid-typeof: 2
|
||||
|
||||
# Best Practices
|
||||
accessor-pairs: 2
|
||||
block-scoped-var: 0
|
||||
complexity: [2, 6]
|
||||
consistent-return: 0
|
||||
curly: 0
|
||||
default-case: 0
|
||||
dot-location: 0
|
||||
dot-notation: 0
|
||||
eqeqeq: 2
|
||||
guard-for-in: 2
|
||||
no-alert: 2
|
||||
no-caller: 2
|
||||
no-case-declarations: 2
|
||||
no-div-regex: 2
|
||||
no-else-return: 0
|
||||
no-empty-label: 2
|
||||
no-empty-pattern: 2
|
||||
no-eq-null: 2
|
||||
no-eval: 2
|
||||
no-extend-native: 2
|
||||
no-extra-bind: 2
|
||||
no-fallthrough: 2
|
||||
no-floating-decimal: 0
|
||||
no-implicit-coercion: 0
|
||||
no-implied-eval: 2
|
||||
no-invalid-this: 0
|
||||
no-iterator: 2
|
||||
no-labels: 0
|
||||
no-lone-blocks: 2
|
||||
no-loop-func: 2
|
||||
no-magic-number: 0
|
||||
no-multi-spaces: 0
|
||||
no-multi-str: 0
|
||||
no-native-reassign: 2
|
||||
no-new-func: 2
|
||||
no-new-wrappers: 2
|
||||
no-new: 2
|
||||
no-octal-escape: 2
|
||||
no-octal: 2
|
||||
no-proto: 2
|
||||
no-redeclare: 2
|
||||
no-return-assign: 2
|
||||
no-script-url: 2
|
||||
no-self-compare: 2
|
||||
no-sequences: 0
|
||||
no-throw-literal: 0
|
||||
no-unused-expressions: 2
|
||||
no-useless-call: 2
|
||||
no-useless-concat: 2
|
||||
no-void: 2
|
||||
no-warning-comments: 0
|
||||
no-with: 2
|
||||
radix: 2
|
||||
vars-on-top: 0
|
||||
wrap-iife: 2
|
||||
yoda: 0
|
||||
|
||||
# Strict
|
||||
strict: 0
|
||||
|
||||
# Variables
|
||||
init-declarations: 0
|
||||
no-catch-shadow: 2
|
||||
no-delete-var: 2
|
||||
no-label-var: 2
|
||||
no-shadow-restricted-names: 2
|
||||
no-shadow: 0
|
||||
no-undef-init: 2
|
||||
no-undef: 0
|
||||
no-undefined: 0
|
||||
no-unused-vars: 0
|
||||
no-use-before-define: 0
|
||||
|
||||
# Node.js and CommonJS
|
||||
callback-return: 2
|
||||
global-require: 2
|
||||
handle-callback-err: 2
|
||||
no-mixed-requires: 0
|
||||
no-new-require: 0
|
||||
no-path-concat: 2
|
||||
no-process-exit: 2
|
||||
no-restricted-modules: 0
|
||||
no-sync: 0
|
||||
|
||||
# Stylistic Issues
|
||||
array-bracket-spacing: 0
|
||||
block-spacing: 0
|
||||
brace-style: 0
|
||||
camelcase: 0
|
||||
comma-spacing: 0
|
||||
comma-style: 0
|
||||
computed-property-spacing: 0
|
||||
consistent-this: 0
|
||||
eol-last: 0
|
||||
func-names: 0
|
||||
func-style: 0
|
||||
id-length: 0
|
||||
id-match: 0
|
||||
indent: 0
|
||||
jsx-quotes: 0
|
||||
key-spacing: 0
|
||||
linebreak-style: 0
|
||||
lines-around-comment: 0
|
||||
max-depth: 0
|
||||
max-len: 0
|
||||
max-nested-callbacks: 0
|
||||
max-params: 0
|
||||
max-statements: [2, 30]
|
||||
new-cap: 0
|
||||
new-parens: 0
|
||||
newline-after-var: 0
|
||||
no-array-constructor: 0
|
||||
no-bitwise: 0
|
||||
no-continue: 0
|
||||
no-inline-comments: 0
|
||||
no-lonely-if: 0
|
||||
no-mixed-spaces-and-tabs: 0
|
||||
no-multiple-empty-lines: 0
|
||||
no-negated-condition: 0
|
||||
no-nested-ternary: 0
|
||||
no-new-object: 0
|
||||
no-plusplus: 0
|
||||
no-restricted-syntax: 0
|
||||
no-spaced-func: 0
|
||||
no-ternary: 0
|
||||
no-trailing-spaces: 0
|
||||
no-underscore-dangle: 0
|
||||
no-unneeded-ternary: 0
|
||||
object-curly-spacing: 0
|
||||
one-var: 0
|
||||
operator-assignment: 0
|
||||
operator-linebreak: 0
|
||||
padded-blocks: 0
|
||||
quote-props: 0
|
||||
quotes: 0
|
||||
require-jsdoc: 0
|
||||
semi-spacing: 0
|
||||
semi: 0
|
||||
sort-vars: 0
|
||||
space-after-keywords: 0
|
||||
space-before-blocks: 0
|
||||
space-before-function-paren: 0
|
||||
space-before-keywords: 0
|
||||
space-in-parens: 0
|
||||
space-infix-ops: 0
|
||||
space-return-throw-case: 0
|
||||
space-unary-ops: 0
|
||||
spaced-comment: 0
|
||||
wrap-regex: 0
|
||||
|
||||
# ECMAScript 6
|
||||
arrow-body-style: 0
|
||||
arrow-parens: 0
|
||||
arrow-spacing: 0
|
||||
constructor-super: 0
|
||||
generator-star-spacing: 0
|
||||
no-arrow-condition: 0
|
||||
no-class-assign: 0
|
||||
no-const-assign: 0
|
||||
no-dupe-class-members: 0
|
||||
no-this-before-super: 0
|
||||
no-var: 0
|
||||
object-shorthand: 0
|
||||
prefer-arrow-callback: 0
|
||||
prefer-const: 0
|
||||
prefer-reflect: 0
|
||||
prefer-spread: 0
|
||||
prefer-template: 0
|
||||
require-yield: 0
|
||||
69
.github/workflows/django.yml
vendored
69
.github/workflows/django.yml
vendored
@@ -8,41 +8,56 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
env:
|
||||
IMGUR_UPLOAD_CLIENT_ID: ${{ secrets.IMGUR_UPLOAD_CLIENT_ID }}
|
||||
IMGUR_UPLOAD_CLIENT_SECRET: ${{ secrets.IMGUR_UPLOAD_CLIENT_SECRET }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
python-version: [3.8]
|
||||
matrix:
|
||||
browser: ['chrome', 'firefox']
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BROWSER: ${{ matrix.browser }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: node node_modules/gulp/bin/gulp build
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Setup Chromedriver
|
||||
run: |
|
||||
wget https://chromedriver.storage.googleapis.com/2.36/chromedriver_linux64.zip
|
||||
unzip chromedriver_linux64.zip
|
||||
export PATH=$PATH:$(pwd)
|
||||
chmod +x chromedriver
|
||||
export PATH=$PATH:/usr/lib/chromium-browser/
|
||||
python-version: 3.9
|
||||
- name: Cache python deps
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}
|
||||
key: ${{ env.pythonLocation }}-${{ hashFiles('requirements.txt') }}
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pycodestyle coverage coveralls django_coverage_plugin
|
||||
pip install -r requirements.txt
|
||||
python manage.py collectstatic --noinput
|
||||
python -m pip install --upgrade pip
|
||||
pip install pycodestyle coverage coveralls django_coverage_plugin pytest-cov
|
||||
pip install --upgrade --upgrade-strategy eager -r requirements.txt
|
||||
python manage.py collectstatic --noinput
|
||||
- name: Cache gulp output
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: static/
|
||||
key: static-${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
||||
- name: Basic Checks
|
||||
run: |
|
||||
pycodestyle . --exclude=migrations,importer*
|
||||
python manage.py check
|
||||
python manage.py makemigrations --check --dry-run
|
||||
pycodestyle . --exclude=migrations,node_modules
|
||||
python manage.py check
|
||||
python manage.py makemigrations --check --dry-run
|
||||
- name: Run Tests
|
||||
run: |
|
||||
coverage run manage.py test --verbosity=2
|
||||
coveralls --service=github
|
||||
uses: paambaati/codeclimate-action@v2.7.5
|
||||
env:
|
||||
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
|
||||
with:
|
||||
coverageCommand: coverage run -m pytest -n 8
|
||||
coverageLocations: |
|
||||
${{github.workspace}}/.coverage:coverage.py
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: failure-screenshots ${{ matrix.test-group }}
|
||||
path: screenshots/
|
||||
retention-days: 5
|
||||
- name: Coveralls
|
||||
run: coveralls --service=github
|
||||
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -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
|
||||
|
||||
1
.idea/.name
generated
1
.idea/.name
generated
@@ -1 +0,0 @@
|
||||
PyRIGS
|
||||
5
.idea/encodings.xml
generated
5
.idea/encodings.xml
generated
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
|
||||
</project>
|
||||
|
||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/pyrigs.iml" filepath="$PROJECT_DIR$/.idea/pyrigs.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
5
.idea/scopes/scope_settings.xml
generated
5
.idea/scopes/scope_settings.xml
generated
@@ -1,5 +0,0 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<state>
|
||||
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
|
||||
</state>
|
||||
</component>
|
||||
7
.idea/vcs.xml
generated
7
.idea/vcs.xml
generated
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
||||
1156
.rubocop.yml
1156
.rubocop.yml
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,3 @@
|
||||
*.sqlite3
|
||||
*.scss
|
||||
*.md
|
||||
*.rb
|
||||
Vagrantfile
|
||||
config/vagrant/*
|
||||
config/vagrant.yml
|
||||
**/tests
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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,11 +8,12 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.7/ref/settings/
|
||||
"""
|
||||
|
||||
import datetime
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
import raven
|
||||
import secrets
|
||||
import datetime
|
||||
|
||||
import raven
|
||||
from envparse import env
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
@@ -235,6 +236,9 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
|
||||
STATIC_DIRS = (
|
||||
os.path.join(BASE_DIR, 'static/')
|
||||
)
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, 'pipeline/built_assets/'),
|
||||
]
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
@@ -263,6 +267,3 @@ USE_GRAVATAR = True
|
||||
|
||||
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
||||
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
|
||||
|
||||
IMGUR_UPLOAD_CLIENT_ID = env('IMGUR_UPLOAD_CLIENT_ID', default="")
|
||||
IMGUR_UPLOAD_CLIENT_SECRET = env('IMGUR_UPLOAD_CLIENT_SECRET', default="")
|
||||
|
||||
0
PyRIGS/tests/__init__.py
Normal file
0
PyRIGS/tests/__init__.py
Normal file
@@ -1,16 +1,17 @@
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.test import LiveServerTestCase
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
|
||||
from RIGS import models as rigsmodels
|
||||
from . import pages
|
||||
import os
|
||||
import pytz
|
||||
from datetime import date, time, datetime, timedelta
|
||||
from django.conf import settings
|
||||
import imgurpython
|
||||
import PyRIGS.settings
|
||||
import sys
|
||||
import pathlib
|
||||
import inspect
|
||||
from envparse import env
|
||||
|
||||
|
||||
def create_datetime(year, month, day, hour, min):
|
||||
@@ -18,23 +19,26 @@ def create_datetime(year, month, day, hour, min):
|
||||
return tz.localize(datetime(year, month, day, hour, min)).astimezone(pytz.utc)
|
||||
|
||||
|
||||
def create_browser():
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument("--window-size=1920,1080")
|
||||
# No caching, please and thank you
|
||||
options.add_argument("--aggressive-cache-discard")
|
||||
options.add_argument("--disk-cache-size=0")
|
||||
options.add_argument("--headless")
|
||||
if settings.CI:
|
||||
options.add_argument("--no-sandbox")
|
||||
driver = webdriver.Chrome(options=options)
|
||||
def create_browser(browser):
|
||||
if browser == "firefox":
|
||||
options = webdriver.FirefoxOptions()
|
||||
options.headless = True
|
||||
driver = webdriver.Firefox(options=options)
|
||||
else:
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument("--window-size=1920,1080")
|
||||
options.add_argument("--headless")
|
||||
if settings.CI:
|
||||
options.add_argument("--no-sandbox")
|
||||
driver = webdriver.Chrome(options=options)
|
||||
return driver
|
||||
|
||||
|
||||
class BaseTest(LiveServerTestCase):
|
||||
def setUp(self):
|
||||
super().setUpClass()
|
||||
self.driver = create_browser()
|
||||
self.driver = create_browser(env('BROWSER', default="chrome"))
|
||||
self.wait = WebDriverWait(self.driver, 15)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
@@ -48,8 +52,8 @@ 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")
|
||||
|
||||
|
||||
def screenshot_failure(func):
|
||||
@@ -62,20 +66,9 @@ def screenshot_failure(func):
|
||||
if not pathlib.Path("screenshots").is_dir():
|
||||
os.mkdir("screenshots")
|
||||
self.driver.save_screenshot(screenshot_file)
|
||||
|
||||
if settings.IMGUR_UPLOAD_CLIENT_ID != "":
|
||||
config = {
|
||||
'album': None,
|
||||
'name': screenshot_name,
|
||||
'title': screenshot_name,
|
||||
'description': ""
|
||||
}
|
||||
client = imgurpython.ImgurClient(settings.IMGUR_UPLOAD_CLIENT_ID, settings.IMGUR_UPLOAD_CLIENT_SECRET)
|
||||
image = client.upload_from_path(screenshot_file, config=config)
|
||||
print("Error in test {} is at url {}".format(screenshot_name, image['link']), file=sys.stderr)
|
||||
else:
|
||||
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
|
||||
print("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
|
||||
raise e
|
||||
|
||||
return wrapper_func
|
||||
|
||||
|
||||
@@ -86,12 +79,5 @@ def screenshot_failure_cls(cls):
|
||||
return cls
|
||||
|
||||
|
||||
# Checks if animation is done
|
||||
class animation_is_finished():
|
||||
def __call__(self, driver):
|
||||
numberAnimating = driver.execute_script('return $(":animated").length')
|
||||
finished = numberAnimating == 0
|
||||
if finished:
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
return finished
|
||||
def assert_times_equal(first_time, second_time):
|
||||
assert first_time.replace(microsecond=0) == second_time.replace(microsecond=0)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from pypom import Page, Region
|
||||
from pypom import Page
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver import Chrome
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
|
||||
from PyRIGS.tests import regions
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ class FormPage(BasePage):
|
||||
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
|
||||
@@ -73,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
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
from pypom import Region
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions
|
||||
from selenium.webdriver.remote.webelement import WebElement
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support.select import Select
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
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:
|
||||
@@ -77,8 +75,7 @@ class BootstrapSelectElement(Region):
|
||||
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))
|
||||
self.wait.until(expected_conditions.invisibility_of_element_located(*self._status_locator))
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
from django.urls import path
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.conf import settings
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.contrib.auth.views import LoginView
|
||||
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, re_path
|
||||
from django.views.generic import TemplateView
|
||||
from PyRIGS.decorators import permission_required_with_403
|
||||
import RIGS
|
||||
import users
|
||||
import versioning
|
||||
|
||||
from PyRIGS import views
|
||||
|
||||
urlpatterns = [
|
||||
@@ -39,6 +34,6 @@ if settings.DEBUG:
|
||||
|
||||
import debug_toolbar
|
||||
urlpatterns = [
|
||||
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||
re_path(r'^__debug__/', include(debug_toolbar.urls)),
|
||||
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
|
||||
] + urlpatterns
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
||||
from django.views import generic
|
||||
from django.contrib.auth.views import LoginView
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.core import serializers
|
||||
from django.conf import settings
|
||||
import simplejson
|
||||
from django.contrib import messages
|
||||
import datetime
|
||||
import pytz
|
||||
import operator
|
||||
from registration.views import RegistrationView
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from RIGS import models, forms
|
||||
from assets import models as asset_models
|
||||
from functools import reduce
|
||||
|
||||
from django.views.decorators.cache import never_cache, cache_page
|
||||
from django.utils.decorators import method_decorator
|
||||
import simplejson
|
||||
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 RIGS import models
|
||||
from assets import models as asset_models
|
||||
|
||||
|
||||
def is_ajax(request):
|
||||
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
|
||||
|
||||
# Displays the current rig count along with a few other bits and pieces
|
||||
|
||||
|
||||
class Index(generic.TemplateView):
|
||||
template_name = 'index.html'
|
||||
|
||||
@@ -151,7 +148,7 @@ class SecureAPIRequest(generic.View):
|
||||
|
||||
class ModalURLMixin:
|
||||
def get_close_url(self, update, detail):
|
||||
if self.request.is_ajax():
|
||||
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]))
|
||||
@@ -170,7 +167,7 @@ class GenericListView(generic.ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(GenericListView, self).get_context_data(**kwargs)
|
||||
context['page_title'] = self.model.__name__ + "s"
|
||||
if self.request.is_ajax():
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
return context
|
||||
|
||||
@@ -202,7 +199,7 @@ class GenericDetailView(generic.DetailView):
|
||||
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 self.request.is_ajax():
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
return context
|
||||
|
||||
@@ -213,7 +210,7 @@ class GenericUpdateView(generic.UpdateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(GenericUpdateView, self).get_context_data(**kwargs)
|
||||
context['page_title'] = "Edit {}".format(self.model.__name__)
|
||||
if self.request.is_ajax():
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
return context
|
||||
|
||||
@@ -224,7 +221,7 @@ class GenericCreateView(generic.CreateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(GenericCreateView, self).get_context_data(**kwargs)
|
||||
context['page_title'] = "Create {}".format(self.model.__name__)
|
||||
if self.request.is_ajax():
|
||||
if is_ajax(self.request):
|
||||
context['override'] = "base_ajax.html"
|
||||
return context
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# TEC PA & Lighting - PyRIGS #
|
||||

|
||||
[](https://coveralls.io/github/nottinghamtec/PyRIGS)
|
||||
[](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability)
|
||||
|
||||
Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets.
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
from django.contrib import admin
|
||||
from RIGS import models, forms
|
||||
from users import forms as user_forms
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from django.contrib.admin import helpers
|
||||
from django.template.response import TemplateResponse
|
||||
from django.contrib import messages
|
||||
from django.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)
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
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_lazy
|
||||
from django.views import generic
|
||||
from django.db.models import Q
|
||||
from z3c.rml import rml2pdf
|
||||
from django.db.models import Q
|
||||
|
||||
from django.db import transaction
|
||||
import reversion
|
||||
|
||||
from RIGS import models
|
||||
|
||||
from django import forms
|
||||
|
||||
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
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.core.mail import EmailMessage, EmailMultiAlternatives
|
||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
||||
from django.db import transaction
|
||||
from registration.forms import RegistrationFormUniqueEmail
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from captcha.fields import ReCaptchaField
|
||||
from reversion import revisions as reversion
|
||||
import simplejson
|
||||
from datetime import datetime
|
||||
from django.utils import timezone
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
10
RIGS/hs.py
10
RIGS/hs.py
@@ -1,11 +1,11 @@
|
||||
from RIGS import models, forms
|
||||
from django.views import generic
|
||||
from django.utils import timezone
|
||||
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 django.db.models import AutoField, ManyToOneRel
|
||||
from django.contrib import messages
|
||||
|
||||
from RIGS import models, forms
|
||||
|
||||
|
||||
class EventRiskAssessmentCreate(generic.CreateView):
|
||||
|
||||
13
RIGS/ical.py
13
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):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.db import transaction
|
||||
from reversion import revisions as reversion
|
||||
|
||||
import datetime
|
||||
import random
|
||||
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import datetime
|
||||
import hashlib
|
||||
import pytz
|
||||
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.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from reversion import revisions as reversion
|
||||
from reversion.models import Version
|
||||
import string
|
||||
|
||||
import random
|
||||
from collections import Counter
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
class Profile(AbstractUser):
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
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.shortcuts import get_object_or_404
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.template import RequestContext
|
||||
from django.template.loader import get_template
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.urls import reverse_lazy
|
||||
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 django.utils import timezone
|
||||
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.storage import staticfiles_storage
|
||||
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 RIGS import models, forms
|
||||
|
||||
__author__ = 'ghost'
|
||||
|
||||
@@ -294,7 +291,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
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import datetime
|
||||
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.core.mail import EmailMessage, EmailMultiAlternatives
|
||||
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 registration.signals import user_activated
|
||||
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
|
||||
from reversion import revisions as reversion
|
||||
|
||||
|
||||
def send_eventauthorisation_success_email(instance):
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/*!
|
||||
* Ajax Bootstrap Select
|
||||
*
|
||||
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
|
||||
*
|
||||
* @version 1.4.5
|
||||
* @author Adam Heim - https://github.com/truckingsim
|
||||
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
|
||||
* @copyright 2019 Adam Heim
|
||||
* @license Released under the MIT license.
|
||||
*
|
||||
* Contributors:
|
||||
* Mark Carver - https://github.com/markcarver
|
||||
*
|
||||
* Last build: 2019-04-23 12:18:56 PM EDT
|
||||
*/
|
||||
.bootstrap-select .status {
|
||||
background: #f0f0f0;
|
||||
clear: both;
|
||||
color: #999;
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
margin-bottom: -5px;
|
||||
padding: 10px 20px; }
|
||||
|
||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7OztFQWVFO0FBQ0Y7RUFDRSxtQkFBbUI7RUFDbkIsV0FBVztFQUNYLFdBQVc7RUFDWCxlQUFlO0VBQ2Ysa0JBQWtCO0VBQ2xCLGdCQUFnQjtFQUNoQixjQUFjO0VBQ2QsbUJBQW1CO0VBQ25CLGtCQUFrQixFQUFBIiwiZmlsZSI6ImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyIvKiFcbiAqIEFqYXggQm9vdHN0cmFwIFNlbGVjdFxuICpcbiAqIEV4dGVuZHMgZXhpc3RpbmcgW0Jvb3RzdHJhcCBTZWxlY3RdIGltcGxlbWVudGF0aW9ucyBieSBhZGRpbmcgdGhlIGFiaWxpdHkgdG8gc2VhcmNoIHZpYSBBSkFYIHJlcXVlc3RzIGFzIHlvdSB0eXBlLiBPcmlnaW5hbGx5IGZvciBDUk9TQ09OLlxuICpcbiAqIEB2ZXJzaW9uIDEuNC41XG4gKiBAYXV0aG9yIEFkYW0gSGVpbSAtIGh0dHBzOi8vZ2l0aHViLmNvbS90cnVja2luZ3NpbVxuICogQGxpbmsgaHR0cHM6Ly9naXRodWIuY29tL3RydWNraW5nc2ltL0FqYXgtQm9vdHN0cmFwLVNlbGVjdFxuICogQGNvcHlyaWdodCAyMDE5IEFkYW0gSGVpbVxuICogQGxpY2Vuc2UgUmVsZWFzZWQgdW5kZXIgdGhlIE1JVCBsaWNlbnNlLlxuICpcbiAqIENvbnRyaWJ1dG9yczpcbiAqICAgTWFyayBDYXJ2ZXIgLSBodHRwczovL2dpdGh1Yi5jb20vbWFya2NhcnZlclxuICpcbiAqIExhc3QgYnVpbGQ6IDIwMTktMDQtMjMgMTI6MTg6NTYgUE0gRURUXG4gKi9cbi5ib290c3RyYXAtc2VsZWN0IC5zdGF0dXMge1xuICBiYWNrZ3JvdW5kOiAjZjBmMGYwO1xuICBjbGVhcjogYm90aDtcbiAgY29sb3I6ICM5OTk7XG4gIGZvbnQtc2l6ZTogMTFweDtcbiAgZm9udC1zdHlsZTogaXRhbGljO1xuICBmb250LXdlaWdodDogNTAwO1xuICBsaW5lLWhlaWdodDogMTtcbiAgbWFyZ2luLWJvdHRvbTogLTVweDtcbiAgcGFkZGluZzogMTBweCAyMHB4O1xufVxuIl19 */
|
||||
28
RIGS/static/css/ajax-bootstrap-select.min.css
vendored
28
RIGS/static/css/ajax-bootstrap-select.min.css
vendored
@@ -1,28 +0,0 @@
|
||||
/*!
|
||||
* Ajax Bootstrap Select
|
||||
*
|
||||
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
|
||||
*
|
||||
* @version 1.4.5
|
||||
* @author Adam Heim - https://github.com/truckingsim
|
||||
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
|
||||
* @copyright 2019 Adam Heim
|
||||
* @license Released under the MIT license.
|
||||
*
|
||||
* Contributors:
|
||||
* Mark Carver - https://github.com/markcarver
|
||||
*
|
||||
* Last build: 2019-04-23 12:18:56 PM EDT
|
||||
*/
|
||||
.bootstrap-select .status {
|
||||
background: #f0f0f0;
|
||||
clear: both;
|
||||
color: #999;
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
margin-bottom: -5px;
|
||||
padding: 10px 20px; }
|
||||
|
||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5taW4uY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7RUFlRTtBQUFDO0VBQTBCLG1CQUFrQjtFQUFDLFdBQVU7RUFBQyxXQUFVO0VBQUMsZUFBYztFQUFDLGtCQUFpQjtFQUFDLGdCQUFlO0VBQUMsY0FBYTtFQUFDLG1CQUFrQjtFQUFDLGtCQUFpQixFQUFBIiwiZmlsZSI6ImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5taW4uY3NzIiwic291cmNlc0NvbnRlbnQiOlsiLyohXG4gKiBBamF4IEJvb3RzdHJhcCBTZWxlY3RcbiAqXG4gKiBFeHRlbmRzIGV4aXN0aW5nIFtCb290c3RyYXAgU2VsZWN0XSBpbXBsZW1lbnRhdGlvbnMgYnkgYWRkaW5nIHRoZSBhYmlsaXR5IHRvIHNlYXJjaCB2aWEgQUpBWCByZXF1ZXN0cyBhcyB5b3UgdHlwZS4gT3JpZ2luYWxseSBmb3IgQ1JPU0NPTi5cbiAqXG4gKiBAdmVyc2lvbiAxLjQuNVxuICogQGF1dGhvciBBZGFtIEhlaW0gLSBodHRwczovL2dpdGh1Yi5jb20vdHJ1Y2tpbmdzaW1cbiAqIEBsaW5rIGh0dHBzOi8vZ2l0aHViLmNvbS90cnVja2luZ3NpbS9BamF4LUJvb3RzdHJhcC1TZWxlY3RcbiAqIEBjb3B5cmlnaHQgMjAxOSBBZGFtIEhlaW1cbiAqIEBsaWNlbnNlIFJlbGVhc2VkIHVuZGVyIHRoZSBNSVQgbGljZW5zZS5cbiAqXG4gKiBDb250cmlidXRvcnM6XG4gKiAgIE1hcmsgQ2FydmVyIC0gaHR0cHM6Ly9naXRodWIuY29tL21hcmtjYXJ2ZXJcbiAqXG4gKiBMYXN0IGJ1aWxkOiAyMDE5LTA0LTIzIDEyOjE4OjU2IFBNIEVEVFxuICovLmJvb3RzdHJhcC1zZWxlY3QgLnN0YXR1c3tiYWNrZ3JvdW5kOiNmMGYwZjA7Y2xlYXI6Ym90aDtjb2xvcjojOTk5O2ZvbnQtc2l6ZToxMXB4O2ZvbnQtc3R5bGU6aXRhbGljO2ZvbnQtd2VpZ2h0OjUwMDtsaW5lLWhlaWdodDoxO21hcmdpbi1ib3R0b206LTVweDtwYWRkaW5nOjEwcHggMjBweH0iXX0= */
|
||||
@@ -1,23 +0,0 @@
|
||||
.autocomplete {
|
||||
background: white;
|
||||
z-index: 1000;
|
||||
font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(50, 50, 50, 0.6); }
|
||||
|
||||
.autocomplete * {
|
||||
font: inherit; }
|
||||
|
||||
.autocomplete > div {
|
||||
padding: 0 4px; }
|
||||
|
||||
.autocomplete .group {
|
||||
background: #eee; }
|
||||
|
||||
.autocomplete > div:hover:not(.group),
|
||||
.autocomplete > div.selected {
|
||||
background: #81ca91;
|
||||
cursor: pointer; }
|
||||
|
||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImF1dG9jb21wbGV0ZS5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0E7RUFDSSxpQkFBaUI7RUFDakIsYUFBYTtFQUNiLDRHQUE0RztFQUM1RyxjQUFjO0VBQ2Qsc0JBQXNCO0VBQ3RCLHVDQUF1QyxFQUFBOztBQUczQztFQUNJLGFBQWEsRUFBQTs7QUFHakI7RUFDSSxjQUFjLEVBQUE7O0FBR2xCO0VBQ0ksZ0JBQWdCLEVBQUE7O0FBR3BCOztFQUVJLG1CQUFtQjtFQUNuQixlQUFlLEVBQUEiLCJmaWxlIjoiYXV0b2NvbXBsZXRlLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIlxyXG4uYXV0b2NvbXBsZXRlIHtcclxuICAgIGJhY2tncm91bmQ6IHdoaXRlO1xyXG4gICAgei1pbmRleDogMTAwMDtcclxuICAgIGZvbnQ6IDE0cHgvMjJweCBcIi1hcHBsZS1zeXN0ZW1cIiwgQmxpbmtNYWNTeXN0ZW1Gb250LCBcIlNlZ29lIFVJXCIsIFJvYm90bywgXCJIZWx2ZXRpY2EgTmV1ZVwiLCBBcmlhbCwgc2Fucy1zZXJpZjtcclxuICAgIG92ZXJmbG93OiBhdXRvO1xyXG4gICAgYm94LXNpemluZzogYm9yZGVyLWJveDtcclxuICAgIGJvcmRlcjogMXB4IHNvbGlkIHJnYmEoNTAsIDUwLCA1MCwgMC42KTtcclxufVxyXG5cclxuLmF1dG9jb21wbGV0ZSAqIHtcclxuICAgIGZvbnQ6IGluaGVyaXQ7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgPiBkaXYge1xyXG4gICAgcGFkZGluZzogMCA0cHg7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgLmdyb3VwIHtcclxuICAgIGJhY2tncm91bmQ6ICNlZWU7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgPiBkaXY6aG92ZXI6bm90KC5ncm91cCksXHJcbi5hdXRvY29tcGxldGUgPiBkaXYuc2VsZWN0ZWQge1xyXG4gICAgYmFja2dyb3VuZDogIzgxY2E5MTtcclxuICAgIGN1cnNvcjogcG9pbnRlcjtcclxufVxyXG5cclxuIl19 */
|
||||
248
RIGS/static/css/bootstrap-datetimepicker.min.css
vendored
248
RIGS/static/css/bootstrap-datetimepicker.min.css
vendored
File diff suppressed because one or more lines are too long
455
RIGS/static/css/bootstrap-select.css
vendored
455
RIGS/static/css/bootstrap-select.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,39 +0,0 @@
|
||||
body {
|
||||
margin: 0px; }
|
||||
|
||||
.main-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse; }
|
||||
|
||||
.client-header {
|
||||
background-image: url("https://www.nottinghamtec.co.uk/imgs/wof2014-1.jpg");
|
||||
background-color: #222;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
width: 100%;
|
||||
margin-bottom: 28px; }
|
||||
.client-header .logos {
|
||||
width: 100%;
|
||||
max-width: 640px; }
|
||||
.client-header img {
|
||||
height: 110px; }
|
||||
|
||||
.content-container {
|
||||
width: 100%; }
|
||||
.content-container .content {
|
||||
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
padding: 10px;
|
||||
text-align: left; }
|
||||
.content-container .content .button-container {
|
||||
width: 100%; }
|
||||
.content-container .content .button-container .button {
|
||||
padding: 6px 12px;
|
||||
background-color: #357ebf;
|
||||
border-radius: 4px; }
|
||||
.content-container .content .button-container .button a {
|
||||
color: #fff;
|
||||
text-decoration: none; }
|
||||
|
||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImVtYWlsLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUE7RUFDRSxXQUFXLEVBQUE7O0FBR2I7RUFDRSxXQUFXO0VBQ1gseUJBQXlCLEVBQUE7O0FBSTNCO0VBQ0UsMkVBQTJFO0VBQzNFLHNCQUFzQjtFQUN0Qiw0QkFBNEI7RUFDNUIsMkJBQTJCO0VBRTNCLFdBQVc7RUFFWCxtQkFBbUIsRUFBQTtFQVJyQjtJQVdJLFdBQVc7SUFDWCxnQkFBZ0IsRUFBQTtFQVpwQjtJQWdCSSxhQUFhLEVBQUE7O0FBSWpCO0VBQ0UsV0FBVyxFQUFBO0VBRGI7SUFJSSx3RUFBd0U7SUFFeEUsV0FBVztJQUNYLGdCQUFnQjtJQUNoQixhQUFhO0lBQ2IsZ0JBQWdCLEVBQUE7SUFUcEI7TUFZTSxXQUFXLEVBQUE7TUFaakI7UUFlUSxpQkFBaUI7UUFDakIseUJBaERjO1FBaURkLGtCQUFrQixFQUFBO1FBakIxQjtVQW9CVSxXQUFXO1VBQ1gscUJBQXFCLEVBQUEiLCJmaWxlIjoiZW1haWwuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiJGJ1dHRvbl9jb2xvcjogIzM1N2ViZjtcblxuYm9keXtcbiAgbWFyZ2luOiAwcHg7XG59XG5cbi5tYWluLXRhYmxle1xuICB3aWR0aDogMTAwJTtcbiAgYm9yZGVyLWNvbGxhcHNlOiBjb2xsYXBzZTtcblxufVxuXG4uY2xpZW50LWhlYWRlciB7XG4gIGJhY2tncm91bmQtaW1hZ2U6IHVybChcImh0dHBzOi8vd3d3Lm5vdHRpbmdoYW10ZWMuY28udWsvaW1ncy93b2YyMDE0LTEuanBnXCIpO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMjIyO1xuICBiYWNrZ3JvdW5kLXJlcGVhdDogbm8tcmVwZWF0O1xuICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBjZW50ZXI7XG5cbiAgd2lkdGg6IDEwMCU7XG5cbiAgbWFyZ2luLWJvdHRvbTogMjhweDtcblxuICAubG9nb3N7XG4gICAgd2lkdGg6IDEwMCU7XG4gICAgbWF4LXdpZHRoOiA2NDBweDtcbiAgfVxuXG4gIGltZyB7XG4gICAgaGVpZ2h0OiAxMTBweDtcbiAgfVxufVxuXG4uY29udGVudC1jb250YWluZXJ7XG4gIHdpZHRoOiAxMDAlO1xuXG4gIC5jb250ZW50IHtcbiAgICBmb250LWZhbWlseTogXCJPcGVuIFNhbnNcIiwgXCJIZWx2ZXRpY2EgTmV1ZVwiLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmO1xuXG4gICAgd2lkdGg6IDEwMCU7XG4gICAgbWF4LXdpZHRoOiA2MDBweDtcbiAgICBwYWRkaW5nOiAxMHB4O1xuICAgIHRleHQtYWxpZ246IGxlZnQ7XG5cbiAgICAuYnV0dG9uLWNvbnRhaW5lcntcbiAgICAgIHdpZHRoOiAxMDAlO1xuXG4gICAgICAuYnV0dG9uIHtcbiAgICAgICAgcGFkZGluZzogNnB4IDEycHg7XG4gICAgICAgIGJhY2tncm91bmQtY29sb3I6ICRidXR0b25fY29sb3I7XG4gICAgICAgIGJvcmRlci1yYWRpdXM6IDRweDtcblxuICAgICAgICBhIHtcbiAgICAgICAgICBjb2xvcjogI2ZmZjtcbiAgICAgICAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7XG4gICAgICAgIH1cblxuICAgICAgfVxuXG4gICAgfVxuXG4gIH1cbn1cblxuIl19 */
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
|
||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJpZS5jc3MiLCJzb3VyY2VzQ29udGVudCI6W119 */
|
||||
File diff suppressed because one or more lines are too long
1047
RIGS/static/css/main.min.css
vendored
1047
RIGS/static/css/main.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* Bootstrap alert.js v4.5.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("jquery"),require("./util.js")):"function"==typeof define&&define.amd?define(["jquery","./util.js"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Alert=t(e.jQuery,e.Util)}(this,(function(e,t){"use strict";function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}e=e&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e,t=t&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t;var r=e.fn.alert,o=function(){function r(e){this._element=e}var o,l,i,a=r.prototype;return a.close=function(e){var t=this._element;e&&(t=this._getRootElement(e)),this._triggerCloseEvent(t).isDefaultPrevented()||this._removeElement(t)},a.dispose=function(){e.removeData(this._element,"bs.alert"),this._element=null},a._getRootElement=function(n){var r=t.getSelectorFromElement(n),o=!1;return r&&(o=document.querySelector(r)),o||(o=e(n).closest(".alert")[0]),o},a._triggerCloseEvent=function(t){var n=e.Event("close.bs.alert");return e(t).trigger(n),n},a._removeElement=function(n){var r=this;if(e(n).removeClass("show"),e(n).hasClass("fade")){var o=t.getTransitionDurationFromElement(n);e(n).one(t.TRANSITION_END,(function(e){return r._destroyElement(n,e)})).emulateTransitionEnd(o)}else this._destroyElement(n)},a._destroyElement=function(t){e(t).detach().trigger("closed.bs.alert").remove()},r._jQueryInterface=function(t){return this.each((function(){var n=e(this),o=n.data("bs.alert");o||(o=new r(this),n.data("bs.alert",o)),"close"===t&&o[t](this)}))},r._handleDismiss=function(e){return function(t){t&&t.preventDefault(),e.close(this)}},o=r,i=[{key:"VERSION",get:function(){return"4.5.2"}}],(l=null)&&n(o.prototype,l),i&&n(o,i),r}();return e(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',o._handleDismiss(new o)),e.fn.alert=o._jQueryInterface,e.fn.alert.Constructor=o,e.fn.alert.noConflict=function(){return e.fn.alert=r,o._jQueryInterface},o}));
|
||||
File diff suppressed because one or more lines are too long
1
RIGS/static/js/asteroids.min.js
vendored
1
RIGS/static/js/asteroids.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
function changeSelectedValue(e,t,a,r){e.find("option").remove(),e.append($("<option></option>").attr("value",t).text(a).data("update_url",r)),e.selectpicker("render"),e.selectpicker("refresh"),e.selectpicker("val",t),e.change()}function refreshUpdateHref(e){targetObject=$("#"+e.attr("id")+"-update"),update_url=$("option:selected",e).data("update_url"),""==update_url?(targetObject.removeAttr("href"),targetObject.addClass("disabled")):(targetObject.prop("href",update_url),targetObject.removeClass("disabled"))}function initPicker(e){var t={ajax:{url:e.data("sourceurl"),type:"GET",dataType:"json",data:{term:"{{{q}}}"}},locale:{emptyTitle:""},clearOnEmpty:!1,preprocessData:function(e){var t,a=e.length,r=[];if(r.push({text:clearSelectionLabel,value:"",data:{update_url:"",subtext:""}}),a)for(t=0;t<a;t++)r.push($.extend(!0,e[t],{text:e[t].label,value:e[t].pk,data:{update_url:e[t].update,subtext:""}}));return r}};e.prepend($("<option></option>").attr("value","").text(clearSelectionLabel).data("update_url","")),e.selectpicker().ajaxSelectPicker(t),e.change((function(){refreshUpdateHref(e)})),refreshUpdateHref(e)}$(document).ready((function(){clearSelectionLabel="(no selection)",$(".selectpicker").each((function(){initPicker($(this))})),$("#modal").on("hide.bs.modal",(function(e){null!=modaltarget&&""!=modalobject&&changeSelectedValue($(modaltarget),modalobject[0].pk,modalobject[0].fields.name,modalobject[0].update_url)}))}));
|
||||
1
RIGS/static/js/bootstrap-datetimepicker.js
vendored
1
RIGS/static/js/bootstrap-datetimepicker.js
vendored
File diff suppressed because one or more lines are too long
7
RIGS/static/js/bootstrap-select.js
vendored
7
RIGS/static/js/bootstrap-select.js
vendored
File diff suppressed because one or more lines are too long
7
RIGS/static/js/clipboard.min.js
vendored
7
RIGS/static/js/clipboard.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
RIGS/static/js/dark-mode-switch.min.js
vendored
1
RIGS/static/js/dark-mode-switch.min.js
vendored
@@ -1 +0,0 @@
|
||||
!function(){var t,e=document.getElementById("darkSwitch");e&&(t=null!==localStorage.getItem("darkSwitch")&&"dark"===localStorage.getItem("darkSwitch"),(e.checked=t)?document.body.setAttribute("data-theme","dark"):document.body.removeAttribute("data-theme"),e.addEventListener("change",(function(t){e.checked?(document.body.setAttribute("data-theme","dark"),localStorage.setItem("darkSwitch","dark")):(document.body.removeAttribute("data-theme"),localStorage.removeItem("darkSwitch"))})))}();
|
||||
@@ -1 +0,0 @@
|
||||
$(document).ready((function(){var t;(t=document.createElement("input")).setAttribute("type","datetime-local"),("text"===t.type||navigator.userAgent.toLowerCase().indexOf("firefox")>-1)&&($("<link>").appendTo("head").attr({type:"text/css",rel:"stylesheet"}).attr("href",'{% static "css/flatpickr.css" %}'),$.when($.getScript('{% static "js/flatpickr.min.js" %}'),$.Deferred((function(t){$(t.resolve)}))).done((function(){$("input[type=datetime-local]").attr("type","text").flatpickr({dateFormat:"Y-m-dTH:m",enableTime:!0,altInput:!0,altFormat:"d/m/y H:m"})})))}));
|
||||
File diff suppressed because one or more lines are too long
2
RIGS/static/js/flatpickr.min.js
vendored
2
RIGS/static/js/flatpickr.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
function setupItemTable(t){objectitems=JSON.parse(t),$.each(objectitems,(function(t,e){objectitems[t]=JSON.parse(e)})),newitem=-1}function nl2br(t,e){return(t+"").replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g,"$1"+(e||void 0===e?"<br />":"<br>")+"$2")}function escapeHtml(t){return $("<div/>").text(t).html()}function updatePrices(){var t=0;for(var e in objectitems){var i=objectitems[e].fields,a=i.cost*i.quantity;$("#item-"+e+" .sub-total").html(parseFloat(a).toFixed(2)).data("subtotal",a),t+=Number(a)}$("#sumtotal").text(parseFloat(t).toFixed(2));var o=t*Number($("#vat-rate").data("rate"));$("#vat").text(parseFloat(o).toFixed(2)),$("#total").text(parseFloat(t+o).toFixed(2))}$("#item-table").on("click",".item-delete",(function(){delete objectitems[$(this).data("pk")],$("#item-"+$(this).data("pk")).remove(),updatePrices()})),$("#item-table").on("click",".item-add",(function(){$("#item-form").data("pk",newitem),$("#item_name").val(""),$("#item_description").val(""),$("#item_quantity").val(""),$("#item_cost").val(""),$($(this).data("target")).modal("show")})),$("#item-table").on("click",".item-edit",(function(){var t=$(this).data("pk");$("#item-form").data("pk",t);var e=objectitems[t].fields;$("#item_name").val(e.name),$("#item_description").val(e.description),$("#item_quantity").val(e.quantity),$("#item_cost").val(e.cost),$($(this).data("target")).modal("show")})),$("body").on("submit","#item-form",(function(t){t.preventDefault();var e,i=$(this).data("pk");if($("#itemModal").modal("hide"),i==newitem--){(e=new Object).name=$("#item_name").val(),e.description=$("#item_description").val(),e.cost=$("#item_cost").val(),e.quantity=$("#item_quantity").val();var a=0;for(item in objectitems)a++;e.order=a,objectitems[i]=new Object,objectitems[i].fields=e,$("#new-item-row").clone().attr("id","item-"+i).data("pk",i).appendTo("#item-table-body"),$("#item-"+i+" .item-delete, #item-"+i+" .item-edit").data("pk",i)}else(e=objectitems[i].fields).name=$("#item_name").val(),e.description=$("#item_description").val(),e.cost=$("#item_cost").val(),e.quantity=$("#item_quantity").val(),objectitems[i].fields=e;$row=$("#item-"+i),$row.find(".name").html(escapeHtml(e.name)),$row.find(".description").html(nl2br(escapeHtml(e.description))),$row.find(".cost").html(parseFloat(e.cost).toFixed(2)),$row.find(".quantity").html(e.quantity),updatePrices()})),$("body").on("submit",".itemised_form",(function(t){$("#id_items_json").val(JSON.stringify(objectitems))}));var fixHelper=function(t,e){return e.children().each((function(){$(this).width($(this).width())})),e};$("#item-table tbody").sortable({helper:fixHelper,update:function(t,e){info=$(this).sortable("toArray"),itemorder=new Array,$.each(info,(function(t,e){pk=$("#"+e).data("pk"),objectitems[pk].fields.order=t}))}});
|
||||
186
RIGS/static/js/jquery-ui.js
vendored
186
RIGS/static/js/jquery-ui.js
vendored
File diff suppressed because one or more lines are too long
25
RIGS/static/js/jquery.js
vendored
25
RIGS/static/js/jquery.js
vendored
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
var Konami=function(t){var e={addEvent:function(t,e,n,o){t.addEventListener?t.addEventListener(e,n,!1):t.attachEvent&&(t["e"+e+n]=n,t[e+n]=function(){t["e"+e+n](window.event,o)},t.attachEvent("on"+e,t[e+n]))},removeEvent:function(t,e,n){t.removeEventListener?t.removeEventListener(e,n):t.attachEvent&&t.detachEvent(e)},input:"",pattern:"38384040373937396665",keydownHandler:function(t,n){if(n&&(e=n),e.input+=t?t.keyCode:event.keyCode,e.input.length>e.pattern.length&&(e.input=e.input.substr(e.input.length-e.pattern.length)),e.input===e.pattern)return e.code(e._currentLink),e.input="",t.preventDefault(),!1},load:function(t){this._currentLink=t,this.addEvent(document,"keydown",this.keydownHandler,this),this.iphone.load(t)},unload:function(){this.removeEvent(document,"keydown",this.keydownHandler),this.iphone.unload()},code:function(t){window.location=t},iphone:{start_x:0,start_y:0,stop_x:0,stop_y:0,tap:!1,capture:!1,orig_keys:"",keys:["UP","UP","DOWN","DOWN","LEFT","RIGHT","LEFT","RIGHT","TAP","TAP"],input:[],code:function(t){e.code(t)},touchmoveHandler:function(t){if(1===t.touches.length&&!0===e.iphone.capture){var n=t.touches[0];e.iphone.stop_x=n.pageX,e.iphone.stop_y=n.pageY,e.iphone.tap=!1,e.iphone.capture=!1,e.iphone.check_direction()}},touchendHandler:function(){if(e.iphone.input.push(e.iphone.check_direction()),e.iphone.input.length>e.iphone.keys.length&&e.iphone.input.shift(),e.iphone.input.length===e.iphone.keys.length){for(var t=!0,n=0;n<e.iphone.keys.length;n++)e.iphone.input[n]!==e.iphone.keys[n]&&(t=!1);t&&e.iphone.code(e._currentLink)}},touchstartHandler:function(t){e.iphone.start_x=t.changedTouches[0].pageX,e.iphone.start_y=t.changedTouches[0].pageY,e.iphone.tap=!0,e.iphone.capture=!0},load:function(t){this.orig_keys=this.keys,e.addEvent(document,"touchmove",this.touchmoveHandler),e.addEvent(document,"touchend",this.touchendHandler,!1),e.addEvent(document,"touchstart",this.touchstartHandler)},unload:function(){e.removeEvent(document,"touchmove",this.touchmoveHandler),e.removeEvent(document,"touchend",this.touchendHandler),e.removeEvent(document,"touchstart",this.touchstartHandler)},check_direction:function(){return x_magnitude=Math.abs(this.start_x-this.stop_x),y_magnitude=Math.abs(this.start_y-this.stop_y),x=this.start_x-this.stop_x<0?"RIGHT":"LEFT",y=this.start_y-this.stop_y<0?"DOWN":"UP",result=x_magnitude>y_magnitude?x:y,result=!0===this.tap?"TAP":result,result}}};return"string"==typeof t&&e.load(t),"function"==typeof t&&(e.code=t,e.load()),e};"undefined"!=typeof module&&void 0!==module.exports?module.exports=Konami:"function"==typeof define&&define.amd?define([],(function(){return Konami})):window.Konami=Konami;
|
||||
File diff suppressed because one or more lines are too long
6
RIGS/static/js/main.min.js
vendored
6
RIGS/static/js/main.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* Bootstrap popover.js v4.5.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("jquery"),require("./tooltip.js")):"function"==typeof define&&define.amd?define(["jquery","./tooltip.js"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).Popover=e(t.jQuery,t.Tooltip)}(this,(function(t,e){"use strict";function n(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}function o(){return(o=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var o in n)Object.prototype.hasOwnProperty.call(n,o)&&(t[o]=n[o])}return t}).apply(this,arguments)}t=t&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t,e=e&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e;var r="popover",i=".bs.popover",s=t.fn[r],p=new RegExp("(^|\\s)bs-popover\\S+","g"),u=o({},e.Default,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'}),a=o({},e.DefaultType,{content:"(string|element|function)"}),l={HIDE:"hide"+i,HIDDEN:"hidden"+i,SHOW:"show"+i,SHOWN:"shown"+i,INSERTED:"inserted"+i,CLICK:"click"+i,FOCUSIN:"focusin"+i,FOCUSOUT:"focusout"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i},c=function(e){var o,s;function c(){return e.apply(this,arguments)||this}s=e,(o=c).prototype=Object.create(s.prototype),o.prototype.constructor=o,o.__proto__=s;var f,h,d,y=c.prototype;return y.isWithContent=function(){return this.getTitle()||this._getContent()},y.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-popover-"+e)},y.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0],this.tip},y.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(".popover-header"),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(e.find(".popover-body"),n),e.removeClass("fade show")},y._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},y._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(p);null!==n&&n.length>0&&e.removeClass(n.join(""))},c._jQueryInterface=function(e){return this.each((function(){var n=t(this).data("bs.popover"),o="object"==typeof e?e:null;if((n||!/dispose|hide/.test(e))&&(n||(n=new c(this,o),t(this).data("bs.popover",n)),"string"==typeof e)){if(void 0===n[e])throw new TypeError('No method named "'+e+'"');n[e]()}}))},f=c,d=[{key:"VERSION",get:function(){return"4.5.2"}},{key:"Default",get:function(){return u}},{key:"NAME",get:function(){return r}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return l}},{key:"EVENT_KEY",get:function(){return i}},{key:"DefaultType",get:function(){return a}}],(h=null)&&n(f.prototype,h),d&&n(f,d),c}(e);return t.fn[r]=c._jQueryInterface,t.fn[r].Constructor=c,t.fn[r].noConflict=function(){return t.fn[r]=s,c._jQueryInterface},c}));
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +0,0 @@
|
||||
/*!
|
||||
* Bootstrap util.js v4.5.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).Util=e(t.jQuery)}(this,(function(t){"use strict";t=t&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t;function e(e){var r=this,o=!1;return t(this).one(n.TRANSITION_END,(function(){o=!0})),setTimeout((function(){o||n.triggerTransitionEnd(r)}),e),this}var n={TRANSITION_END:"bsTransitionEnd",getUID:function(t){do{t+=~~(1e6*Math.random())}while(document.getElementById(t));return t},getSelectorFromElement:function(t){var e=t.getAttribute("data-target");if(!e||"#"===e){var n=t.getAttribute("href");e=n&&"#"!==n?n.trim():""}try{return document.querySelector(e)?e:null}catch(t){return null}},getTransitionDurationFromElement:function(e){if(!e)return 0;var n=t(e).css("transition-duration"),r=t(e).css("transition-delay"),o=parseFloat(n),i=parseFloat(r);return o||i?(n=n.split(",")[0],r=r.split(",")[0],1e3*(parseFloat(n)+parseFloat(r))):0},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(e){t(e).trigger("transitionend")},supportsTransitionEnd:function(){return Boolean("transitionend")},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,r){for(var o in r)if(Object.prototype.hasOwnProperty.call(r,o)){var i=r[o],a=e[o],u=a&&n.isElement(a)?"element":null==(s=a)?""+s:{}.toString.call(s).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(i).test(u))throw new Error(t.toUpperCase()+': Option "'+o+'" provided type "'+u+'" but expected type "'+i+'".')}var s},findShadowRoot:function(t){if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){var e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?n.findShadowRoot(t.parentNode):null},jQueryDetection:function(){if(void 0===t)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1===e[0]&&9===e[1]&&e[2]<1||e[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};return n.jQueryDetection(),t.fn.emulateTransitionEnd=e,t.event.special[n.TRANSITION_END]={bindType:"transitionend",delegateType:"transitionend",handle:function(e){if(t(e.target).is(this))return e.handleObj.handler.apply(this,arguments)}},n}));
|
||||
@@ -11,14 +11,6 @@
|
||||
<script src="{% static 'js/moment.js' %}"></script>
|
||||
<script src="{% static 'js/main.min.js' %}"></script>
|
||||
<script>
|
||||
function getUrlVars() {
|
||||
var vars = {};
|
||||
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
|
||||
vars[key] = value;
|
||||
});
|
||||
return vars;
|
||||
}
|
||||
|
||||
viewToUrl = {
|
||||
'timeGridWeek':'week',
|
||||
'timeGridDay':'day',
|
||||
@@ -78,7 +70,7 @@
|
||||
'url': $(this).attr('url')
|
||||
}
|
||||
|
||||
if($(this).attr('is_rig')==true || $(this).attr('status') == "Cancelled"){
|
||||
if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){
|
||||
thisEvent['color'] = colours[$(this).attr('status')];
|
||||
}else{
|
||||
thisEvent['color'] = colours['non-rig'];
|
||||
@@ -95,31 +87,35 @@
|
||||
$('#calendar-header').text(view.title);
|
||||
|
||||
// Enable/Disable "Today" button as required
|
||||
let $today = $('#today-button');
|
||||
if(moment().isBetween(view.currentStart, view.currentEnd)){
|
||||
//Today is within the current view
|
||||
$('#today-button').prop('disabled', true);
|
||||
$today.prop('disabled', true);
|
||||
}else{
|
||||
$('#today-button').prop('disabled', false);
|
||||
$today.prop('disabled', false);
|
||||
}
|
||||
|
||||
// Set active view select button
|
||||
let $month = $('#month-button');
|
||||
let $week = $('#week-button');
|
||||
let $day = $('#day-button');
|
||||
switch(view.type){
|
||||
case 'dayGridMonth':
|
||||
$('#month-button').addClass('active');
|
||||
$('#week-button').removeClass('active');
|
||||
$('#day-button').removeClass('active');
|
||||
$month.addClass('active');
|
||||
$week.removeClass('active');
|
||||
$day.removeClass('active');
|
||||
break;
|
||||
|
||||
case 'timeGridWeek':
|
||||
$('#month-button').removeClass('active');
|
||||
$('#week-button').addClass('active');
|
||||
$('#day-button').removeClass('active');
|
||||
$month.removeClass('active');
|
||||
$week.addClass('active');
|
||||
$day.removeClass('active');
|
||||
break;
|
||||
|
||||
case 'timeGridDay':
|
||||
$('#month-button').removeClass('active');
|
||||
$('#week-button').removeClass('active');
|
||||
$('#day-button').addClass('active');
|
||||
$month.removeClass('active');
|
||||
$week.removeClass('active');
|
||||
$day.addClass('active');
|
||||
break;
|
||||
}
|
||||
history.replaceState(null,null,"{% url 'web_calendar' %}"+viewToUrl[view.type]+'/'+moment(view.currentStart).format('YYYY-MM-DD')+'/');
|
||||
|
||||
@@ -30,21 +30,6 @@
|
||||
{% include 'partials/datetime-fix.html' %}
|
||||
|
||||
<script>
|
||||
function setTime23Hours() {
|
||||
$('#{{ form.end_time.id_for_label }}').val('23:00');
|
||||
}
|
||||
|
||||
function setTime02Hours() {
|
||||
var id_start = "{{ form.start_date.id_for_label }}";
|
||||
var id_end_date = "{{ form.end_date.id_for_label }}";
|
||||
var id_end_time = "{{ form.end_time.id_for_label }}";
|
||||
if ($('#'+id_start).val() == $('#'+id_end_date).val()) {
|
||||
var end_date = new Date($('#'+id_end_date).val());
|
||||
end_date.setDate(end_date.getDate() + 1);
|
||||
$('#'+id_end_date).val(end_date.getISOString());
|
||||
}
|
||||
$('#'+id_end_time).val('02:00');
|
||||
}
|
||||
const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches;
|
||||
$(document).ready(function () {
|
||||
dur = matches ? 0 : 500;
|
||||
@@ -53,14 +38,14 @@
|
||||
$('.form-is_rig').slideUp(dur);
|
||||
});
|
||||
{% elif not object.pk and form.errors %}
|
||||
if ($('#{{form.is_rig.auto_id}}').attr('checked') != 'checked') {
|
||||
if ($('#{{form.is_rig.auto_id}}').attr('checked') !== 'checked') {
|
||||
$('.form-is_rig').hide();
|
||||
}
|
||||
{% endif %}
|
||||
{% if not object.pk %}
|
||||
$('#is_rig-selector button').on('click', function () {
|
||||
$('.form-non_rig').slideDown(dur);
|
||||
if ($(this).data('is_rig') == 1) {
|
||||
if ($(this).data('is_rig') === 1) {
|
||||
$('#{{form.is_rig.auto_id}}').prop('checked', true);
|
||||
if ($('.form-non_rig').is(':hidden')) {
|
||||
$('.form-is_rig').show();
|
||||
|
||||
@@ -27,12 +27,7 @@
|
||||
|
||||
<script>
|
||||
function parseBool(str) {
|
||||
if (str.toLowerCase() == 'true') {
|
||||
return true
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
return str.toLowerCase() == 'true';
|
||||
}
|
||||
$('input[type=radio][name=big_power]').change(function() {
|
||||
$('#{{ form.power_mic.id_for_label }}').prop('required', parseBool(this.value));
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from django import template
|
||||
from django import forms
|
||||
from django import template
|
||||
from django.forms.forms import NON_FIELD_ERRORS
|
||||
from django.forms.utils import ErrorDict
|
||||
from django.utils.text import normalize_newlines
|
||||
from django.template.defaultfilters import stringfilter
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
from django.utils.html import escape
|
||||
from RIGS import models
|
||||
import json
|
||||
from django.template.defaultfilters import yesno, title, truncatewords
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
from django.utils.text import normalize_newlines
|
||||
|
||||
from RIGS import models
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
from pypom import Page, Region
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions
|
||||
from selenium.webdriver import Chrome
|
||||
from django.urls import reverse
|
||||
from PyRIGS.tests import regions
|
||||
from RIGS.tests import regions as rigs_regions
|
||||
from PyRIGS.tests.pages import BasePage, FormPage
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from pypom import Region
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from PyRIGS.tests import regions
|
||||
from PyRIGS.tests.pages import BasePage, FormPage
|
||||
from RIGS.tests import regions as rigs_regions
|
||||
|
||||
|
||||
class Index(BasePage):
|
||||
URL_TEMPLATE = reverse('index')
|
||||
@@ -304,3 +302,43 @@ class UserPage(BasePage):
|
||||
|
||||
def generate_key(self):
|
||||
self.find_element(*self._generation_button_selector).click()
|
||||
|
||||
|
||||
class CalendarPage(BasePage):
|
||||
URL_TEMPLATE = 'rigboard/calendar'
|
||||
|
||||
_go_locator = (By.ID, 'go-to-date-button')
|
||||
|
||||
_today_selector = (By.ID, 'today-button')
|
||||
|
||||
_prev_selector = (By.ID, 'prev-button')
|
||||
_next_selector = (By.ID, 'next-button')
|
||||
|
||||
_month_selector = (By.ID, 'month-button')
|
||||
_week_selector = (By.ID, 'week-button')
|
||||
_day_selector = (By.ID, 'day-button')
|
||||
|
||||
def go(self):
|
||||
return self.find_element(*self._go_locator).click()
|
||||
|
||||
@property
|
||||
def target_date(self):
|
||||
return regions.DatePicker(self, self.find_element(By.ID, 'go-to-date-input'))
|
||||
|
||||
def today(self):
|
||||
return self.find_element(*self._today_selector).click()
|
||||
|
||||
def prev(self):
|
||||
return self.find_element(*self._prev_selector).click()
|
||||
|
||||
def next(self):
|
||||
return self.find_element(*self._next_selector).click()
|
||||
|
||||
def month(self):
|
||||
return self.find_element(*self._month_selector).click()
|
||||
|
||||
def week(self):
|
||||
return self.find_element(*self._week_selector).click()
|
||||
|
||||
def day(self):
|
||||
return self.find_element(*self._day_selector).click()
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
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 PyRIGS.tests.regions import TextBox, Modal
|
||||
|
||||
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import datetime
|
||||
from datetime import date, time, timedelta
|
||||
from urllib.parse import urlparse
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.core import mail, signing
|
||||
from django.core.management import call_command
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from reversion import revisions as reversion
|
||||
from RIGS import models, urls
|
||||
from RIGS.tests import regions
|
||||
|
||||
from . import pages
|
||||
from RIGS import models
|
||||
from pytest_django.asserts import assertContains, assertNotContains
|
||||
|
||||
|
||||
class BaseCase(TestCase):
|
||||
@@ -52,12 +45,32 @@ class TestEventValidation(BaseCase):
|
||||
def test_create(self):
|
||||
url = reverse('event_create')
|
||||
# end time before start access after start
|
||||
response = self.client.post(url, {'start_date': datetime.date(2020, 1, 1), 'start_time': datetime.time(10, 00), 'end_time': datetime.time(9, 00), 'access_at': datetime.datetime(2020, 1, 5, 10)})
|
||||
self.assertFormError(response, 'form', 'end_time', "Unless you've invented time travel, the event can't finish before it has started.")
|
||||
self.assertFormError(response, 'form', 'access_at', "Regardless of what some clients might think, access time cannot be after the event has started.")
|
||||
response = self.client.post(url, {'start_date': datetime.date(2020, 1, 1), 'start_time': datetime.time(10, 00),
|
||||
'end_time': datetime.time(9, 00),
|
||||
'access_at': datetime.datetime(2020, 1, 5, 10)})
|
||||
self.assertFormError(response, 'form', 'end_time',
|
||||
"Unless you've invented time travel, the event can't finish before it has started.")
|
||||
self.assertFormError(response, 'form', 'access_at',
|
||||
"Regardless of what some clients might think, access time cannot be after the event has started.")
|
||||
|
||||
|
||||
class ClientEventAuthorisationTest(BaseCase):
|
||||
def setup_event():
|
||||
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
venue = models.Venue.objects.create(name='Authorisation Test Venue')
|
||||
client = models.Person.objects.create(name='Authorisation Test Person', email='authorisation@functional.test')
|
||||
organisation = models.Organisation.objects.create(name='Authorisation Test Organisation', union_account=True)
|
||||
return models.Event.objects.create(
|
||||
name='Authorisation Test',
|
||||
start_date=date.today(),
|
||||
venue=venue,
|
||||
person=client,
|
||||
organisation=organisation,
|
||||
)
|
||||
|
||||
|
||||
def setup_mail(event, profile):
|
||||
profile.email = "teccie@nottinghamtec.co.uk"
|
||||
profile.save()
|
||||
auth_data = {
|
||||
'name': 'Test ABC',
|
||||
'po': '1234ABCZXY',
|
||||
@@ -65,72 +78,78 @@ class ClientEventAuthorisationTest(BaseCase):
|
||||
'uni_id': 1234567890,
|
||||
'tos': True
|
||||
}
|
||||
hmac = signing.dumps({'pk': event.pk, 'email': 'authemail@function.test',
|
||||
'sent_by': profile.pk})
|
||||
url = reverse('event_authorise', kwargs={'pk': event.pk, 'hmac': hmac})
|
||||
return auth_data, hmac, url
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.hmac = signing.dumps({'pk': self.event.pk, 'email': 'authemail@function.test',
|
||||
'sent_by': self.profile.pk})
|
||||
self.url = reverse('event_authorise', kwargs={'pk': self.event.pk, 'hmac': self.hmac})
|
||||
|
||||
def test_requires_valid_hmac(self):
|
||||
bad_hmac = self.hmac[:-1]
|
||||
url = reverse('event_authorise', kwargs={'pk': self.event.pk, 'hmac': bad_hmac})
|
||||
response = self.client.get(url)
|
||||
self.assertIsInstance(response, HttpResponseBadRequest)
|
||||
# TODO: Add some form of sensbile user facing error
|
||||
# self.assertIn(response.content, "new URL") # check there is some level of sane instruction
|
||||
def test_requires_valid_hmac(client, admin_user):
|
||||
event = setup_event()
|
||||
auth_data, hmac, url = setup_mail(event, admin_user)
|
||||
bad_hmac = hmac[:-1]
|
||||
url = reverse('event_authorise', kwargs={'pk': event.pk, 'hmac': bad_hmac})
|
||||
response = client.get(url)
|
||||
assert isinstance(response, HttpResponseBadRequest)
|
||||
# TODO: Add some form of sensible user facing error
|
||||
# self.assertIn(response.content, "new URL") # check there is some level of sane instruction
|
||||
# response = client.get(url)
|
||||
# assertContains(response, event.organisation.name)
|
||||
|
||||
response = self.client.get(self.url)
|
||||
self.assertContains(response, self.event.organisation.name)
|
||||
|
||||
def test_validation(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertContains(response, "Terms of Hire")
|
||||
self.assertContains(response, "Account code")
|
||||
self.assertContains(response, "University ID")
|
||||
def test_validation(client, admin_user):
|
||||
event = setup_event()
|
||||
auth_data, hmac, url = setup_mail(event, admin_user)
|
||||
response = client.get(url)
|
||||
assertContains(response, "Terms of Hire")
|
||||
assertContains(response, "Account code")
|
||||
assertContains(response, "University ID")
|
||||
|
||||
response = self.client.post(self.url)
|
||||
self.assertContains(response, "This field is required.", 5)
|
||||
response = client.post(url)
|
||||
assertContains(response, "This field is required.", 5)
|
||||
|
||||
data = self.auth_data
|
||||
data['amount'] = self.event.total + 1
|
||||
auth_data['amount'] = event.total + 1
|
||||
|
||||
response = self.client.post(self.url, data)
|
||||
self.assertContains(response, "The amount authorised must equal the total for the event")
|
||||
self.assertNotContains(response, "This field is required.")
|
||||
response = client.post(url, auth_data)
|
||||
assertContains(response, "The amount authorised must equal the total for the event")
|
||||
assertNotContains(response, "This field is required.")
|
||||
|
||||
data['amount'] = self.event.total
|
||||
response = self.client.post(self.url, data)
|
||||
self.assertContains(response, "Your event has been authorised")
|
||||
auth_data['amount'] = event.total
|
||||
response = client.post(url, auth_data)
|
||||
assertContains(response, "Your event has been authorised")
|
||||
|
||||
self.event.refresh_from_db()
|
||||
self.assertTrue(self.event.authorised)
|
||||
self.assertEqual(self.event.authorisation.email, "authemail@function.test")
|
||||
event.refresh_from_db()
|
||||
assert event.authorised
|
||||
assert str(event.authorisation.email) == "authemail@function.test"
|
||||
|
||||
def test_duplicate_warning(self):
|
||||
auth = models.EventAuthorisation.objects.create(event=self.event, name='Test ABC', email='dupe@functional.test',
|
||||
amount=self.event.total, sent_by=self.profile)
|
||||
response = self.client.get(self.url)
|
||||
self.assertContains(response, 'This event has already been authorised.')
|
||||
|
||||
auth.amount += 1
|
||||
auth.save()
|
||||
def test_duplicate_warning(client, admin_user):
|
||||
event = setup_event()
|
||||
auth_data, hmac, url = setup_mail(event, admin_user)
|
||||
auth = models.EventAuthorisation.objects.create(event=event, name='Test ABC', email='dupe@functional.test',
|
||||
amount=event.total, sent_by=admin_user)
|
||||
response = client.get(url)
|
||||
assertContains(response, 'This event has already been authorised.')
|
||||
|
||||
response = self.client.get(self.url)
|
||||
self.assertContains(response, 'amount has changed')
|
||||
auth.amount += 1
|
||||
auth.save()
|
||||
|
||||
def test_email_sent(self):
|
||||
mail.outbox = []
|
||||
response = client.get(url)
|
||||
assertContains(response, 'amount has changed')
|
||||
|
||||
data = self.auth_data
|
||||
data['amount'] = self.event.total
|
||||
|
||||
response = self.client.post(self.url, data)
|
||||
self.assertContains(response, "Your event has been authorised.")
|
||||
self.assertEqual(len(mail.outbox), 2)
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_email_sent(admin_client, admin_user, mailoutbox):
|
||||
event = setup_event()
|
||||
auth_data, hmac, url = setup_mail(event, admin_user)
|
||||
|
||||
self.assertEqual(mail.outbox[0].to, ['authemail@function.test'])
|
||||
self.assertEqual(mail.outbox[1].to, [settings.AUTHORISATION_NOTIFICATION_ADDRESS])
|
||||
data = auth_data
|
||||
data['amount'] = event.total
|
||||
response = admin_client.post(url, data)
|
||||
assertContains(response, "Your event has been authorised.")
|
||||
assert len(mailoutbox) == 2
|
||||
assert mailoutbox[0].to == ['authemail@function.test']
|
||||
assert mailoutbox[1].to == [settings.AUTHORISATION_NOTIFICATION_ADDRESS]
|
||||
|
||||
|
||||
class TECEventAuthorisationTest(BaseCase):
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
import datetime
|
||||
from datetime import date, time, timedelta
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import mail, signing
|
||||
from django.core.management import call_command
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from PyRIGS.tests import base
|
||||
from PyRIGS.tests import regions as base_regions
|
||||
from PyRIGS.tests.base import (AutoLoginTest, BaseTest, animation_is_finished,
|
||||
screenshot_failure_cls)
|
||||
from reversion import revisions as reversion
|
||||
from RIGS import models, urls
|
||||
from RIGS.tests import regions
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
|
||||
from PyRIGS.tests import base
|
||||
from PyRIGS.tests import regions as base_regions
|
||||
from PyRIGS.tests.base import (AutoLoginTest, screenshot_failure_cls)
|
||||
from PyRIGS.tests.pages import animation_is_finished
|
||||
from RIGS import models
|
||||
from RIGS.tests import regions
|
||||
from . import pages
|
||||
|
||||
|
||||
@@ -251,7 +239,7 @@ class TestEventCreate(BaseRigboardTest):
|
||||
# Double-check we don't lose data when swapping
|
||||
self.page.select_event_type("Rig")
|
||||
self.wait.until(animation_is_finished())
|
||||
self.assertEquals(self.page.name, rig_name)
|
||||
self.assertEqual(self.page.name, rig_name)
|
||||
self.wait.until(animation_is_finished())
|
||||
self.page.select_event_type("Non-Rig")
|
||||
|
||||
@@ -645,7 +633,18 @@ class TestCalendar(BaseRigboardTest):
|
||||
else:
|
||||
self.assertNotContains(response, "TE E" + str(test) + " ")
|
||||
|
||||
# Wow - that was a lot of tests
|
||||
def test_calendar_buttons(self): # If FullCalendar fails to load for whatever reason, the buttons don't work
|
||||
self.page = pages.CalendarPage(self.driver, self.live_server_url).open()
|
||||
self.assertIn(timezone.now().strftime("%Y-%m"), self.driver.current_url)
|
||||
|
||||
target_date = datetime.date(2020, 1, 1)
|
||||
self.page.target_date.set_value(target_date)
|
||||
self.page.go()
|
||||
self.assertIn(self.page.target_date.value.strftime("%Y-%m"), self.driver.current_url)
|
||||
|
||||
self.page.next()
|
||||
target_date += datetime.timedelta(days=32)
|
||||
self.assertIn(target_date.strftime("%m"), self.driver.current_url)
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import pytz
|
||||
from reversion import revisions as reversion
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
from RIGS import models
|
||||
from versioning import versioning
|
||||
from datetime import date, timedelta, datetime, time
|
||||
from decimal import *
|
||||
from PyRIGS.tests.base import create_browser
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
class ProfileTestCase(TestCase):
|
||||
|
||||
@@ -2,13 +2,14 @@ from datetime import date
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.management import call_command
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from pytest_django.asserts import assertRedirects, assertNotContains, assertContains
|
||||
|
||||
from PyRIGS.tests.base import assert_times_equal
|
||||
from RIGS import models
|
||||
from reversion import revisions as reversion
|
||||
|
||||
|
||||
class TestAdminMergeObjects(TestCase):
|
||||
@@ -354,203 +355,105 @@ class TestSampleDataGenerator(TestCase):
|
||||
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleRIGSData')
|
||||
|
||||
|
||||
class TestSearchLogic(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
|
||||
is_active=True, is_staff=True)
|
||||
|
||||
cls.persons = {
|
||||
1: models.Person.objects.create(name="Right Person", phone="1234"),
|
||||
2: models.Person.objects.create(name="Wrong Person", phone="5678"),
|
||||
}
|
||||
|
||||
cls.organisations = {
|
||||
1: models.Organisation.objects.create(name="Right Organisation", email="test@example.com"),
|
||||
2: models.Organisation.objects.create(name="Wrong Organisation", email="check@fake.co.uk"),
|
||||
}
|
||||
|
||||
cls.venues = {
|
||||
1: models.Venue.objects.create(name="Right Venue", address="1 Test Street, EX1"),
|
||||
2: models.Venue.objects.create(name="Wrong Venue", address="2 Check Way, TS2"),
|
||||
}
|
||||
|
||||
cls.events = {
|
||||
1: models.Event.objects.create(name="Right Event", start_date=date.today(), person=cls.persons[1],
|
||||
organisation=cls.organisations[1], venue=cls.venues[1]),
|
||||
2: models.Event.objects.create(name="Wrong Event", start_date=date.today(), person=cls.persons[2],
|
||||
organisation=cls.organisations[2], venue=cls.venues[2]),
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test_event_search(self):
|
||||
# Test search by name
|
||||
request_url = "%s?q=%s" % (reverse('event_archive'), self.events[1].name)
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, self.events[1].name)
|
||||
self.assertNotContains(response, self.events[2].name)
|
||||
|
||||
# Test search by ID
|
||||
request_url = "%s?q=%s" % (reverse('event_archive'), self.events[1].pk)
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, self.events[1].name)
|
||||
self.assertNotContains(response, self.events[2].name)
|
||||
|
||||
def test_people_search(self):
|
||||
# Test search by name
|
||||
request_url = "%s?q=%s" % (reverse('person_list'), self.persons[1].name)
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, self.persons[1].name)
|
||||
self.assertNotContains(response, self.persons[2].name)
|
||||
|
||||
# Test search by ID
|
||||
request_url = "%s?q=%s" % (reverse('person_list'), self.persons[1].pk)
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, self.persons[1].name)
|
||||
self.assertNotContains(response, self.persons[2].name)
|
||||
|
||||
# Test search by phone
|
||||
request_url = "%s?q=%s" % (reverse('person_list'), self.persons[1].phone)
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, self.persons[1].name)
|
||||
self.assertNotContains(response, self.persons[2].name)
|
||||
|
||||
def test_organisation_search(self):
|
||||
# Test search by name
|
||||
request_url = "%s?q=%s" % (reverse('organisation_list'), self.organisations[1].name)
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, self.organisations[1].name)
|
||||
self.assertNotContains(response, self.organisations[2].name)
|
||||
|
||||
# Test search by ID
|
||||
request_url = "%s?q=%s" % (reverse('organisation_list'), self.organisations[1].pk)
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, self.organisations[1].name)
|
||||
self.assertNotContains(response, self.organisations[2].name)
|
||||
|
||||
# Test search by email
|
||||
request_url = "%s?q=%s" % (reverse('organisation_list'), self.organisations[1].email)
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, self.organisations[1].email)
|
||||
self.assertNotContains(response, self.organisations[2].email)
|
||||
|
||||
def test_venue_search(self):
|
||||
# Test search by name
|
||||
request_url = "%s?q=%s" % (reverse('venue_list'), self.venues[1].name)
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, self.venues[1].name)
|
||||
self.assertNotContains(response, self.venues[2].name)
|
||||
|
||||
# Test search by ID
|
||||
request_url = "%s?q=%s" % (reverse('venue_list'), self.venues[1].pk)
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, self.venues[1].name)
|
||||
self.assertNotContains(response, self.venues[2].name)
|
||||
|
||||
# Test search by address
|
||||
request_url = "%s?q=%s" % (reverse('venue_list'), self.venues[1].address)
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, self.venues[1].address)
|
||||
self.assertNotContains(response, self.venues[2].address)
|
||||
def search(client, url, found, notfound, arguments):
|
||||
for argument in arguments:
|
||||
query = getattr(found, argument)
|
||||
request_url = "%s?q=%s" % (reverse_lazy(url), query)
|
||||
response = client.get(request_url, follow=True)
|
||||
assertContains(response, getattr(found, 'name'))
|
||||
assertNotContains(response, getattr(notfound, 'name'))
|
||||
|
||||
|
||||
class TestHSLogic(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
|
||||
is_active=True, is_staff=True)
|
||||
def test_search(admin_client):
|
||||
persons = {
|
||||
1: models.Person.objects.create(name="Right Person", phone="1234"),
|
||||
2: models.Person.objects.create(name="Wrong Person", phone="5678"),
|
||||
}
|
||||
organisations = {
|
||||
1: models.Organisation.objects.create(name="Right Organisation", email="test@example.com"),
|
||||
2: models.Organisation.objects.create(name="Wrong Organisation", email="check@fake.co.uk"),
|
||||
}
|
||||
venues = {
|
||||
1: models.Venue.objects.create(name="Right Venue", address="1 Test Street, EX1"),
|
||||
2: models.Venue.objects.create(name="Wrong Venue", address="2 Check Way, TS2"),
|
||||
}
|
||||
events = {
|
||||
1: models.Event.objects.create(name="Right Event", start_date=date.today(), venue=venues[1], person=persons[1],
|
||||
organisation=organisations[1]),
|
||||
2: models.Event.objects.create(name="Wrong Event", start_date=date.today(), venue=venues[2], person=persons[2],
|
||||
organisation=organisations[2]),
|
||||
}
|
||||
search(admin_client, 'event_archive', events[1], events[2], ['name', 'id'])
|
||||
search(admin_client, 'person_list', persons[1], persons[2], ['name', 'id', 'phone'])
|
||||
search(admin_client, 'organisation_list', organisations[1], organisations[2],
|
||||
['name', 'id', 'email'])
|
||||
search(admin_client, 'venue_list', venues[1], venues[2],
|
||||
['name', 'id', 'address'])
|
||||
|
||||
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
cls.venue = models.Venue.objects.create(name="Venue 1")
|
||||
|
||||
cls.events = {
|
||||
1: models.Event.objects.create(name="TE E1", start_date=date.today(),
|
||||
description="This is an event description\nthat for a very specific reason spans two lines.",
|
||||
venue=cls.venue),
|
||||
2: models.Event.objects.create(name="TE E2", start_date=date.today()),
|
||||
}
|
||||
def setup_for_hs():
|
||||
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
venue = models.Venue.objects.create(name="Venue 1")
|
||||
return venue, {
|
||||
1: models.Event.objects.create(name="TE E1", start_date=date.today(),
|
||||
description="This is an event description\nthat for a very specific reason spans two lines.",
|
||||
venue=venue),
|
||||
2: models.Event.objects.create(name="TE E2", start_date=date.today()),
|
||||
}
|
||||
|
||||
cls.ras = {
|
||||
1: models.RiskAssessment.objects.create(event=cls.events[1],
|
||||
nonstandard_equipment=False,
|
||||
nonstandard_use=False,
|
||||
contractors=False,
|
||||
other_companies=False,
|
||||
crew_fatigue=False,
|
||||
big_power=False,
|
||||
power_mic=cls.profile,
|
||||
generators=False,
|
||||
other_companies_power=False,
|
||||
nonstandard_equipment_power=False,
|
||||
multiple_electrical_environments=False,
|
||||
noise_monitoring=False,
|
||||
known_venue=True,
|
||||
safe_loading=True,
|
||||
safe_storage=True,
|
||||
area_outside_of_control=True,
|
||||
barrier_required=True,
|
||||
nonstandard_emergency_procedure=True,
|
||||
special_structures=False,
|
||||
suspended_structures=False,
|
||||
outside=False),
|
||||
}
|
||||
|
||||
cls.checklists = {
|
||||
1: models.EventChecklist.objects.create(event=cls.events[1],
|
||||
power_mic=cls.profile,
|
||||
safe_parking=False,
|
||||
safe_packing=False,
|
||||
exits=False,
|
||||
trip_hazard=False,
|
||||
warning_signs=False,
|
||||
ear_plugs=False,
|
||||
hs_location="Locked away safely",
|
||||
extinguishers_location="Somewhere, I forgot",
|
||||
earthing=False,
|
||||
pat=False,
|
||||
date=timezone.now(),
|
||||
venue=cls.venue),
|
||||
}
|
||||
def create_ra(usr):
|
||||
venue, events = setup_for_hs()
|
||||
return models.RiskAssessment.objects.create(event=events[1], nonstandard_equipment=False, nonstandard_use=False,
|
||||
contractors=False, other_companies=False, crew_fatigue=False,
|
||||
big_power=False, power_mic=usr, generators=False,
|
||||
other_companies_power=False, nonstandard_equipment_power=False,
|
||||
multiple_electrical_environments=False, noise_monitoring=False,
|
||||
known_venue=True, safe_loading=True, safe_storage=True,
|
||||
area_outside_of_control=True, barrier_required=True,
|
||||
nonstandard_emergency_procedure=True, special_structures=False,
|
||||
suspended_structures=False, outside=False)
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test_list(self):
|
||||
request_url = reverse('hs_list')
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, self.events[1].name)
|
||||
self.assertContains(response, self.events[2].name)
|
||||
self.assertContains(response, 'Create')
|
||||
def create_checklist(usr):
|
||||
venue, events = setup_for_hs()
|
||||
return models.EventChecklist.objects.create(event=events[1], power_mic=usr, safe_parking=False,
|
||||
safe_packing=False, exits=False, trip_hazard=False, warning_signs=False,
|
||||
ear_plugs=False, hs_location="Locked away safely",
|
||||
extinguishers_location="Somewhere, I forgot", earthing=False, pat=False,
|
||||
date=timezone.now(), venue=venue)
|
||||
|
||||
def test_ra_review(self):
|
||||
request_url = reverse('ra_review', kwargs={'pk': self.ras[1].pk})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, 'Reviewed by')
|
||||
self.assertContains(response, self.profile.name)
|
||||
ra = models.RiskAssessment.objects.get(event=self.events[1])
|
||||
self.assertEqual(timezone.now().date(), ra.reviewed_at.date())
|
||||
self.assertEqual(timezone.now().hour, ra.reviewed_at.hour)
|
||||
self.assertEqual(timezone.now().minute, ra.reviewed_at.minute)
|
||||
|
||||
def test_checklist_review(self):
|
||||
request_url = reverse('ec_review', kwargs={'pk': self.checklists[1].pk})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertContains(response, 'Reviewed by')
|
||||
self.assertContains(response, self.profile.name)
|
||||
checklist = models.EventChecklist.objects.get(event=self.events[1])
|
||||
self.assertEqual(timezone.now().date(), checklist.reviewed_at.date())
|
||||
self.assertEqual(timezone.now().hour, checklist.reviewed_at.hour)
|
||||
self.assertEqual(timezone.now().minute, checklist.reviewed_at.minute)
|
||||
def test_list(admin_client):
|
||||
venue, events = setup_for_hs()
|
||||
request_url = reverse('hs_list')
|
||||
response = admin_client.get(request_url, follow=True)
|
||||
assertContains(response, events[1].name)
|
||||
assertContains(response, events[2].name)
|
||||
assertContains(response, 'Create')
|
||||
|
||||
def test_ra_redirect(self):
|
||||
request_url = reverse('event_ra', kwargs={'pk': self.events[1].pk})
|
||||
expected_url = reverse('ra_edit', kwargs={'pk': self.ras[1].pk})
|
||||
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
||||
def review(client, profile, obj, request_url):
|
||||
time = timezone.now()
|
||||
response = client.get(reverse(request_url, kwargs={'pk': obj.pk}), follow=True)
|
||||
obj.refresh_from_db()
|
||||
assertContains(response, 'Reviewed by')
|
||||
assertContains(response, profile.name)
|
||||
assert_times_equal(time, obj.reviewed_at)
|
||||
|
||||
|
||||
def test_ra_review(admin_client, admin_user):
|
||||
review(admin_client, admin_user, create_ra(admin_user), 'ra_review')
|
||||
|
||||
|
||||
def test_checklist_review(admin_client, admin_user):
|
||||
review(admin_client, admin_user, create_checklist(admin_user), 'ec_review')
|
||||
|
||||
|
||||
def test_ra_redirect(admin_client, admin_user):
|
||||
ra = create_ra(admin_user)
|
||||
request_url = reverse('event_ra', kwargs={'pk': ra.event.pk})
|
||||
expected_url = reverse('ra_edit', kwargs={'pk': ra.pk})
|
||||
|
||||
response = admin_client.get(request_url, follow=True)
|
||||
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
||||
|
||||
23
RIGS/urls.py
23
RIGS/urls.py
@@ -1,11 +1,12 @@
|
||||
from django.conf.urls import url
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import path
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import path, re_path
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from PyRIGS.decorators import (api_key_required, has_oembed,
|
||||
permission_required_with_403)
|
||||
from RIGS import finance, ical, models, rigboard, views, hs
|
||||
from RIGS import finance, ical, rigboard, views, hs
|
||||
|
||||
urlpatterns = [
|
||||
# People
|
||||
@@ -45,10 +46,10 @@ urlpatterns = [
|
||||
path('rigboard/', login_required(rigboard.RigboardIndex.as_view()), name='rigboard'),
|
||||
path('rigboard/calendar/', login_required()(rigboard.WebCalendar.as_view()),
|
||||
name='web_calendar'),
|
||||
url(r'^rigboard/calendar/(?P<view>(month|week|day))/$',
|
||||
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
||||
url(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$',
|
||||
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
||||
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/$',
|
||||
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
||||
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$',
|
||||
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
||||
path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
||||
|
||||
|
||||
@@ -130,12 +131,12 @@ urlpatterns = [
|
||||
path('event/<int:pk>/auth/preview/',
|
||||
permission_required_with_403('RIGS.change_event')(rigboard.EventAuthoriseRequestEmailPreview.as_view()),
|
||||
name='event_authorise_preview'),
|
||||
url(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', rigboard.EventAuthorise.as_view(),
|
||||
name='event_authorise'),
|
||||
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', rigboard.EventAuthorise.as_view(),
|
||||
name='event_authorise'),
|
||||
|
||||
# ICS Calendar - API key authentication
|
||||
url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),
|
||||
name="ics_calendar"),
|
||||
re_path(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),
|
||||
name="ics_calendar"),
|
||||
|
||||
|
||||
# Legacy URLs
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
||||
from django.views import generic
|
||||
from django.contrib.auth.views import LoginView
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.core import serializers
|
||||
from django.conf import settings
|
||||
import simplejson
|
||||
import datetime
|
||||
import pytz
|
||||
import operator
|
||||
from registration.views import RegistrationView
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from RIGS import models, forms
|
||||
from assets import models as asset_models
|
||||
from functools import reduce
|
||||
|
||||
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin
|
||||
from RIGS import models
|
||||
|
||||
|
||||
class PersonList(GenericListView):
|
||||
|
||||
134
Vagrantfile
vendored
134
Vagrantfile
vendored
@@ -1,134 +0,0 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
require 'yaml'
|
||||
|
||||
unless File.exist?('config/vagrant.yml')
|
||||
raise "There is no config/vagrant.yml file.\nCopy config/vagrant.template.yml, make any changes you need, then try again."
|
||||
end
|
||||
|
||||
settings = YAML.load_file 'config/vagrant.yml'
|
||||
|
||||
$script = <<SCRIPT
|
||||
echo Beginning Vagrant provisioning...
|
||||
date > /etc/vagrant_provisioned_at
|
||||
SCRIPT
|
||||
|
||||
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
|
||||
VAGRANTFILE_API_VERSION = '2'
|
||||
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
# The most common configuration options are documented and commented below.
|
||||
# For a complete reference, please see the online documentation at
|
||||
# https://docs.vagrantup.com.
|
||||
|
||||
config.vm.provision 'shell', inline: $script
|
||||
|
||||
# Every Vagrant development environment requires a box. You can search for
|
||||
# boxes at https://atlas.hashicorp.com/search.
|
||||
config.vm.box = 'ubuntu/trusty64'
|
||||
|
||||
# Share an additional folder to the guest VM. The first argument is
|
||||
# the path on the host to the actual folder. The second argument is
|
||||
# the path on the guest to mount the folder. And the optional third
|
||||
# argument is a set of non-required options.
|
||||
config.vm.synced_folder ".", "/vagrant"
|
||||
|
||||
# Create a forwarded port mapping which allows access to a specific port
|
||||
# within the machine from a port on the host machine. In the example below,
|
||||
# accessing "localhost:5000" will access port 5000 on the guest machine.
|
||||
config.vm.network "forwarded_port", guest: 5000, host: 5000
|
||||
|
||||
# PostgreSQL Server port forwarding
|
||||
config.vm.network "forwarded_port", host: 15432, guest: 5432
|
||||
|
||||
# You can provision with just one of these scripts by user its name, eg:
|
||||
# $ vagrant provision --provision-with postgresql
|
||||
|
||||
config.vm.provision 'build',
|
||||
type: 'shell',
|
||||
path: 'config/vagrant/build_dependency_setup.sh'
|
||||
|
||||
config.vm.provision 'git',
|
||||
type: 'shell',
|
||||
path: 'config/vagrant/git_setup.sh'
|
||||
|
||||
config.vm.provision 'postgresql',
|
||||
type: 'shell',
|
||||
path: 'config/vagrant/postgresql_setup.sh',
|
||||
args: [
|
||||
settings['db']['name'],
|
||||
settings['db']['user'],
|
||||
settings['db']['password'],
|
||||
]
|
||||
|
||||
config.vm.provision 'python',
|
||||
type: 'shell',
|
||||
path: 'config/vagrant/python_setup.sh'
|
||||
|
||||
config.vm.provision 'virtualenv',
|
||||
type: 'shell',
|
||||
path: 'config/vagrant/virtualenv_setup.sh',
|
||||
args: [
|
||||
settings['virtualenv']['envname'],
|
||||
]
|
||||
|
||||
# Will install foreman and, if there's a Procfile, start it:
|
||||
config.vm.provision 'foreman',
|
||||
type: 'shell',
|
||||
path: 'config/vagrant/foreman_setup.sh',
|
||||
run: 'always',
|
||||
args: [
|
||||
settings['virtualenv']['envname'],
|
||||
settings['django']['settings_module'],
|
||||
settings['foreman']['procfile'],
|
||||
]
|
||||
|
||||
# Disable automatic box update checking. If you disable this, then
|
||||
# boxes will only be checked for updates when the user runs
|
||||
# `vagrant box outdated`. This is not recommended.
|
||||
# config.vm.box_check_update = false
|
||||
|
||||
# Create a forwarded port mapping which allows access to a specific port
|
||||
# within the machine from a port on the host machine. In the example below,
|
||||
# accessing "localhost:8080" will access port 80 on the guest machine.
|
||||
# config.vm.network "forwarded_port", guest: 80, host: 8080
|
||||
|
||||
# Create a private network, which allows host-only access to the machine
|
||||
# using a specific IP.
|
||||
# config.vm.network "private_network", ip: "192.168.33.10"
|
||||
|
||||
# Create a public network, which generally matched to bridged network.
|
||||
# Bridged networks make the machine appear as another physical device on
|
||||
# your network.
|
||||
# config.vm.network "public_network"
|
||||
|
||||
# Provider-specific configuration so you can fine-tune various
|
||||
# backing providers for Vagrant. These expose provider-specific options.
|
||||
# Example for VirtualBox:
|
||||
#
|
||||
config.vm.provider "virtualbox" do |vb|
|
||||
# # Display the VirtualBox GUI when booting the machine
|
||||
# vb.gui = true
|
||||
#
|
||||
# # Customize the amount of memory on the VM:
|
||||
vb.memory = "1024"
|
||||
end
|
||||
#
|
||||
# View the documentation for the provider you are using for more
|
||||
# information on available options.
|
||||
|
||||
# Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
|
||||
# such as FTP and Heroku are also available. See the documentation at
|
||||
# https://docs.vagrantup.com/v2/push/atlas.html for more information.
|
||||
# config.push.define "atlas" do |push|
|
||||
# push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
|
||||
# end
|
||||
|
||||
# Enable provisioning with a shell script. Additional provisioners such as
|
||||
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
|
||||
# documentation for more information about their specific syntax and use.
|
||||
# config.vm.provision "shell", inline: <<-SHELL
|
||||
# sudo apt-get update
|
||||
# sudo apt-get install -y apache2
|
||||
# SHELL
|
||||
end
|
||||
10
app.json
10
app.json
@@ -2,9 +2,10 @@
|
||||
"name": "PyRIGS",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"postdeploy": "python manage.py migrate && python manage.py generateSampleData"
|
||||
"postdeploy": "python manage.py migrate && python manage.py generateSampleData",
|
||||
"heroku-cleanup": "rm -rf node_modules pipeline"
|
||||
},
|
||||
"stack": "heroku-18",
|
||||
"stack": "heroku-20",
|
||||
"env": {
|
||||
"DEBUG": {
|
||||
"required": true
|
||||
@@ -47,8 +48,11 @@
|
||||
"heroku-postgresql"
|
||||
],
|
||||
"buildpacks": [
|
||||
{
|
||||
"url": "heroku/nodejs"
|
||||
},
|
||||
{
|
||||
"url": "heroku/python"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from assets import models as assets
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
|
||||
from assets import models
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
class AssetForm(forms.ModelForm):
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import random
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils import timezone
|
||||
from reversion import revisions as reversion
|
||||
from assets import models
|
||||
|
||||
from RIGS import models as rigsmodels
|
||||
from assets import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, connection
|
||||
from django.urls import reverse
|
||||
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch.dispatcher import receiver
|
||||
|
||||
from django.urls import reverse
|
||||
from reversion import revisions as reversion
|
||||
from reversion.models import Version
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{% extends request.is_ajax|yesno:'base_ajax.html,base_assets.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% block title %}Audit Asset {{ object.asset_id }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<script>
|
||||
@@ -15,11 +14,7 @@
|
||||
$('#' + String(ID)).val(CSA);
|
||||
}
|
||||
function checkIfCableHidden() {
|
||||
if (document.getElementById("id_is_cable").checked) {
|
||||
document.getElementById("cable-table").hidden = false;
|
||||
} else {
|
||||
document.getElementById("cable-table").hidden = true;
|
||||
}
|
||||
document.getElementById("cable-table").hidden = !document.getElementById("id_is_cable").checked;
|
||||
}
|
||||
checkIfCableHidden();
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block js %}
|
||||
<script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
|
||||
<script src="{% static 'js/jquery-ui.js' %}"></script>
|
||||
<script src="{% static "js/interaction.js" %}"></script>
|
||||
<script src="{% static "js/modal.js" %}"></script>
|
||||
<script>
|
||||
@@ -23,7 +23,7 @@
|
||||
success: function(){
|
||||
$link = $(this);
|
||||
// Anti modal inception
|
||||
if ($link.parents('#modal').length == 0) {
|
||||
if ($link.parents('#modal').length === 0) {
|
||||
modaltarget = $link.data('target');
|
||||
modalobject = "";
|
||||
$('#modal').load(url, function (e) {
|
||||
|
||||
@@ -30,19 +30,19 @@
|
||||
//Get query string value
|
||||
var searchUrl=location.search;
|
||||
|
||||
if(key!="") {
|
||||
if(key!=="") {
|
||||
oldValue = getParameterByName(key);
|
||||
removeVal=key+"="+oldValue;
|
||||
if(searchUrl.indexOf('?'+removeVal+'&')!= "-1") {
|
||||
if(searchUrl.indexOf('?'+removeVal+'&')!== "-1") {
|
||||
urlValue=urlValue.replace('?'+removeVal+'&','?');
|
||||
}
|
||||
else if(searchUrl.indexOf('&'+removeVal+'&')!= "-1") {
|
||||
else if(searchUrl.indexOf('&'+removeVal+'&')!== "-1") {
|
||||
urlValue=urlValue.replace('&'+removeVal+'&','&');
|
||||
}
|
||||
else if(searchUrl.indexOf('?'+removeVal)!= "-1") {
|
||||
else if(searchUrl.indexOf('?'+removeVal)!== "-1") {
|
||||
urlValue=urlValue.replace('?'+removeVal,'');
|
||||
}
|
||||
else if(searchUrl.indexOf('&'+removeVal)!= "-1") {
|
||||
else if(searchUrl.indexOf('&'+removeVal)!== "-1") {
|
||||
urlValue=urlValue.replace('&'+removeVal,'');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Collection of page object models for use within tests.
|
||||
from pypom import Page, Region
|
||||
from django.urls import reverse
|
||||
from pypom import Region
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions
|
||||
from selenium.webdriver import Chrome
|
||||
from django.urls import reverse
|
||||
|
||||
from PyRIGS.tests import regions
|
||||
from PyRIGS.tests.pages import BasePage, FormPage
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from PyRIGS.tests.pages import BasePage, FormPage, animation_is_finished
|
||||
|
||||
|
||||
class AssetList(BasePage):
|
||||
@@ -190,13 +190,14 @@ class AssetAuditList(AssetList):
|
||||
def query(self):
|
||||
return self.find_element(*self._search_text_locator).text
|
||||
|
||||
def set_query(self, queryString):
|
||||
def set_query(self, query):
|
||||
element = self.find_element(*self._search_text_locator)
|
||||
element.clear()
|
||||
element.send_keys(queryString)
|
||||
element.send_keys(query)
|
||||
|
||||
def search(self):
|
||||
self.find_element(*self._go_button_locator).click()
|
||||
self.wait.until(animation_is_finished())
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
@@ -205,10 +206,12 @@ class AssetAuditList(AssetList):
|
||||
except NoSuchElementException:
|
||||
return None
|
||||
|
||||
class AssetAuditModal(Region):
|
||||
class AssetAuditModal(regions.Modal):
|
||||
_errors_selector = (By.CLASS_NAME, "alert-danger")
|
||||
# Don't use the usual success selector - that tries and fails to hit the '10m long cable' helper button...
|
||||
_submit_locator = (By.ID, "id_mark_audited")
|
||||
_close_selector = (By.XPATH, "//button[@data-dismiss='modal']")
|
||||
|
||||
form_items = {
|
||||
'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')),
|
||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||
@@ -235,27 +238,10 @@ class AssetAuditList(AssetList):
|
||||
except NoSuchElementException:
|
||||
return None
|
||||
|
||||
def submit(self):
|
||||
previous_errors = self.errors
|
||||
self.root.find_element(*self._submit_locator).click()
|
||||
# self.wait.until(lambda x: not self.is_displayed) TODO
|
||||
def close(self):
|
||||
self.page.find_element(*self._close_selector).click()
|
||||
self.wait.until(expected_conditions.invisibility_of_element_located((By.ID, 'modal')))
|
||||
|
||||
def remove_all_required(self):
|
||||
self.driver.execute_script("Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
||||
self.driver.execute_script("Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self.form_items:
|
||||
element = self.form_items[name]
|
||||
form_element = element[0](self, self.find_element(*element[1]))
|
||||
return form_element.value
|
||||
else:
|
||||
return super().__getattribute__(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in self.form_items:
|
||||
element = self.form_items[name]
|
||||
form_element = element[0](self, self.find_element(*element[1]))
|
||||
form_element.set_value(value)
|
||||
else:
|
||||
self.__dict__[name] = value
|
||||
|
||||
@@ -1,622 +0,0 @@
|
||||
from . import pages
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from assets import models
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from urllib.parse import urlparse
|
||||
from RIGS import models as rigsmodels
|
||||
from PyRIGS.tests.base import BaseTest, AutoLoginTest, screenshot_failure_cls
|
||||
from assets import models, urls
|
||||
from reversion import revisions as reversion
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from PyRIGS.tests.base import animation_is_finished
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from django.test import tag
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestAssetList(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
sound = models.AssetCategory.objects.create(name="Sound")
|
||||
lighting = models.AssetCategory.objects.create(name="Lighting")
|
||||
|
||||
working = models.AssetStatus.objects.create(name="Working", should_show=True)
|
||||
broken = models.AssetStatus.objects.create(name="Broken", should_show=False)
|
||||
|
||||
models.Asset.objects.create(asset_id="1", description="Broken XLR", status=broken, category=sound, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="10", description="Working Mic", status=working, category=sound, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="2", description="A light", status=working, category=lighting, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="C1", description="The pearl", status=broken, category=lighting, date_acquired=datetime.date(2020, 2, 1))
|
||||
self.page = pages.AssetList(self.driver, self.live_server_url).open()
|
||||
|
||||
def test_default_statuses_applied(self):
|
||||
# Only the working stuff should be shown initially
|
||||
assetDescriptions = list(map(lambda x: x.description, self.page.assets))
|
||||
self.assertEqual(2, len(assetDescriptions))
|
||||
self.assertIn("A light", assetDescriptions)
|
||||
self.assertIn("Working Mic", assetDescriptions)
|
||||
|
||||
def test_asset_order(self):
|
||||
# Only the working stuff should be shown initially
|
||||
self.page.status_selector.open()
|
||||
self.page.status_selector.set_option("Broken", True)
|
||||
self.page.status_selector.close()
|
||||
|
||||
self.page.search()
|
||||
|
||||
assetIDs = list(map(lambda x: x.id, self.page.assets))
|
||||
self.assertEqual("1", assetIDs[0])
|
||||
self.assertEqual("2", assetIDs[1])
|
||||
self.assertEqual("10", assetIDs[2])
|
||||
self.assertEqual("C1", assetIDs[3])
|
||||
|
||||
def test_search(self):
|
||||
self.page.set_query("10")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 1)
|
||||
self.assertEqual("Working Mic", self.page.assets[0].description)
|
||||
self.assertEqual("10", self.page.assets[0].id)
|
||||
|
||||
self.page.set_query("light")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 1)
|
||||
self.assertEqual("A light", self.page.assets[0].description)
|
||||
|
||||
self.page.set_query("Random string")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 0)
|
||||
|
||||
self.page.set_query("")
|
||||
self.page.search()
|
||||
# Only working stuff shown by default
|
||||
self.assertTrue(len(self.page.assets) == 2)
|
||||
|
||||
self.page.status_selector.toggle()
|
||||
self.assertTrue(self.page.status_selector.is_open)
|
||||
self.page.status_selector.select_all()
|
||||
self.page.status_selector.toggle()
|
||||
self.assertFalse(self.page.status_selector.is_open)
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 4)
|
||||
|
||||
self.page.category_selector.toggle()
|
||||
self.assertTrue(self.page.category_selector.is_open)
|
||||
self.page.category_selector.set_option("Sound", True)
|
||||
self.page.category_selector.close()
|
||||
self.assertFalse(self.page.category_selector.is_open)
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 2)
|
||||
assetIDs = list(map(lambda x: x.id, self.page.assets))
|
||||
self.assertEqual("1", assetIDs[0])
|
||||
self.assertEqual("10", assetIDs[1])
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestAssetForm(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = models.AssetCategory.objects.create(name="Health & Safety")
|
||||
self.status = models.AssetStatus.objects.create(name="O.K.", should_show=True)
|
||||
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
self.parent = models.Asset.objects.create(asset_id="9000", description="Shelf", status=self.status, category=self.category, date_acquired=datetime.date(2000, 1, 1))
|
||||
self.connector = models.Connector.objects.create(description="IEC", current_rating=10, voltage_rating=240, num_pins=3)
|
||||
self.cable_type = models.CableType.objects.create(plug=self.connector, socket=self.connector, circuits=1, cores=3)
|
||||
self.wait = WebDriverWait(self.driver, 5)
|
||||
self.page = pages.AssetCreate(self.driver, self.live_server_url).open()
|
||||
|
||||
def test_asset_create(self):
|
||||
# Test that ID is automatically assigned and properly incremented
|
||||
self.assertIn(self.page.asset_id, "9001")
|
||||
|
||||
self.page.remove_all_required()
|
||||
self.page.asset_id = "XX$X"
|
||||
self.page.submit()
|
||||
self.assertFalse(self.page.success)
|
||||
self.assertIn("An Asset ID can only consist of letters and numbers, with a final number", self.page.errors["Asset id"])
|
||||
self.assertIn("This field is required.", self.page.errors["Description"])
|
||||
|
||||
self.page.open()
|
||||
|
||||
self.page.description = desc = "Bodge Lead"
|
||||
self.page.category = cat = "Health & Safety"
|
||||
self.page.status = status = "O.K."
|
||||
self.page.serial_number = sn = "0124567890-SAUSAGE"
|
||||
self.page.comments = comments = "This is actually a sledgehammer, not a cable..."
|
||||
|
||||
self.page.purchased_from_selector.toggle()
|
||||
self.assertTrue(self.page.purchased_from_selector.is_open)
|
||||
self.page.purchased_from_selector.search(self.supplier.name[:-8])
|
||||
self.page.purchased_from_selector.set_option(self.supplier.name, True)
|
||||
self.page.purchase_price = "12.99"
|
||||
self.page.salvage_value = "99.12"
|
||||
self.page.date_acquired = acquired = datetime.date(2020, 5, 2)
|
||||
|
||||
self.page.parent_selector.toggle()
|
||||
self.assertTrue(self.page.parent_selector.is_open)
|
||||
self.page.parent_selector.search(self.parent.asset_id)
|
||||
# Needed here but not earlier for whatever reason
|
||||
self.driver.implicitly_wait(1)
|
||||
self.page.parent_selector.set_option(self.parent.asset_id + " | " + self.parent.description, True)
|
||||
self.assertTrue(self.page.parent_selector.options[0].selected)
|
||||
self.page.parent_selector.toggle()
|
||||
|
||||
self.assertFalse(self.driver.find_element_by_id('cable-table').is_displayed())
|
||||
|
||||
self.page.submit()
|
||||
self.wait.until(animation_is_finished())
|
||||
self.assertTrue(self.page.success)
|
||||
# Check that data is right
|
||||
asset = models.Asset.objects.get(asset_id="9001")
|
||||
self.assertEqual(asset.description, desc)
|
||||
self.assertEqual(asset.category.name, cat)
|
||||
self.assertEqual(asset.status.name, status)
|
||||
self.assertEqual(asset.serial_number, sn)
|
||||
self.assertEqual(asset.comments, comments)
|
||||
# This one is important as it defaults to today's date
|
||||
self.assertEqual(asset.date_acquired, acquired)
|
||||
|
||||
def test_cable_create(self):
|
||||
self.page.description = "IEC -> IEC"
|
||||
self.page.category = "Health & Safety"
|
||||
self.page.status = "O.K."
|
||||
self.page.serial_number = "MELON-MELON-MELON"
|
||||
self.page.comments = "You might need that"
|
||||
self.page.is_cable = True
|
||||
|
||||
self.assertTrue(self.driver.find_element_by_id('cable-table').is_displayed())
|
||||
self.wait.until(animation_is_finished())
|
||||
self.page.cable_type = "IEC → IEC"
|
||||
self.page.socket = "IEC"
|
||||
self.page.length = 10
|
||||
self.page.csa = "1.5"
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
def test_asset_edit(self):
|
||||
self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
|
||||
|
||||
self.assertTrue(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly') is not None)
|
||||
|
||||
new_description = "Big Shelf"
|
||||
self.page.description = new_description
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
self.assertEqual(models.Asset.objects.get(asset_id=self.parent.asset_id).description, new_description)
|
||||
|
||||
def test_asset_duplicate(self):
|
||||
self.page = pages.AssetDuplicate(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
|
||||
|
||||
self.assertNotEqual(self.parent.asset_id, self.page.asset_id)
|
||||
self.assertEqual(self.parent.description, self.page.description)
|
||||
self.assertEqual(self.parent.status.name, self.page.status)
|
||||
self.assertEqual(self.parent.category.name, self.page.category)
|
||||
self.assertEqual(self.parent.date_acquired, self.page.date_acquired.date())
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
self.assertEqual(models.Asset.objects.last().description, self.parent.description)
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestSupplierList(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
models.Supplier.objects.create(name="Acme.")
|
||||
models.Supplier.objects.create(name="TEC PA & Lighting")
|
||||
models.Supplier.objects.create(name="Caterpillar Inc.")
|
||||
models.Supplier.objects.create(name="N.E.R.D")
|
||||
models.Supplier.objects.create(name="Khumalo")
|
||||
models.Supplier.objects.create(name="1984 Incorporated")
|
||||
self.page = pages.SupplierList(self.driver, self.live_server_url).open()
|
||||
|
||||
# Should be sorted alphabetically
|
||||
def test_order(self):
|
||||
names = list(map(lambda x: x.name, self.page.suppliers))
|
||||
self.assertEqual("1984 Incorporated", names[0])
|
||||
self.assertEqual("Acme.", names[1])
|
||||
self.assertEqual("Caterpillar Inc.", names[2])
|
||||
self.assertEqual("Fullmetal Heavy Industry", names[3])
|
||||
self.assertEqual("Khumalo", names[4])
|
||||
self.assertEqual("N.E.R.D", names[5])
|
||||
self.assertEqual("TEC PA & Lighting", names[6])
|
||||
|
||||
def test_search(self):
|
||||
self.page.set_query("TEC")
|
||||
self.page.search()
|
||||
|
||||
self.assertTrue(len(self.page.suppliers) == 1)
|
||||
self.assertEqual("TEC PA & Lighting", self.page.suppliers[0].name)
|
||||
|
||||
self.page.set_query("")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.suppliers) == 7)
|
||||
|
||||
self.page.set_query("This is not a supplier")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.suppliers) == 0)
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestSupplierCreateAndEdit(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
|
||||
def test_supplier_create(self):
|
||||
self.page = pages.SupplierCreate(self.driver, self.live_server_url).open()
|
||||
|
||||
self.page.remove_all_required()
|
||||
self.page.submit()
|
||||
self.assertFalse(self.page.success)
|
||||
self.assertIn("This field is required.", self.page.errors["Name"])
|
||||
|
||||
self.page.name = "Optican Health Supplies"
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
def test_supplier_edit(self):
|
||||
self.page = pages.SupplierEdit(self.driver, self.live_server_url, supplier_id=self.supplier.pk).open()
|
||||
|
||||
self.assertEqual("Fullmetal Heavy Industry", self.page.name)
|
||||
new_name = "Cyberdyne Systems"
|
||||
self.page.name = new_name
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestAssetAudit(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = models.AssetCategory.objects.create(name="Haulage")
|
||||
self.status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True)
|
||||
self.supplier = models.Supplier.objects.create(name="The Bazaar")
|
||||
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1, voltage_rating=40, num_pins=13)
|
||||
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
||||
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
|
||||
self.wait = WebDriverWait(self.driver, 5)
|
||||
|
||||
def test_audit_process(self):
|
||||
asset_id = "1111"
|
||||
self.page.set_query(asset_id)
|
||||
self.page.search()
|
||||
mdl = self.page.modal
|
||||
self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
|
||||
# Do it wrong on purpose to check error display
|
||||
mdl.remove_all_required()
|
||||
mdl.description = ""
|
||||
mdl.submit()
|
||||
# self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
|
||||
self.wait.until(animation_is_finished())
|
||||
# self.assertTrue(self.driver.find_element_by_id('modal').is_displayed())
|
||||
self.assertIn("This field is required.", mdl.errors["Description"])
|
||||
# Now do it properly
|
||||
new_desc = "A BIG hammer"
|
||||
mdl.description = new_desc
|
||||
mdl.submit()
|
||||
self.wait.until(EC.invisibility_of_element_located((By.ID, 'modal')))
|
||||
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
|
||||
|
||||
# Check data is correct
|
||||
audited = models.Asset.objects.get(asset_id="1111")
|
||||
self.assertEqual(audited.description, new_desc)
|
||||
# Make sure audit 'log' was filled out
|
||||
self.assertEqual(self.profile.initials, audited.last_audited_by.initials)
|
||||
self.assertEqual(timezone.now().date(), audited.last_audited_at.date())
|
||||
self.assertEqual(timezone.now().hour, audited.last_audited_at.hour)
|
||||
self.assertEqual(timezone.now().minute, audited.last_audited_at.minute)
|
||||
# Check we've removed it from the 'needing audit' list
|
||||
self.assertNotIn(asset_id, self.page.assets)
|
||||
|
||||
def test_audit_list(self):
|
||||
self.assertEqual(len(models.Asset.objects.filter(last_audited_at=None)), len(self.page.assets))
|
||||
|
||||
asset_row = self.page.assets[0]
|
||||
self.driver.find_element(By.XPATH, "//*[@id='asset_table_body']/tr[1]/td[4]/a").click()
|
||||
self.wait.until(EC.visibility_of_element_located((By.ID, 'modal')))
|
||||
self.assertEqual(self.page.modal.asset_id, asset_row.id)
|
||||
|
||||
# First close button is for the not found error
|
||||
self.page.find_element(By.XPATH, '//*[@id="modal"]/div/div/div[1]/button').click()
|
||||
self.wait.until(EC.invisibility_of_element_located((By.ID, 'modal')))
|
||||
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
|
||||
# Make sure audit log was NOT filled out
|
||||
audited = models.Asset.objects.get(asset_id=asset_row.id)
|
||||
self.assertEqual(None, audited.last_audited_by)
|
||||
|
||||
# Check that a failed search works
|
||||
self.page.set_query("NOTFOUND")
|
||||
self.page.search()
|
||||
self.wait.until(animation_is_finished())
|
||||
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
|
||||
self.assertIn("Asset with that ID does not exist!", self.page.error.text)
|
||||
|
||||
|
||||
class TestSupplierValidation(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="SupplierValidationTest", email="SVT@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
cls.supplier = models.Supplier.objects.create(name="Gadgetron Corporation")
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test_create(self):
|
||||
url = reverse('supplier_create')
|
||||
response = self.client.post(url)
|
||||
self.assertFormError(response, 'form', 'name', 'This field is required.')
|
||||
|
||||
def test_edit(self):
|
||||
url = reverse('supplier_update', kwargs={'pk': self.supplier.pk})
|
||||
response = self.client.post(url, {'name': ""})
|
||||
self.assertFormError(response, 'form', 'name', 'This field is required.')
|
||||
|
||||
|
||||
class Test404(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="404Test", email="404@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test(self):
|
||||
urls = {'asset_detail', 'asset_update', 'asset_duplicate', 'supplier_detail', 'supplier_update'}
|
||||
for url_name in urls:
|
||||
request_url = reverse(url_name, kwargs={'pk': "0000"})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
class TestAccessLevels(TestCase):
|
||||
@override_settings(DEBUG=True)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Shortcut to create the levels - bonus side effect of testing the command (hopefully) matches production
|
||||
call_command('generateSampleData')
|
||||
|
||||
# Nothing should be available to the unauthenticated
|
||||
def test_unauthenticated(self):
|
||||
for url in urls.urlpatterns:
|
||||
if url.name is not None:
|
||||
pattern = str(url.pattern)
|
||||
if "json" in url.name or pattern:
|
||||
# TODO
|
||||
pass
|
||||
elif ":pk>" in pattern:
|
||||
request_url = reverse(url.name, kwargs={'pk': 9})
|
||||
else:
|
||||
request_url = reverse(url.name)
|
||||
response = self.client.get(request_url, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = self.client.get(request_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'login')
|
||||
|
||||
def test_basic_access(self):
|
||||
self.assertTrue(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
|
||||
self.assertNotContains(response, 'Edit')
|
||||
self.assertNotContains(response, 'Duplicate')
|
||||
|
||||
url = reverse('asset_detail', kwargs={'pk': "9000"})
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, 'Purchase Details')
|
||||
self.assertNotContains(response, 'View Revision History')
|
||||
|
||||
urls = {'asset_history', 'asset_update', 'asset_duplicate'}
|
||||
for url_name in urls:
|
||||
request_url = reverse(url_name, kwargs={'pk': "9000"})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
request_url = reverse('supplier_create')
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
request_url = reverse('supplier_update', kwargs={'pk': "1"})
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_keyholder_access(self):
|
||||
self.assertTrue(self.client.login(username="keyholder", password="keyholder"))
|
||||
|
||||
url = reverse('asset_list')
|
||||
response = self.client.get(url)
|
||||
# Check edit and duplicate buttons shown in list
|
||||
self.assertContains(response, 'Edit')
|
||||
self.assertContains(response, 'Duplicate')
|
||||
|
||||
url = reverse('asset_detail', kwargs={'pk': "9000"})
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, 'Purchase Details')
|
||||
self.assertContains(response, 'View Revision History')
|
||||
|
||||
# def test_finance_access(self): Level not used in assets currently
|
||||
|
||||
|
||||
class TestFormValidation(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="AssetCreateValidationTest", email="acvt@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
cls.category = models.AssetCategory.objects.create(name="Sound")
|
||||
cls.status = models.AssetStatus.objects.create(name="Broken", should_show=True)
|
||||
cls.asset = models.Asset.objects.create(asset_id="9999", description="The Office", status=cls.status, category=cls.category, date_acquired=datetime.date(2018, 6, 15))
|
||||
cls.connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
|
||||
cls.cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=cls.connector, socket=cls.connector)
|
||||
cls.cable_asset = models.Asset.objects.create(asset_id="666", description="125A -> Jack", comments="The cable from Hell...", status=cls.status, category=cls.category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cls.cable_type, length=10, csa="1.5")
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
def test_asset_create(self):
|
||||
url = reverse('asset_create')
|
||||
response = self.client.post(url, {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'})
|
||||
self.assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'description', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'status', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
|
||||
self.assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||
self.assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||
self.assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
|
||||
|
||||
def test_cable_create(self):
|
||||
url = reverse('asset_create')
|
||||
response = self.client.post(url, {'asset_id': 'X$%A', 'is_cable': True})
|
||||
self.assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number')
|
||||
|
||||
self.assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
|
||||
self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
|
||||
# Given that validation is done at model level it *shouldn't* need retesting...gonna do it anyway!
|
||||
def test_asset_edit(self):
|
||||
url = reverse('asset_update', kwargs={'pk': self.asset.asset_id})
|
||||
response = self.client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""})
|
||||
# self.assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'description', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'status', 'This field is required.')
|
||||
self.assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
|
||||
self.assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||
self.assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||
self.assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
|
||||
|
||||
def test_cable_edit(self):
|
||||
url = reverse('asset_update', kwargs={'pk': self.cable_asset.asset_id})
|
||||
# TODO Why do I have to send is_cable=True here?
|
||||
response = self.client.post(url, {'is_cable': True, 'length': -3, 'csa': -3})
|
||||
|
||||
# TODO Can't figure out how to select the 'none' option...
|
||||
# self.assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
|
||||
self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
|
||||
def test_asset_duplicate(self):
|
||||
url = reverse('asset_duplicate', kwargs={'pk': self.cable_asset.asset_id})
|
||||
response = self.client.post(url, {'is_cable': True, 'length': 0, 'csa': 0})
|
||||
|
||||
self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
|
||||
|
||||
class TestSampleDataGenerator(TestCase):
|
||||
@override_settings(DEBUG=True)
|
||||
def test_generate_sample_data(self):
|
||||
# Run the management command and check there are no exceptions
|
||||
call_command('generateSampleAssetsData')
|
||||
|
||||
# Check there are lots
|
||||
self.assertTrue(models.Asset.objects.all().count() > 50)
|
||||
self.assertTrue(models.Supplier.objects.all().count() > 50)
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
def test_delete_sample_data(self):
|
||||
call_command('deleteSampleData')
|
||||
|
||||
self.assertTrue(models.Asset.objects.all().count() == 0)
|
||||
self.assertTrue(models.Supplier.objects.all().count() == 0)
|
||||
|
||||
def test_production_exception(self):
|
||||
from django.core.management.base import CommandError
|
||||
|
||||
self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleAssetsData')
|
||||
self.assertRaisesRegex(CommandError, ".*production", call_command, 'deleteSampleData')
|
||||
|
||||
|
||||
class TestEmbeddedViews(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.profile = rigsmodels.Profile.objects.create(username="EmbeddedViewsTest", email="embedded@test.com", is_superuser=True, is_active=True, is_staff=True)
|
||||
|
||||
working = models.AssetStatus.objects.create(name="Working", should_show=True)
|
||||
lighting = models.AssetCategory.objects.create(name="Lighting")
|
||||
|
||||
cls.assets = {
|
||||
1: models.Asset.objects.create(asset_id="1991", description="Spaceflower", status=working, category=lighting, date_acquired=datetime.date(1991, 12, 26))
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
self.profile.set_password('testuser')
|
||||
self.profile.save()
|
||||
|
||||
def testLoginRedirect(self):
|
||||
request_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id})
|
||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||
|
||||
# Request the page and check it redirects
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
||||
|
||||
# Now login
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
# And check that it no longer redirects
|
||||
response = self.client.get(request_url, follow=True)
|
||||
self.assertEqual(len(response.redirect_chain), 0)
|
||||
|
||||
def testLoginCookieWarning(self):
|
||||
login_url = reverse('login_embed')
|
||||
response = self.client.post(login_url, follow=True)
|
||||
self.assertContains(response, "Cookies do not seem to be enabled")
|
||||
|
||||
def testXFrameHeaders(self):
|
||||
asset_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id})
|
||||
login_url = reverse('login_embed')
|
||||
|
||||
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||
|
||||
response = self.client.get(asset_url, follow=True)
|
||||
with self.assertRaises(KeyError):
|
||||
response._headers["X-Frame-Options"]
|
||||
|
||||
response = self.client.get(login_url, follow=True)
|
||||
with self.assertRaises(KeyError):
|
||||
response._headers["X-Frame-Options"]
|
||||
|
||||
def testOEmbed(self):
|
||||
asset_url = reverse('asset_detail', kwargs={'pk': self.assets[1].asset_id})
|
||||
asset_embed_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id})
|
||||
oembed_url = reverse('asset_oembed', kwargs={'pk': self.assets[1].asset_id})
|
||||
|
||||
alt_oembed_url = reverse('asset_oembed', kwargs={'pk': 999})
|
||||
alt_asset_embed_url = reverse('asset_embed', kwargs={'pk': 999})
|
||||
|
||||
# Test the meta tag is in place
|
||||
response = self.client.get(asset_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertContains(response, '<link rel="alternate" type="application/json+oembed"')
|
||||
self.assertContains(response, oembed_url)
|
||||
|
||||
# Test that the JSON exists
|
||||
response = self.client.get(oembed_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, asset_embed_url)
|
||||
|
||||
# Should also work for non-existant
|
||||
response = self.client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, alt_asset_embed_url)
|
||||
337
assets/tests/test_interaction.py
Normal file
337
assets/tests/test_interaction.py
Normal file
@@ -0,0 +1,337 @@
|
||||
import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from PyRIGS.tests.base import AutoLoginTest, screenshot_failure_cls, assert_times_equal
|
||||
from PyRIGS.tests.pages import animation_is_finished
|
||||
from assets import models
|
||||
from . import pages
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestAssetList(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
sound = models.AssetCategory.objects.create(name="Sound")
|
||||
lighting = models.AssetCategory.objects.create(name="Lighting")
|
||||
|
||||
working = models.AssetStatus.objects.create(name="Working", should_show=True)
|
||||
broken = models.AssetStatus.objects.create(name="Broken", should_show=False)
|
||||
|
||||
models.Asset.objects.create(asset_id="1", description="Broken XLR", status=broken, category=sound,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="10", description="Working Mic", status=working, category=sound,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="2", description="A light", status=working, category=lighting,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="C1", description="The pearl", status=broken, category=lighting,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
self.page = pages.AssetList(self.driver, self.live_server_url).open()
|
||||
|
||||
def test_default_statuses_applied(self):
|
||||
# Only the working stuff should be shown initially
|
||||
asset_descriptions = list(map(lambda x: x.description, self.page.assets))
|
||||
self.assertEqual(2, len(asset_descriptions))
|
||||
self.assertIn("A light", asset_descriptions)
|
||||
self.assertIn("Working Mic", asset_descriptions)
|
||||
|
||||
def test_asset_order(self):
|
||||
# Only the working stuff should be shown initially
|
||||
self.page.status_selector.open()
|
||||
self.page.status_selector.set_option("Broken", True)
|
||||
self.page.status_selector.close()
|
||||
|
||||
self.page.search()
|
||||
|
||||
asset_ids = list(map(lambda x: x.id, self.page.assets))
|
||||
self.assertEqual("1", asset_ids[0])
|
||||
self.assertEqual("2", asset_ids[1])
|
||||
self.assertEqual("10", asset_ids[2])
|
||||
self.assertEqual("C1", asset_ids[3])
|
||||
|
||||
def test_search(self):
|
||||
self.page.set_query("10")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 1)
|
||||
self.assertEqual("Working Mic", self.page.assets[0].description)
|
||||
self.assertEqual("10", self.page.assets[0].id)
|
||||
|
||||
self.page.set_query("light")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 1)
|
||||
self.assertEqual("A light", self.page.assets[0].description)
|
||||
|
||||
self.page.set_query("Random string")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 0)
|
||||
|
||||
self.page.set_query("")
|
||||
self.page.search()
|
||||
# Only working stuff shown by default
|
||||
self.assertTrue(len(self.page.assets) == 2)
|
||||
|
||||
self.page.status_selector.toggle()
|
||||
self.assertTrue(self.page.status_selector.is_open)
|
||||
self.page.status_selector.select_all()
|
||||
self.page.status_selector.toggle()
|
||||
self.assertFalse(self.page.status_selector.is_open)
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 4)
|
||||
|
||||
self.page.category_selector.toggle()
|
||||
self.assertTrue(self.page.category_selector.is_open)
|
||||
self.page.category_selector.set_option("Sound", True)
|
||||
self.page.category_selector.close()
|
||||
self.assertFalse(self.page.category_selector.is_open)
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.assets) == 2)
|
||||
asset_ids = list(map(lambda x: x.id, self.page.assets))
|
||||
self.assertEqual("1", asset_ids[0])
|
||||
self.assertEqual("10", asset_ids[1])
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestAssetForm(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = models.AssetCategory.objects.create(name="Health & Safety")
|
||||
self.status = models.AssetStatus.objects.create(name="O.K.", should_show=True)
|
||||
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
self.parent = models.Asset.objects.create(asset_id="9000", description="Shelf", status=self.status,
|
||||
category=self.category, date_acquired=datetime.date(2000, 1, 1))
|
||||
self.connector = models.Connector.objects.create(description="IEC", current_rating=10, voltage_rating=240,
|
||||
num_pins=3)
|
||||
self.cable_type = models.CableType.objects.create(plug=self.connector, socket=self.connector, circuits=1,
|
||||
cores=3)
|
||||
self.page = pages.AssetCreate(self.driver, self.live_server_url).open()
|
||||
|
||||
def test_asset_create(self):
|
||||
# Test that ID is automatically assigned and properly incremented
|
||||
self.assertIn(self.page.asset_id, "9001")
|
||||
|
||||
self.page.remove_all_required()
|
||||
self.page.asset_id = "XX$X"
|
||||
self.page.submit()
|
||||
self.assertFalse(self.page.success)
|
||||
self.assertIn("An Asset ID can only consist of letters and numbers, with a final number",
|
||||
self.page.errors["Asset id"])
|
||||
self.assertIn("This field is required.", self.page.errors["Description"])
|
||||
|
||||
self.page.open()
|
||||
|
||||
self.page.description = desc = "Bodge Lead"
|
||||
self.page.category = cat = "Health & Safety"
|
||||
self.page.status = status = "O.K."
|
||||
self.page.serial_number = sn = "0124567890-SAUSAGE"
|
||||
self.page.comments = comments = "This is actually a sledgehammer, not a cable..."
|
||||
|
||||
self.page.purchased_from_selector.toggle()
|
||||
self.assertTrue(self.page.purchased_from_selector.is_open)
|
||||
self.page.purchased_from_selector.search(self.supplier.name[:-8])
|
||||
self.page.purchased_from_selector.set_option(self.supplier.name, True)
|
||||
self.page.purchase_price = "12.99"
|
||||
self.page.salvage_value = "99.12"
|
||||
self.page.date_acquired = acquired = datetime.date(2020, 5, 2)
|
||||
|
||||
self.page.parent_selector.toggle()
|
||||
self.assertTrue(self.page.parent_selector.is_open)
|
||||
self.page.parent_selector.search(self.parent.asset_id)
|
||||
# Needed here but not earlier for whatever reason
|
||||
self.driver.implicitly_wait(1)
|
||||
self.page.parent_selector.set_option(self.parent.asset_id + " | " + self.parent.description, True)
|
||||
self.assertTrue(self.page.parent_selector.options[0].selected)
|
||||
self.page.parent_selector.toggle()
|
||||
|
||||
self.assertFalse(self.driver.find_element_by_id('cable-table').is_displayed())
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
# Check that data is right
|
||||
asset = models.Asset.objects.get(asset_id="9001")
|
||||
self.assertEqual(asset.description, desc)
|
||||
self.assertEqual(asset.category.name, cat)
|
||||
self.assertEqual(asset.status.name, status)
|
||||
self.assertEqual(asset.serial_number, sn)
|
||||
self.assertEqual(asset.comments, comments)
|
||||
# This one is important as it defaults to today's date
|
||||
self.assertEqual(asset.date_acquired, acquired)
|
||||
|
||||
def test_cable_create(self):
|
||||
self.page.description = "IEC -> IEC"
|
||||
self.page.category = "Health & Safety"
|
||||
self.page.status = "O.K."
|
||||
self.page.serial_number = "MELON-MELON-MELON"
|
||||
self.page.comments = "You might need that"
|
||||
self.page.is_cable = True
|
||||
|
||||
self.assertTrue(self.driver.find_element_by_id('cable-table').is_displayed())
|
||||
self.wait.until(animation_is_finished())
|
||||
self.page.cable_type = "IEC → IEC"
|
||||
self.page.socket = "IEC"
|
||||
self.page.length = 10
|
||||
self.page.csa = "1.5"
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
def test_asset_edit(self):
|
||||
self.page = pages.AssetEdit(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
|
||||
|
||||
self.assertTrue(self.driver.find_element_by_id('id_asset_id').get_attribute('readonly') is not None)
|
||||
|
||||
new_description = "Big Shelf"
|
||||
self.page.description = new_description
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
self.assertEqual(models.Asset.objects.get(asset_id=self.parent.asset_id).description, new_description)
|
||||
|
||||
def test_asset_duplicate(self):
|
||||
self.page = pages.AssetDuplicate(self.driver, self.live_server_url, asset_id=self.parent.asset_id).open()
|
||||
|
||||
self.assertNotEqual(self.parent.asset_id, self.page.asset_id)
|
||||
self.assertEqual(self.parent.description, self.page.description)
|
||||
self.assertEqual(self.parent.status.name, self.page.status)
|
||||
self.assertEqual(self.parent.category.name, self.page.category)
|
||||
self.assertEqual(self.parent.date_acquired, self.page.date_acquired.date())
|
||||
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
self.assertEqual(models.Asset.objects.last().description, self.parent.description)
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestSupplierList(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
models.Supplier.objects.create(name="Acme.")
|
||||
models.Supplier.objects.create(name="TEC PA & Lighting")
|
||||
models.Supplier.objects.create(name="Caterpillar Inc.")
|
||||
models.Supplier.objects.create(name="N.E.R.D")
|
||||
models.Supplier.objects.create(name="Khumalo")
|
||||
models.Supplier.objects.create(name="1984 Incorporated")
|
||||
self.page = pages.SupplierList(self.driver, self.live_server_url).open()
|
||||
|
||||
# Should be sorted alphabetically
|
||||
def test_order(self):
|
||||
names = list(map(lambda x: x.name, self.page.suppliers))
|
||||
self.assertEqual("1984 Incorporated", names[0])
|
||||
self.assertEqual("Acme.", names[1])
|
||||
self.assertEqual("Caterpillar Inc.", names[2])
|
||||
self.assertEqual("Fullmetal Heavy Industry", names[3])
|
||||
self.assertEqual("Khumalo", names[4])
|
||||
self.assertEqual("N.E.R.D", names[5])
|
||||
self.assertEqual("TEC PA & Lighting", names[6])
|
||||
|
||||
def test_search(self):
|
||||
self.page.set_query("TEC")
|
||||
self.page.search()
|
||||
|
||||
self.assertTrue(len(self.page.suppliers) == 1)
|
||||
self.assertEqual("TEC PA & Lighting", self.page.suppliers[0].name)
|
||||
|
||||
self.page.set_query("")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.suppliers) == 7)
|
||||
|
||||
self.page.set_query("This is not a supplier")
|
||||
self.page.search()
|
||||
self.assertTrue(len(self.page.suppliers) == 0)
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestSupplierCreateAndEdit(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry")
|
||||
|
||||
def test_supplier_create(self):
|
||||
self.page = pages.SupplierCreate(self.driver, self.live_server_url).open()
|
||||
|
||||
self.page.remove_all_required()
|
||||
self.page.submit()
|
||||
self.assertFalse(self.page.success)
|
||||
self.assertIn("This field is required.", self.page.errors["Name"])
|
||||
|
||||
self.page.name = "Optican Health Supplies"
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
def test_supplier_edit(self):
|
||||
self.page = pages.SupplierEdit(self.driver, self.live_server_url, supplier_id=self.supplier.pk).open()
|
||||
|
||||
self.assertEqual("Fullmetal Heavy Industry", self.page.name)
|
||||
new_name = "Cyberdyne Systems"
|
||||
self.page.name = new_name
|
||||
self.page.submit()
|
||||
self.assertTrue(self.page.success)
|
||||
|
||||
|
||||
@screenshot_failure_cls
|
||||
class TestAssetAudit(AutoLoginTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = models.AssetCategory.objects.create(name="Haulage")
|
||||
self.status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True)
|
||||
self.supplier = models.Supplier.objects.create(name="The Bazaar")
|
||||
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1,
|
||||
voltage_rating=40, num_pins=13)
|
||||
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status,
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status,
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status, category=self.category,
|
||||
date_acquired=datetime.date(2020, 2, 1))
|
||||
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
|
||||
self.wait = WebDriverWait(self.driver, 20)
|
||||
|
||||
def test_audit_process(self):
|
||||
asset_id = "1111"
|
||||
self.page.set_query(asset_id)
|
||||
self.page.search()
|
||||
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
||||
# Do it wrong on purpose to check error display
|
||||
self.page.modal.remove_all_required()
|
||||
self.page.modal.description = ""
|
||||
self.page.modal.submit()
|
||||
self.wait.until(animation_is_finished())
|
||||
self.assertIn("This field is required.", self.page.modal.errors["Description"])
|
||||
# Now do it properly
|
||||
self.page.modal.description = new_desc = "A BIG hammer"
|
||||
self.page.modal.submit()
|
||||
self.wait.until(animation_is_finished())
|
||||
submit_time = timezone.now()
|
||||
# Check data is correct
|
||||
audited = models.Asset.objects.get(asset_id=asset_id)
|
||||
self.assertEqual(audited.description, new_desc)
|
||||
# Make sure audit 'log' was filled out
|
||||
self.assertEqual(self.profile.initials, audited.last_audited_by.initials)
|
||||
assert_times_equal(submit_time, audited.last_audited_at)
|
||||
# Check we've removed it from the 'needing audit' list
|
||||
self.assertNotIn(asset_id, self.page.assets)
|
||||
|
||||
def test_audit_list(self):
|
||||
self.assertEqual(len(models.Asset.objects.filter(last_audited_at=None)), len(self.page.assets))
|
||||
asset_row = self.page.assets[0]
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click()
|
||||
self.wait.until(ec.visibility_of_element_located((By.ID, 'modal')))
|
||||
self.assertEqual(self.page.modal.asset_id, asset_row.id)
|
||||
self.page.modal.close()
|
||||
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
|
||||
# Make sure audit log was NOT filled out
|
||||
audited = models.Asset.objects.get(asset_id=asset_row.id)
|
||||
assert audited.last_audited_by is None
|
||||
|
||||
def test_audit_search(self):
|
||||
# Check that a failed search works
|
||||
self.page.set_query("NOTFOUND")
|
||||
self.page.search()
|
||||
self.assertFalse(self.driver.find_element_by_id('modal').is_displayed())
|
||||
self.assertIn("Asset with that ID does not exist!", self.page.error.text)
|
||||
289
assets/tests/test_unit.py
Normal file
289
assets/tests/test_unit.py
Normal file
@@ -0,0 +1,289 @@
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.core.management import call_command
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains
|
||||
|
||||
from assets import models, urls
|
||||
|
||||
pytestmark = pytest.mark.django_db # TODO
|
||||
|
||||
|
||||
def login(client, django_user_model):
|
||||
pwd = 'testuser'
|
||||
usr = "TestUser"
|
||||
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)
|
||||
|
||||
|
||||
def create_test_asset():
|
||||
working = models.AssetStatus.objects.create(name="Working", should_show=True)
|
||||
lighting = models.AssetCategory.objects.create(name="Lighting")
|
||||
asset = models.Asset.objects.create(asset_id="1991", description="Spaceflower", status=working, category=lighting, date_acquired=datetime.date(1991, 12, 26))
|
||||
return asset
|
||||
|
||||
|
||||
def create_test_cable():
|
||||
category = models.AssetCategory.objects.create(name="Sound")
|
||||
status = models.AssetStatus.objects.create(name="Broken", should_show=True)
|
||||
connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
|
||||
cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=connector, socket=connector)
|
||||
return models.Asset.objects.create(asset_id="666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5")
|
||||
|
||||
|
||||
def test_supplier_create(client, django_user_model):
|
||||
login(client, django_user_model)
|
||||
url = reverse('supplier_create')
|
||||
response = client.post(url)
|
||||
assertFormError(response, 'form', 'name', 'This field is required.')
|
||||
|
||||
|
||||
def test_supplier_edit(client, django_user_model):
|
||||
login(client, django_user_model)
|
||||
supplier = models.Supplier.objects.create(name="Gadgetron Corporation")
|
||||
url = reverse('supplier_update', kwargs={'pk': supplier.pk})
|
||||
response = client.post(url, {'name': ""})
|
||||
assertFormError(response, 'form', 'name', 'This field is required.')
|
||||
|
||||
|
||||
def test_404(client, django_user_model):
|
||||
login(client, django_user_model)
|
||||
urls = {'asset_detail', 'asset_update', 'asset_duplicate', 'supplier_detail', 'supplier_update'}
|
||||
for url_name in urls:
|
||||
request_url = reverse(url_name, kwargs={'pk': "0000"})
|
||||
response = client.get(request_url, follow=True)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_embed_login_redirect(client, django_user_model):
|
||||
request_url = reverse('asset_embed', kwargs={'pk': create_test_asset().asset_id})
|
||||
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||
|
||||
# Request the page and check it redirects
|
||||
response = client.get(request_url, follow=True)
|
||||
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
||||
|
||||
# Now login
|
||||
login(client, django_user_model)
|
||||
|
||||
# And check that it no longer redirects
|
||||
response = client.get(request_url, follow=True)
|
||||
assert len(response.redirect_chain) == 0
|
||||
|
||||
|
||||
def test_login_cookie_warning(client, django_user_model):
|
||||
login_url = reverse('login_embed')
|
||||
response = client.post(login_url, follow=True)
|
||||
assert "Cookies do not seem to be enabled" in str(response.content)
|
||||
|
||||
|
||||
def test_x_frame_headers(client, django_user_model):
|
||||
asset_url = reverse('asset_embed', kwargs={'pk': create_test_asset().asset_id})
|
||||
login_url = reverse('login_embed')
|
||||
|
||||
login(client, django_user_model)
|
||||
|
||||
response = client.get(asset_url, follow=True)
|
||||
with pytest.raises(KeyError):
|
||||
response._headers["X-Frame-Options"]
|
||||
|
||||
response = client.get(login_url, follow=True)
|
||||
with pytest.raises(KeyError):
|
||||
response._headers["X-Frame-Options"]
|
||||
|
||||
|
||||
def test_oembed(client):
|
||||
asset = create_test_asset()
|
||||
asset_url = reverse('asset_detail', kwargs={'pk': asset.asset_id})
|
||||
asset_embed_url = reverse('asset_embed', kwargs={'pk': asset.asset_id})
|
||||
oembed_url = reverse('asset_oembed', kwargs={'pk': asset.asset_id})
|
||||
|
||||
alt_oembed_url = reverse('asset_oembed', kwargs={'pk': 999})
|
||||
alt_asset_embed_url = reverse('asset_embed', kwargs={'pk': 999})
|
||||
|
||||
# Test the meta tag is in place
|
||||
response = client.get(asset_url, follow=True, HTTP_HOST='example.com')
|
||||
assert '<link rel="alternate" type="application/json+oembed"' in str(response.content)
|
||||
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, asset_embed_url)
|
||||
|
||||
# Should also work for non-existant
|
||||
response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
|
||||
assert response.status_code == 200
|
||||
assertContains(response, alt_asset_embed_url)
|
||||
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
def test_generate_sample_data(client):
|
||||
# Run the management command and check there are no exceptions
|
||||
call_command('generateSampleAssetsData')
|
||||
|
||||
# Check there are lots
|
||||
assert models.Asset.objects.all().count() > 50
|
||||
assert models.Supplier.objects.all().count() > 50
|
||||
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
def test_delete_sample_data(client):
|
||||
call_command('deleteSampleData')
|
||||
|
||||
assert models.Asset.objects.all().count() == 0
|
||||
assert models.Supplier.objects.all().count() == 0
|
||||
|
||||
|
||||
def test_production_exception(client):
|
||||
from django.core.management.base import CommandError
|
||||
|
||||
with pytest.raises(CommandError, match=".*production"):
|
||||
call_command('generateSampleAssetsData')
|
||||
call_command('deleteSampleData')
|
||||
|
||||
|
||||
def test_asset_create(client, django_user_model):
|
||||
login(client, django_user_model)
|
||||
response = client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'})
|
||||
assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||
assertFormError(response, 'form', 'description', 'This field is required.')
|
||||
assertFormError(response, 'form', 'status', 'This field is required.')
|
||||
assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
|
||||
assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||
assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
|
||||
|
||||
|
||||
def test_cable_create(client, django_user_model):
|
||||
login(client, django_user_model)
|
||||
response = client.post(reverse('asset_create'), {'asset_id': 'X$%A', 'is_cable': True})
|
||||
assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number')
|
||||
|
||||
assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
|
||||
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
|
||||
# Given that validation is done at model level it *shouldn't* need retesting...gonna do it anyway!
|
||||
|
||||
|
||||
def test_asset_edit(client, django_user_model):
|
||||
login(client, django_user_model)
|
||||
url = reverse('asset_update', kwargs={'pk': create_test_asset().asset_id})
|
||||
response = client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""})
|
||||
# assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||
assertFormError(response, 'form', 'description', 'This field is required.')
|
||||
assertFormError(response, 'form', 'status', 'This field is required.')
|
||||
assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||
assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
|
||||
|
||||
|
||||
def test_cable_edit(client, django_user_model):
|
||||
login(client, django_user_model)
|
||||
url = reverse('asset_update', kwargs={'pk': create_test_cable().asset_id})
|
||||
# TODO Why do I have to send is_cable=True here?
|
||||
response = client.post(url, {'is_cable': True, 'length': -3, 'csa': -3})
|
||||
|
||||
# TODO Can't figure out how to select the 'none' option...
|
||||
# assertFormError(response, 'form', 'cable_type', 'A cable must have a type')
|
||||
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
|
||||
|
||||
def test_asset_duplicate(client, django_user_model):
|
||||
login(client, django_user_model)
|
||||
url = reverse('asset_duplicate', kwargs={'pk': create_test_cable().asset_id})
|
||||
response = client.post(url, {'is_cable': True, 'length': 0, 'csa': 0})
|
||||
|
||||
assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0')
|
||||
assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0')
|
||||
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
def create_asset_one():
|
||||
# Shortcut to create the levels - bonus side effect of testing the command (hopefully) matches production
|
||||
call_command('generateSampleData')
|
||||
# Create an asset with ID 1 to make things easier in loops (we can always use pk=1)
|
||||
category = models.AssetCategory.objects.create(name="Number One")
|
||||
status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True)
|
||||
return models.Asset.objects.create(asset_id="1", description="Half Price Fish", status=status, category=category, date_acquired=datetime.date(2020, 2, 1))
|
||||
|
||||
# Nothing should be available to the unauthenticated
|
||||
|
||||
|
||||
def test_unauthenticated(client):
|
||||
for url in filter(lambda url: url.name is not None and "json" not in str(url), urls.urlpatterns):
|
||||
pattern = str(url.pattern)
|
||||
request_url = ""
|
||||
if ":pk>" in pattern:
|
||||
request_url = reverse(url.name, kwargs={'pk': 1})
|
||||
else:
|
||||
request_url = reverse(url.name)
|
||||
if request_url:
|
||||
response = client.get(request_url, follow=True, HTTP_HOST='example.com')
|
||||
# TODO Check the URL here
|
||||
assertContains(response, 'Login')
|
||||
|
||||
|
||||
def test_basic_access(client):
|
||||
create_asset_one()
|
||||
client.login(username="basic", password="basic")
|
||||
|
||||
url = reverse('asset_list')
|
||||
response = client.get(url)
|
||||
# Check edit and duplicate buttons NOT shown in list
|
||||
assertNotContains(response, 'Edit')
|
||||
assertNotContains(response, 'Duplicate')
|
||||
|
||||
url = reverse('asset_detail', kwargs={'pk': "9000"})
|
||||
response = client.get(url)
|
||||
assertNotContains(response, 'Purchase Details')
|
||||
assertNotContains(response, 'View Revision History')
|
||||
|
||||
urls = {'asset_history', 'asset_update', 'asset_duplicate'}
|
||||
for url_name in urls:
|
||||
request_url = reverse(url_name, kwargs={'pk': "9000"})
|
||||
response = client.get(request_url, follow=True)
|
||||
assert response.status_code == 403
|
||||
|
||||
request_url = reverse('supplier_create')
|
||||
response = client.get(request_url, follow=True)
|
||||
assert response.status_code == 403
|
||||
|
||||
request_url = reverse('supplier_update', kwargs={'pk': "1"})
|
||||
response = client.get(request_url, follow=True)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_keyholder_access(client):
|
||||
create_asset_one()
|
||||
client.login(username="keyholder", password="keyholder")
|
||||
|
||||
url = reverse('asset_list')
|
||||
response = client.get(url)
|
||||
# Check edit and duplicate buttons shown in list
|
||||
assertContains(response, 'Edit')
|
||||
assertContains(response, 'Duplicate')
|
||||
|
||||
url = reverse('asset_detail', kwargs={'pk': "9000"})
|
||||
response = client.get(url)
|
||||
assertContains(response, 'Purchase Details')
|
||||
assertContains(response, 'View Revision History')
|
||||
|
||||
|
||||
def test_page_titles(admin_client):
|
||||
for url in filter(lambda url: url.name is not None and not any(s in url.name for s in ["json", "embed"]), urls.urlpatterns):
|
||||
request_url = ""
|
||||
if ":pk>" in str(url.pattern):
|
||||
request_url = reverse(url.name, kwargs={'pk': "1"})
|
||||
else:
|
||||
request_url = reverse(url.name)
|
||||
response = admin_client.get(request_url)
|
||||
if hasattr(response, "context_data") and "page_title" in response.context_data:
|
||||
expected_title = response.context_data["page_title"]
|
||||
assertContains(response, '<title>{} | Rig Information Gathering System</title>'.format(expected_title))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user