Toolchain/Dependency Upgrade (#418)

* Upgrade to heroku-20 stack

* Move some gulp deps to dev rather than prod

* npm upgrade

* Fix audit time check in asset audit test

* Attempt at parallelising tests where possible

* Add basic calendar button test

Mainly to pickup on FullCalendar loading errors

* Upgrade python deps

* Tends to help if I push valid yaml

* You valid now?

* Fix whoops in requirements.txt

* Change python ver

* Define service in coveralls task

* Run parallelised RIGS tests as one matrix job

* Update python version in tests

* Cache python dependencies

Should majorly speedup parallelillelelised testing

* Purge old vagrant config

* No Ruby compass bodge, no need for rubocop!

* Purge old .idea config

* Switch to gh-a artifact uploading instead of imgur 'hack'

For test failure screenshots. Happy now @mattysmith22? ;p

* Oops, remove unused import

* Exclude tests from the coverage stats

Seems to be artifically deflating our stats

* Refactor asset audit tests with better selectors

Also fixed a silly title error with the modal

* Add title checking to the slightly insane assets test

* Fix unauth test to not just immediately pass out

* Upload failure screenshots as individual artifacts not a zip

Turns out I can't unzip things from my phone, which is a pain

* Should fix asset test on CI

* What about this?

* What about this?

Swear I spend my life jiggerypokerying the damn test suite...

* Does this help the coverage be less weird?

* Revert "Does this help the coverage be less weird?"

This reverts commit 39ab9df836.

* Use pytest as our test runner for better parallelism

Also rewrote some asset tests to be in the pytest style. May do some more. Some warnings cleaned up in the process.

* Bah, codestyle

* Oops, remove obsolete if check

* Fix screenshot uploading on CI (again)

* Try this way of parallel coverage

* Add codeclimate maintainability badge

* Remove some unused gulp dependencies

* Run asset building serverside

* Still helps if I commit valid YAML

* See below

* Different approach to CI dependencies

* Exclude node_modules from codestyle

* Does this work?

* Parallel parallel builds were giving me a headache, try this

* Update codeclimate settings, purge some config files

* Well the YAML was *syntactically* valid....

* Switch back to old coveralls method

* Fix codeclimate config, mark 2

* Attempt to bodge asset test

* Oops, again

Probably bedtime..

* Might fix heroku building

* Attempt #2 at fixing heroku

* Belt and braces approach to coverage

* Github, you need a Actions YAML validator!

* Might fix actions?

* Try ignoring some third party deprecation warnings

* Another go at making coverage show up

* Some template cleanup

* Minor python cleanup

* Import optimisation

* Revert "Minor python cleanup"

This reverts commit 6a4620a2e5.

* Add format arg to coverage command

* Ignore test directories from Heroku slug

* Maybe this works to purge deps postbuild

* Bunch of test refactoring

* Restore signals import, screw you import optimisation

* Further template refactoring

* Add support for running tests with geckodriver, do this on CI

* Screw you codestyle

* Disable firefox tests for now

That was way more errors than I expected

* Run cleanup script from the right location

* Plausibly fix tests

* Helps if I don't delete the pipeline folder prior to collectstatic

* Enable whitenoise

* Can I delete pipeline here?

* Allow seconds difference in assert_times_equal

* Disable codeclimate

* Remove not working rm command

* Maybe this fixes coverage?

* Try different coverage reporter

* Fix search_help to need login

* Made versioning magic a bit less expansive

We have more apps than I thought...

* Fix IDI0T error in Assets URLS

* Refactor 'no access to unauthed' test to cover all of PyRIGS

* Add RAs/Checklists to sample data generator

* Fix some HTML errors in templates

Which apparently only Django's HTML parser cares about, browsers DGAF...

* Port title test to project level

* Fix more HTML

* Fix cable type detail
This commit is contained in:
2021-01-31 04:05:33 +00:00
committed by GitHub
parent 8ad629a47e
commit 2bf0175786
157 changed files with 9507 additions and 44028 deletions

View File

@@ -1,32 +0,0 @@
---
engines:
csslint:
enabled: true
duplication:
enabled: true
config:
languages:
- ruby
- javascript
- python
- php
eslint:
enabled: true
fixme:
enabled: true
radon:
enabled: true
rubocop:
enabled: true
ratings:
paths:
- "**.css"
- "**.inc"
- "**.js"
- "**.jsx"
- "**.module"
- "**.php"
- "**.py"
- "**.rb"
exclude_paths:
- config/

View File

@@ -1,8 +1,3 @@
[run]
source =
./
plugins =
django_coverage_plugin
omit =
*/migrations/*
plugins = django_coverage_plugin
omit = */migrations/*, */tests/*

View File

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

View File

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

213
.eslintrc
View File

@@ -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

View File

@@ -8,41 +8,45 @@ 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]')"
strategy:
max-parallel: 1
matrix:
python-version: [3.8]
runs-on: ubuntu-latest
# strategy:
# matrix:
# browser: ['chrome']
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 coveralls django_coverage_plugin pytest-cov
pip install --upgrade --upgrade-strategy eager -r requirements.txt
python manage.py collectstatic --noinput
- 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
run: pytest --cov -n 8
- 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
View File

@@ -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
View File

@@ -1 +0,0 @@
PyRIGS

5
.idea/encodings.xml generated
View File

@@ -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
View File

@@ -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>

View File

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

7
.idea/vcs.xml generated
View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,6 +1,7 @@
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from RIGS import models
@@ -15,11 +16,7 @@ def get_oembed(login_url, request, oembed_view, kwargs):
return resp
def has_oembed(oembed_view, login_url=None):
if not login_url:
from django.conf import settings
login_url = settings.LOGIN_URL
def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
def _dec(view_func):
def _checklogin(request, *args, **kwargs):
if request.user.is_authenticated:

View File

@@ -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__))
@@ -77,6 +78,7 @@ INSTALLED_APPS = (
MIDDLEWARE = (
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
@@ -90,6 +92,7 @@ MIDDLEWARE = (
ROOT_URLCONF = 'PyRIGS.urls'
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
@@ -235,6 +238,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 +269,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
View File

View 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):
@@ -19,15 +20,21 @@ def create_datetime(year, month, day, hour, min):
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)
browser = env('BROWSER', default="chrome")
if browser == "firefox":
options = webdriver.FirefoxOptions()
options.headless = True
driver = webdriver.Firefox(options=options)
driver.set_window_position(0, 0)
# Firefox is pissy about out of bounds otherwise
driver.set_window_size(3840, 2160)
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
@@ -35,6 +42,7 @@ class BaseTest(LiveServerTestCase):
def setUp(self):
super().setUpClass()
self.driver = create_browser()
self.wait = WebDriverWait(self.driver, 15)
def tearDown(self):
super().tearDown()
@@ -48,8 +56,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 +70,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 +83,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=0) == second_time.replace(microsecond=0, second=0)

View File

@@ -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

View File

@@ -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,7 +75,6 @@ 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))
@property

65
PyRIGS/tests/test_unit.py Normal file
View File

@@ -0,0 +1,65 @@
from PyRIGS import urls
from assets.tests.test_unit import create_asset_one
import pytest
from django.urls import URLPattern, URLResolver, reverse
from django.urls.exceptions import NoReverseMatch
from pytest_django.asserts import assertContains, assertRedirects, assertTemplateUsed, assertInHTML
pytestmark = pytest.mark.django_db
def find_urls_recursive(patterns):
urls_to_check = []
for url in patterns:
if isinstance(url, URLResolver):
urls_to_check += find_urls_recursive(url.url_patterns)
elif isinstance(url, URLPattern):
# Skip some thinks that actually don't need auth (mainly OEmbed JSONs that are essentially just a redirect)
if url.name is not None and url.name != "closemodal" and "json" not in str(url):
urls_to_check.append(url)
return urls_to_check
def get_request_url(url):
pattern = str(url.pattern)
request_url = ""
try:
kwargz = {}
if ":pk>" in pattern:
kwargz['pk'] = 1
if ":model>" in pattern:
kwargz['model'] = "event"
return reverse(url.name, kwargs=kwargz)
except NoReverseMatch:
print("Couldn't test url " + pattern)
def test_unauthenticated(client): # Nothing should be available to the unauthenticated
create_asset_one()
for url in find_urls_recursive(urls.urlpatterns):
request_url = get_request_url(url)
if request_url and 'user' not in request_url: # User module is full of edge cases
response = client.get(request_url, follow=True, HTTP_HOST='example.com')
assertContains(response, 'Login')
if 'application/json+oembed' in response.content.decode():
assertTemplateUsed(response, 'login_redirect.html')
else:
if "embed" in str(url):
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
else:
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
assertRedirects(response, expected_url)
def test_page_titles(admin_client):
create_asset_one()
for url in filter((lambda u: "embed" not in u.name), find_urls_recursive(urls.urlpatterns)):
request_url = get_request_url(url)
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"]
# try:
assertInHTML('<title>{} | Rig Information Gathering System'.format(expected_title), response.content.decode())
print("{} | {}".format(request_url, expected_title)) # If test fails, tell me where!
# except:
# print(response.content.decode(), file=open('output.html', 'w'))

View File

@@ -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
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 = [
@@ -27,7 +22,7 @@ urlpatterns = [
name="api_secure"),
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
path('search_help/', views.SearchHelp.as_view(), name='search_help'),
path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'),
path('', include('users.urls')),
@@ -39,6 +34,6 @@ if settings.DEBUG:
import debug_toolbar
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)),
path('__debug__/', include(debug_toolbar.urls)),
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
] + urlpatterns

View File

@@ -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

View File

@@ -1,6 +1,7 @@
# TEC PA & Lighting - PyRIGS #
![Build Status](https://github.com/nottinghamtec/PyRIGS/workflows/Django%20CI/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS)
[![Maintainability](https://api.codeclimate.com/v1/badges/79ca3b8106911a1d143f/maintainability)](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability)
Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets.

View File

@@ -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)

View File

@@ -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'})

View File

@@ -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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -1,11 +1,12 @@
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group, Permission
from django.db import transaction
from reversion import revisions as reversion
import datetime
import random
from django.contrib.auth.models import Group, Permission
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
from reversion import revisions as reversion
from RIGS import models
@@ -343,3 +344,30 @@ class Command(BaseCommand):
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance,
date=datetime.date.today())
if i == 1 or random.randint(0, 5) > 0: # Event 1 and 1 in 5 have a RA
models.RiskAssessment.objects.create(event=newEvent, supervisor_consulted=bool(random.getrandbits(1)), nonstandard_equipment=bool(random.getrandbits(1)),
nonstandard_use=bool(random.getrandbits(1)),
contractors=bool(random.getrandbits(1)),
other_companies=bool(random.getrandbits(1)),
crew_fatigue=bool(random.getrandbits(1)),
big_power=bool(random.getrandbits(1)),
generators=bool(random.getrandbits(1)),
other_companies_power=bool(random.getrandbits(1)),
nonstandard_equipment_power=bool(random.getrandbits(1)),
multiple_electrical_environments=bool(random.getrandbits(1)),
noise_monitoring=bool(random.getrandbits(1)),
known_venue=bool(random.getrandbits(1)),
safe_loading=bool(random.getrandbits(1)),
safe_storage=bool(random.getrandbits(1)),
area_outside_of_control=bool(random.getrandbits(1)),
barrier_required=bool(random.getrandbits(1)),
nonstandard_emergency_procedure=bool(random.getrandbits(1)),
special_structures=bool(random.getrandbits(1)),
suspended_structures=bool(random.getrandbits(1)),
outside=bool(random.getrandbits(1)))
if i == 0 or random.randint(0, 1) > 0: # Event 1 and 1 in 10 have a Checklist
models.EventChecklist.objects.create(event=newEvent, power_mic=random.choice(self.profiles), safe_parking=bool(random.getrandbits(1)),
safe_packing=bool(random.getrandbits(1)), exits=bool(random.getrandbits(1)), trip_hazard=bool(random.getrandbits(1)), warning_signs=bool(random.getrandbits(1)),
ear_plugs=bool(random.getrandbits(1)), hs_location="Locked away safely",
extinguishers_location="Somewhere, I forgot", earthing=bool(random.getrandbits(1)), pat=bool(random.getrandbits(1)),
date=timezone.now(), venue=random.choice(self.venues))

View File

@@ -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):

View File

@@ -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

View File

@@ -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):

View File

@@ -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 */

View File

@@ -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= */

View File

@@ -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 */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

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

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

File diff suppressed because one or more lines are too long

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -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)}))}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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"))})))}();

View File

@@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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}))}});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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}));

View File

@@ -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')+'/');

View File

@@ -235,7 +235,6 @@
</div>
{% endif %}
</div>
</div>
<div class="col-12 text-right">
{% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.pk text="Event" %}

View File

@@ -32,9 +32,9 @@
$(document).ready(function () {
$('button[data-action=add]').on('click', function (event) {
event.preventDefault();
var target = $($(this).attr('data-target'));
var newID = Number(target.attr('data-pk'));
var newRow = $($(this).attr('data-clone'))
let target = $($(this).attr('data-target'));
let newID = Number(target.attr('data-pk'));
let newRow = $($(this).attr('data-clone'))
.clone().attr('style', "")
.attr('id', function(i, val){
return val.split("_")[0] + '_' + newID;
@@ -135,7 +135,7 @@
<tr id="vehicles_new" style="display: none;">
<td><input type="text" class="form-control" name="vehicle_new" disabled="true"/></td>
<td><select class="form-control" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" name="driver_new" disabled="true"></select></td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button</td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button></td>
</tr>
{% for i in object.vehicles.all %}
<tr id="vehicles_{{i.pk}}">
@@ -147,7 +147,7 @@
{% endif %}
</select>
</td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{i.pk}}' data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button</td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{i.pk}}' data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button></td>
</tr>
{% endfor %}
</tbody>
@@ -204,10 +204,10 @@
<td>
<select name="crewmember_new" class="form-control" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" disabled="true"></select>
</td>
<td style="min-width: 15ch"><input name="start_new" type="datetime-local" class="form-control" value="{{ i.start }}" disabled="true"/></td>
<td style="min-width: 15ch"><input name="start_new" type="datetime-local" class="form-control" value="{{ i.start }}" disabled=""/></td>
<td style="min-width: 15ch"><input name="role_new" type="text" class="form-control" value="{{ i.role }}" disabled="true"/></td>
<td style="min-width: 15ch"><input name="end_new" type="datetime-local" class="form-control" value="{{ i.end }}" disabled="true" /></td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{crew.pk}}' data-action='delete' data-target='#crewmember'><span class="fas fa-times"></span></button</td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{crew.pk}}' data-action='delete' data-target='#crewmember'><span class="fas fa-times"></span></button></td>
</tr>
{% for crew in object.crew.all %}
<tr id="crew_{{crew.pk}}">
@@ -221,7 +221,7 @@
<td><input name="start_{{crew.pk}}" type="datetime-local" class="form-control" value="{{ crew.start|date:'Y-m-d' }}T{{ crew.start|date:'H:i:s' }}"/></td>
<td><input name="role_{{crew.pk}}" type="text" class="form-control" value="{{ crew.role }}"/></td>
<td><input name="end_{{crew.pk}}" type="datetime-local" class="form-control" value="{{ crew.end|date:'Y-m-d' }}T{{ crew.end|date:'H:i:s' }}"/></td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{crew.pk}}' data-action='delete' data-target='#crewmember'><span class="fas fa-times"></span></button</td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{crew.pk}}' data-action='delete' data-target='#crewmember'><span class="fas fa-times"></span></button></td>
</tr>
{% endfor %}
</tbody>
@@ -240,7 +240,7 @@
<div class="row my-3" id="size-0">
<div class="col-12">
<div class="card border-success">
<div class="card-header">Electrical Checks <small>for Small TEC Events <6kVA (aprox. 26A)</small></div>
<div class="card-header">Electrical Checks <small>for Small TEC Events <6kVA (approx. 26A)</small></div>
<div class="card-body">
{% include 'partials/checklist_checkbox.html' with formitem=form.rcds %}
{% include 'partials/checklist_checkbox.html' with formitem=form.supply_test %}
@@ -337,7 +337,7 @@
</tbody>
</table>
</div>
<hr>
<hr/>
{% include 'partials/checklist_checkbox.html' with formitem=form.all_rcds_tested %}
{% include 'partials/checklist_checkbox.html' with formitem=form.public_sockets_tested %}
{% include 'partials/ec_power_info.html' %}

View File

@@ -38,7 +38,7 @@
<dt class="col-sm-6">Email</dt>
<dd class="col-sm-6">{{ object.person.email|linkornone:'mailto' }}</dd>
<dt class="col-sm-6">Phone Number</dt>
<dd class="col-sm-6">{{ object.person.phone|linkornone:'tel' }}</a></dd>
<dd class="col-sm-6">{{ object.person.phone|linkornone:'tel' }}</dd>
</dl>
</div>
</div>
@@ -59,7 +59,7 @@
<dt class="col-sm-6">Email</dt>
<dd class="col-sm-6">{{ object.organisation.email|linkornone:'mailto' }}</dd>
<dt class="col-sm-6">Phone Number</dt>
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</a></dd>
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</dd>
<dt class="col-sm-6">Has SU Account</dt>
<dd class="col-sm-6">{{ event.organisation.union_account|yesno|capfirst }}</dd>
</dl>

View File

@@ -2,14 +2,14 @@
{% load static %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<a href="/">
<span class="source"> R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></span>
</a>
</div>
<div class="row">
<div class="col-sm-12">
<a href="/">
<span class="source"> R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></span>
</a>
</div>
<div class="col-sm-12">
<div class="col-sm-12">
<span class="pull-right">
{% if object.mic %}
<div class="text-center">
@@ -20,82 +20,81 @@
{% endif %}
</span>
<h3>
<a href="{% url 'event_detail' object.pk %}">
{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
| {{ object.name }} </a>
<h3>
<a href="{% url 'event_detail' object.pk %}">
{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
| {{ object.name }} </a>
{% if object.venue %}
<small>at {{ object.venue }}</small>
<small>at {{ object.venue }}</small>
{% endif %}
<br/><small>
{{ object.start_date|date:"D d/m/Y" }}
{% if object.has_start_time %}
{{ object.start_time|date:"H:i" }}
{% endif %}
{% if object.end_date or object.has_end_time %}
&ndash;
{% endif %}
{% if object.end_date and object.end_date != object.start_date %}
{{ object.end_date|date:"D d/m/Y" }}
{% endif %}
{% if object.has_end_time %}
{{ object.end_time|date:"H:i" }}
{% endif %}
<br/><small>
{{ object.start_date|date:"D d/m/Y" }}
{% if object.has_start_time %}
{{ object.start_time|date:"H:i" }}
{% endif %}
{% if object.end_date or object.has_end_time %}
&ndash;
{% endif %}
{% if object.end_date and object.end_date != object.start_date %}
{{ object.end_date|date:"D d/m/Y" }}
{% endif %}
{% if object.has_end_time %}
{{ object.end_time|date:"H:i" }}
{% endif %}
</small>
</h3>
</h3>
<div class="row">
<div class="col-xs-6">
<p>
<strong>Status:</strong>
{{ object.get_status_display }}
</p>
<p>
{% if object.is_rig %}
<strong>Client:</strong> {{ object.person.name }}
{% if object.organisation %}
for {{ object.organisation.name }}
<div class="row">
<div class="col-xs-6">
<p>
<strong>Status:</strong>
{{ object.get_status_display }}
</p>
<p>
{% if object.is_rig %}
<strong>Client:</strong> {{ object.person.name }}
{% if object.organisation %}
for {{ object.organisation.name }}
{% endif %}
{% if object.dry_hire %}(Dry Hire){% endif %}
{% else %}
<strong>Non-Rig</strong>
{% endif %}
{% if object.dry_hire %}(Dry Hire){% endif %}
{% else %}
<strong>Non-Rig</strong>
{% endif %}
</p>
<p>
<strong>MIC:</strong>
{% if object.mic %}
{{object.mic.name}}
{% else %}
None
{% endif %}
</p>
</div>
<div class="col-xs-6">
{% if object.meet_at %}
<p>
<strong>Crew meet:</strong>
{{ object.meet_at|date:"H:i" }} {{ object.meet_at|date:"(Y-m-d)" }}
</p>
{% endif %}
{% if object.access_at %}
<p>
<strong>Access at:</strong>
{{ object.access_at|date:"H:i" }} {{ object.access_at|date:"(Y-m-d)" }}
<strong>MIC:</strong>
{% if object.mic %}
{{object.mic.name}}
{% else %}
None
{% endif %}
</p>
{% endif %}
<p>
<strong>Last updated:</strong>
{{ object.last_edited_at }} by "{{ object.last_edited_by.initials }}"
</p>
</div>
<div class="col-xs-6">
{% if object.meet_at %}
<p>
<strong>Crew meet:</strong>
{{ object.meet_at|date:"H:i" }} {{ object.meet_at|date:"(Y-m-d)" }}
</p>
{% endif %}
{% if object.access_at %}
<p>
<strong>Access at:</strong>
{{ object.access_at|date:"H:i" }} {{ object.access_at|date:"(Y-m-d)" }}
</p>
{% endif %}
<p>
<strong>Last updated:</strong>
{{ object.last_edited_at }} by "{{ object.last_edited_by.initials }}"
</p>
</div>
</div>
{% if object.description %}
<p>
<strong>Description: </strong>
{{ object.description|linebreaksbr }}
</p>
{% endif %}
</div>
{% if object.description %}
<p>
<strong>Description: </strong>
{{ object.description|linebreaksbr }}
</p>
{% endif %}
</table>
</div>
</div>
{% endblock %}

View File

@@ -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();

View File

@@ -36,6 +36,7 @@
<p>Your event will not be booked until you complete this form.</p>
<p>TEC PA &amp; Lighting<br/>
<p>TEC PA &amp; Lighting</p>
<br/>
{% endblock %}

View File

@@ -60,10 +60,10 @@
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Date</td>
<th scope="col">Amount</td>
<th scope="col">Method</td>
<th scope="col"></td>
<th scope="col">Date</th>
<th scope="col">Amount</th>
<th scope="col">Method</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
@@ -73,7 +73,7 @@
<td>{{ payment.amount|floatformat:2 }}</td>
<td>{{ payment.get_method_display }}</td>
<td>
<a href="{% url 'payment_delete' payment.pk %}" class="btn btn-small btn-danger"><span class="fas fa-times"</a>
<a href="{% url 'payment_delete' payment.pk %}" class="btn btn-small btn-danger"><span class="fas fa-times"></span></a>
</td>
</tr>
{% endfor %}

View File

@@ -27,7 +27,7 @@
<span class="text-muted">{% if invoice.void %}(VOID){% elif invoice.is_closed %}(PAID){% else %}(O/S){% endif %}</span></th>
<td><a href="{% url 'event_detail' invoice.event.pk %}">N{{ invoice.event.pk|stringformat:"05d" }}</a>: {{ invoice.event.name }} <br>
<span class="text-muted">{{ invoice.event.get_status_display }}{% if not invoice.event.mic %}, No MIC{% endif %}
</span></td>
</span>
</td>
<td>{% if invoice.event.organisation %}
{{ invoice.event.organisation.name }}

View File

@@ -20,6 +20,7 @@
<tr>
<td>300</td>
<td>167</td>
</tr>
<tr>
<td>500</td>
<td>100</td>

View File

@@ -5,8 +5,8 @@
{% load linkornone from filters %}
{% block content %}
<div class="row py-3">
<div class="col-12">
<div class="row py-3">
<div class="col-12">
<h3>Risk Assessment for Event N{{ object.event.pk|stringformat:"05d" }} {{ object.event.name }}</h3>
<div class="card card-default mb-3">
<div class="card-header">General</div>
@@ -152,13 +152,12 @@
</div>
</div>
</div>
<div class="col-12 text-right">
<a href="{% url 'ra_edit' object.pk %}" class="btn btn-warning my-3"><span class="fas fa-edit"></span> <span
class="d-none d-sm-inline">Edit</span></a>
<a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View Event</a>
{{ object.review_string|safe }}
{% include 'partials/last_edited.html' with target="riskassessment_history" %}
</div>
</div>
<div class="col-12 text-right">
<a href="{% url 'ra_edit' object.pk %}" class="btn btn-warning my-3"><span class="fas fa-edit"></span> <span
class="d-none d-sm-inline">Edit</span></a>
<a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary"><span class="fas fa-eye"></span> View Event</a>
{{ object.review_string|safe }}
{% include 'partials/last_edited.html' with target="riskassessment_history" %}
</div>
</div>
{% endblock %}

View File

@@ -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));

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -4,7 +4,7 @@
"scripts": {
"postdeploy": "python manage.py migrate && python manage.py generateSampleData"
},
"stack": "heroku-18",
"stack": "heroku-20",
"env": {
"DEBUG": {
"required": true
@@ -47,6 +47,9 @@
"heroku-postgresql"
],
"buildpacks": [
{
"url": "heroku/nodejs"
},
{
"url": "heroku/python"
}

View File

@@ -1,5 +1,6 @@
from django.contrib import admin
from reversion.admin import VersionAdmin
from assets import models as assets

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