Compare commits

..

21 Commits

Author SHA1 Message Date
e3fc05772e Exclude tests from the coverage stats
Seems to be artifically deflating our stats
2021-01-26 13:42:58 +00:00
e38205b9e7 Oops, remove unused import 2021-01-25 22:45:02 +00:00
f2b9642772 Switch to gh-a artifact uploading instead of imgur 'hack'
For test failure screenshots. Happy now @mattysmith22? ;p
2021-01-25 22:27:21 +00:00
08939a0e1f Purge old .idea config 2021-01-25 22:18:15 +00:00
15b7f9c7c1 No Ruby compass bodge, no need for rubocop! 2021-01-25 22:17:37 +00:00
c67f7e1e54 Purge old vagrant config 2021-01-25 22:16:25 +00:00
4f268b3168 Cache python dependencies
Should majorly speedup parallelillelelised testing
2021-01-25 22:08:32 +00:00
0e7723828a Update python version in tests 2021-01-25 21:59:23 +00:00
c46a2c53f3 Run parallelised RIGS tests as one matrix job 2021-01-25 21:55:32 +00:00
b3617a74bf Define service in coveralls task 2021-01-25 21:44:05 +00:00
5806cbc72d Change python ver 2021-01-25 21:42:55 +00:00
ab2ff9f146 Fix whoops in requirements.txt 2021-01-25 21:39:44 +00:00
fd5575a818 You valid now? 2021-01-25 21:37:22 +00:00
402a1dd7f0 Tends to help if I push valid yaml 2021-01-25 21:29:32 +00:00
be7688aa75 Upgrade python deps 2021-01-25 18:14:49 +00:00
a472e414f7 Add basic calendar button test
Mainly to pickup on FullCalendar loading errors
2021-01-25 18:09:21 +00:00
618c02aa9c Attempt at parallelising tests where possible 2021-01-25 16:42:12 +00:00
3056b6ef71 Fix audit time check in asset audit test 2021-01-25 16:07:44 +00:00
548c960996 npm upgrade 2021-01-25 15:50:36 +00:00
2cc43dcdb7 Move some gulp deps to dev rather than prod 2021-01-25 15:39:51 +00:00
6db25fb56b Upgrade to heroku-20 stack 2021-01-25 15:39:04 +00:00
198 changed files with 40898 additions and 10347 deletions

32
.codeclimate.yml Normal file
View File

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

View File

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

2
.csslintrc Normal file
View File

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

1
.eslintignore Normal file
View File

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

213
.eslintrc Normal file
View File

@@ -0,0 +1,213 @@
ecmaFeatures:
modules: true
jsx: true
env:
amd: true
browser: true
es6: true
jquery: true
node: true
# http://eslint.org/docs/rules/
rules:
# Possible Errors
comma-dangle: [2, never]
no-cond-assign: 2
no-console: 0
no-constant-condition: 2
no-control-regex: 2
no-debugger: 2
no-dupe-args: 2
no-dupe-keys: 2
no-duplicate-case: 2
no-empty: 2
no-empty-character-class: 2
no-ex-assign: 2
no-extra-boolean-cast: 2
no-extra-parens: 0
no-extra-semi: 2
no-func-assign: 2
no-inner-declarations: [2, functions]
no-invalid-regexp: 2
no-irregular-whitespace: 2
no-negated-in-lhs: 2
no-obj-calls: 2
no-regex-spaces: 2
no-sparse-arrays: 2
no-unexpected-multiline: 2
no-unreachable: 2
use-isnan: 2
valid-jsdoc: 0
valid-typeof: 2
# Best Practices
accessor-pairs: 2
block-scoped-var: 0
complexity: [2, 6]
consistent-return: 0
curly: 0
default-case: 0
dot-location: 0
dot-notation: 0
eqeqeq: 2
guard-for-in: 2
no-alert: 2
no-caller: 2
no-case-declarations: 2
no-div-regex: 2
no-else-return: 0
no-empty-label: 2
no-empty-pattern: 2
no-eq-null: 2
no-eval: 2
no-extend-native: 2
no-extra-bind: 2
no-fallthrough: 2
no-floating-decimal: 0
no-implicit-coercion: 0
no-implied-eval: 2
no-invalid-this: 0
no-iterator: 2
no-labels: 0
no-lone-blocks: 2
no-loop-func: 2
no-magic-number: 0
no-multi-spaces: 0
no-multi-str: 0
no-native-reassign: 2
no-new-func: 2
no-new-wrappers: 2
no-new: 2
no-octal-escape: 2
no-octal: 2
no-proto: 2
no-redeclare: 2
no-return-assign: 2
no-script-url: 2
no-self-compare: 2
no-sequences: 0
no-throw-literal: 0
no-unused-expressions: 2
no-useless-call: 2
no-useless-concat: 2
no-void: 2
no-warning-comments: 0
no-with: 2
radix: 2
vars-on-top: 0
wrap-iife: 2
yoda: 0
# Strict
strict: 0
# Variables
init-declarations: 0
no-catch-shadow: 2
no-delete-var: 2
no-label-var: 2
no-shadow-restricted-names: 2
no-shadow: 0
no-undef-init: 2
no-undef: 0
no-undefined: 0
no-unused-vars: 0
no-use-before-define: 0
# Node.js and CommonJS
callback-return: 2
global-require: 2
handle-callback-err: 2
no-mixed-requires: 0
no-new-require: 0
no-path-concat: 2
no-process-exit: 2
no-restricted-modules: 0
no-sync: 0
# Stylistic Issues
array-bracket-spacing: 0
block-spacing: 0
brace-style: 0
camelcase: 0
comma-spacing: 0
comma-style: 0
computed-property-spacing: 0
consistent-this: 0
eol-last: 0
func-names: 0
func-style: 0
id-length: 0
id-match: 0
indent: 0
jsx-quotes: 0
key-spacing: 0
linebreak-style: 0
lines-around-comment: 0
max-depth: 0
max-len: 0
max-nested-callbacks: 0
max-params: 0
max-statements: [2, 30]
new-cap: 0
new-parens: 0
newline-after-var: 0
no-array-constructor: 0
no-bitwise: 0
no-continue: 0
no-inline-comments: 0
no-lonely-if: 0
no-mixed-spaces-and-tabs: 0
no-multiple-empty-lines: 0
no-negated-condition: 0
no-nested-ternary: 0
no-new-object: 0
no-plusplus: 0
no-restricted-syntax: 0
no-spaced-func: 0
no-ternary: 0
no-trailing-spaces: 0
no-underscore-dangle: 0
no-unneeded-ternary: 0
object-curly-spacing: 0
one-var: 0
operator-assignment: 0
operator-linebreak: 0
padded-blocks: 0
quote-props: 0
quotes: 0
require-jsdoc: 0
semi-spacing: 0
semi: 0
sort-vars: 0
space-after-keywords: 0
space-before-blocks: 0
space-before-function-paren: 0
space-before-keywords: 0
space-in-parens: 0
space-infix-ops: 0
space-return-throw-case: 0
space-unary-ops: 0
spaced-comment: 0
wrap-regex: 0
# ECMAScript 6
arrow-body-style: 0
arrow-parens: 0
arrow-spacing: 0
constructor-super: 0
generator-star-spacing: 0
no-arrow-condition: 0
no-class-assign: 0
no-const-assign: 0
no-dupe-class-members: 0
no-this-before-super: 0
no-var: 0
object-shorthand: 0
prefer-arrow-callback: 0
prefer-const: 0
prefer-reflect: 0
prefer-spread: 0
prefer-template: 0
require-yield: 0

View File

@@ -8,52 +8,76 @@ on:
jobs: jobs:
build: build:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
runs-on: ubuntu-latest
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
strategy:
matrix:
test-group: ["RIGS.tests.test_unit RIGS.tests.test_models RIGS.tests.test_functional", "versioning.tests.test_versioning", "users.tests.test_users"]
parallel: [true]
include:
- test-group: "assets.tests.test_assets"
parallel: false
- test-group: "RIGS.tests.test_interaction"
parallel: false
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.9 python-version: 3.9
- uses: actions/cache@v2 - name: Cache python deps
id: pcache
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
restore-keys: |
${{ runner.os }}-pipenv-
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install pipenv
pipenv install -d
# if: steps.pcache.outputs.cache-hit != 'true'
- name: Cache Static Files
id: static-cache
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: 'pipeline/built_assets' path: ${{ env.pythonLocation }}
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }} key: ${{ env.pythonLocation }}-${{ hashFiles('requirements.txt') }}
- uses: bahmutov/npm-install@v1 - name: Setup Chromedriver
if: steps.static-cache.outputs.cache-hit != 'true' if: \!${{ matrix.parallel }}
- run: node node_modules/gulp/bin/gulp build run: |
if: steps.static-cache.outputs.cache-hit != 'true' 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/
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install pycodestyle coverage coveralls django_coverage_plugin
pip install --upgrade --upgrade-strategy eager -r requirements.txt
python manage.py collectstatic --noinput
- name: Basic Checks - name: Basic Checks
run: | run: |
pipenv run pycodestyle . --exclude=migrations,node_modules pycodestyle . --exclude=migrations,importer*
pipenv run python manage.py check python manage.py check
pipenv run python manage.py makemigrations --check --dry-run python manage.py makemigrations --check --dry-run
pipenv run python manage.py collectstatic --noinput
- name: Run Tests - name: Run Tests
run: pipenv run pytest -n auto -vv --cov if: \!${{ matrix.parallel }}
run: coverage run manage.py test ${{ matrix.test-group }} --verbosity=2
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: failure() if: failure() # Screenshots only make sense for the non-parallel, i.e. interaction tests anyway
with: with:
name: failure-screenshots ${{ matrix.test-group }} name: failure-screenshots ${{ matrix.test-group }}
path: screenshots/ path: screenshots/
retention-days: 5 retention-days: 5
- name: Coveralls - name: Run Tests (Parallel)
run: pipenv run coveralls --service=github if: ${{ matrix.parallel }}
run: coverage run manage.py test ${{ matrix.test-group }} --parallel --verbosity=2
- name: Upload Coverage
run: coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: ${{ matrix.test-group }}
COVERALLS_PARALLEL: true
coveralls:
name: Indicate completion to coveralls.io
needs: build
runs-on: ubuntu-latest
container: python:3-slim
steps:
- name: Finished
run: |
pip3 install --upgrade coveralls
coveralls --service=github --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

14
.gitignore vendored
View File

@@ -68,9 +68,19 @@ target/
## Directory-based project format: ## Directory-based project format:
.idea/ .idea/
# if you remove the above rule, at least ignore the following:
#Built dependencies # User-specific stuff:
pipeline/built_assets # .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle: # Gradle:
# .idea/gradle.xml # .idea/gradle.xml

View File

@@ -1,6 +1,3 @@
*.sqlite3 *.sqlite3
*.scss
*.md *.md
**/tests
conftest.py
pytest.ini
Dockerfile

101
Pipfile
View File

@@ -1,101 +0,0 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
ansicolors = "~=1.1.8"
asgiref = "~=3.3.1"
"backports.tempfile" = "~=1.0"
"backports.weakref" = "~=1.0.post1"
beautifulsoup4 = "~=4.9.3"
Brotli = "~=1.0.9"
cachetools = "~=4.2.1"
certifi = "~=2020.12.5"
chardet = "~=4.0.0"
configparser = "~=5.0.1"
contextlib2 = "~=0.6.0.post1"
cssselect = "~=1.1.0"
cssutils = "~=1.0.2"
dj-database-url = "~=0.5.0"
dj-static = "~=0.0.6"
Django = "~=3.1.5"
django-debug-toolbar = "~=3.2"
django-filter = "~=2.4.0"
django-ical = "~=1.7.1"
django-recaptcha = "~=2.0.6"
django-recurrence = "~=1.10.3"
django-registration-redux = "~=2.9"
django-reversion = "~=3.0.9"
django-toolbelt = "~=0.0.1"
django-widget-tweaks = "~=1.4.8"
django-htmlmin = "~=0.11.0"
envparse = "~=0.2.0"
gunicorn = "~=20.0.4"
icalendar = "~=4.0.7"
idna = "~=2.10"
importlib-metadata = "~=3.4.0"
lxml = "~=4.6.2"
Markdown = "~=3.3.3"
msgpack = "~=1.0.2"
pep517 = "~=0.9.1"
Pillow = "~=8.1.0"
premailer = "~=3.7.0"
progress = "~=1.5"
psutil = "~=5.8.0"
psycopg2 = "~=2.8.6"
Pygments = "~=2.7.4"
pyparsing = "~=2.4.7"
PyPDF2 = "~=1.26.0"
PyPOM = "~=2.2.0"
python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21"
pytz = "~=2020.5"
reportlab = "~=3.5.59"
requests = "~=2.25.1"
retrying = "~=1.3.3"
simplejson = "~=3.17.2"
six = "~=1.15.0"
soupsieve = "~=2.1"
sqlparse = "~=0.4.1"
static3 = "~=0.7.0"
svg2rlg = "~=0.3"
tini = "~=3.0.1"
tornado = "~=6.1"
urllib3 = "~=1.26.2"
whitenoise = "~=5.2.0"
yolk = "~=0.4.3"
"z3c.rml" = "~=4.1.2"
zipp = "~=3.4.0"
"zope.component" = "~=4.6.2"
"zope.deferredimport" = "~=4.3.1"
"zope.deprecation" = "~=4.4.0"
"zope.event" = "~=4.5.0"
"zope.hookable" = "~=5.0.1"
"zope.interface" = "~=5.2.0"
"zope.proxy" = "~=4.3.5"
"zope.schema" = "~=6.0.1"
sentry-sdk = "*"
diff-match-patch = "*"
[dev-packages]
selenium = "~=3.141.0"
pycodestyle = "*"
coveralls = "*"
django-coverage-plugin = "*"
pytest-cov = "*"
pytest-django = "*"
pluggy = "*"
pytest-splinter = "*"
pytest = "*"
[requires]
python_version = "3.9"
[dev-packages.pytest-xdist]
extras = [ "psutil",]
version = "*"
[dev-packages.PyPOM]
extras = [ "splinter",]
version = "*"

1544
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -8,22 +8,27 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/ https://docs.djangoproject.com/en/1.7/ref/settings/
""" """
import datetime # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
from pathlib import Path import os
import raven
import secrets import secrets
import datetime
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from envparse import env from envparse import env
# Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = os.path.dirname(os.path.dirname(__file__))
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e') SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG', cast=bool, default=True) DEBUG = env('DEBUG', cast=bool, default=True)
STAGING = env('STAGING', cast=bool, default=False) STAGING = env('STAGING', cast=bool, default=False)
CI = env('CI', cast=bool, default=False) CI = env('CI', cast=bool, default=False)
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com'] ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
@@ -49,7 +54,6 @@ if DEBUG:
# Application definition # Application definition
INSTALLED_APPS = ( INSTALLED_APPS = (
'whitenoise.runserver_nostatic',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@@ -67,11 +71,12 @@ INSTALLED_APPS = (
'reversion', 'reversion',
'captcha', 'captcha',
'widget_tweaks', 'widget_tweaks',
'raven.contrib.django.raven_compat',
) )
MIDDLEWARE = ( MIDDLEWARE = (
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware',
'reversion.middleware.RevisionMiddleware', 'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
@@ -80,8 +85,6 @@ MIDDLEWARE = (
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'htmlmin.middleware.HtmlMinifyMiddleware',
'htmlmin.middleware.MarkRequestMiddleware',
) )
ROOT_URLCONF = 'PyRIGS.urls' ROOT_URLCONF = 'PyRIGS.urls'
@@ -89,10 +92,11 @@ ROOT_URLCONF = 'PyRIGS.urls'
WSGI_APPLICATION = 'PyRIGS.wsgi.application' WSGI_APPLICATION = 'PyRIGS.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': str(BASE_DIR / 'db.sqlite3'), 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
} }
} }
@@ -170,12 +174,9 @@ else:
} }
} }
# Error/performance monitoring RAVEN_CONFIG = {
sentry_sdk.init( 'dsn': env('RAVEN_DSN', default=""),
dsn=env('SENTRY_DSN', default=""), }
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
# User system # User system
AUTH_USER_MODEL = 'RIGS.Profile' AUTH_USER_MODEL = 'RIGS.Profile'
@@ -228,18 +229,18 @@ USE_TZ = True
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S') DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = str(BASE_DIR / 'static/') STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
STATICFILES_DIRS = [ STATIC_DIRS = (
str(BASE_DIR / 'pipeline/built_assets'), os.path.join(BASE_DIR, 'static/')
] )
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ 'DIRS': [
BASE_DIR / 'templates' os.path.join(BASE_DIR, 'templates')
], ],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {

View File

@@ -1,28 +1,28 @@
import os
import pathlib
import sys
from datetime import datetime
import pytz
from django.conf import settings
from django.test import LiveServerTestCase from django.test import LiveServerTestCase
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from RIGS import models as rigsmodels from RIGS import models as rigsmodels
from . import pages from . import pages
import os
from pytest_django.asserts import assertContains import pytz
from datetime import date, time, datetime, timedelta
from django.conf import settings
import PyRIGS.settings
import sys
import pathlib
import inspect
def create_datetime(year, month, day, hour, minute): def create_datetime(year, month, day, hour, min):
tz = pytz.timezone(settings.TIME_ZONE) tz = pytz.timezone(settings.TIME_ZONE)
return tz.localize(datetime(year, month, day, hour, minute)).astimezone(tz) return tz.localize(datetime(year, month, day, hour, min)).astimezone(pytz.utc)
def create_browser(): def create_browser():
options = webdriver.ChromeOptions() options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080") options.add_argument("--window-size=1920,1080")
# No caching, please and thank you
options.add_argument("--aggressive-cache-discard")
options.add_argument("--disk-cache-size=0")
options.add_argument("--headless") options.add_argument("--headless")
if settings.CI: if settings.CI:
options.add_argument("--no-sandbox") options.add_argument("--no-sandbox")
@@ -34,7 +34,6 @@ class BaseTest(LiveServerTestCase):
def setUp(self): def setUp(self):
super().setUpClass() super().setUpClass()
self.driver = create_browser() self.driver = create_browser()
self.wait = WebDriverWait(self.driver, 15)
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
@@ -48,11 +47,10 @@ class AutoLoginTest(BaseTest):
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True) username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
self.profile.set_password("EventTestPassword") self.profile.set_password("EventTestPassword")
self.profile.save() self.profile.save()
login_page = pages.LoginPage(self.driver, self.live_server_url).open() loginPage = pages.LoginPage(self.driver, self.live_server_url).open()
login_page.login("EventTest", "EventTestPassword") loginPage.login("EventTest", "EventTestPassword")
# FIXME Refactor as a pytest fixture
def screenshot_failure(func): def screenshot_failure(func):
def wrapper_func(self, *args, **kwargs): def wrapper_func(self, *args, **kwargs):
try: try:
@@ -65,7 +63,6 @@ def screenshot_failure(func):
self.driver.save_screenshot(screenshot_file) self.driver.save_screenshot(screenshot_file)
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 raise e
return wrapper_func return wrapper_func
@@ -76,30 +73,12 @@ def screenshot_failure_cls(cls):
return cls return cls
def assert_times_almost_equal(first_time, second_time): # Checks if animation is done
assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0) class animation_is_finished():
def __call__(self, driver):
numberAnimating = driver.execute_script('return $(":animated").length')
def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url): finished = numberAnimating == 0
# Test the meta tag is in place if finished:
response = client.get(event_url, follow=True, HTTP_HOST='example.com') import time
assertContains(response, 'application/json+oembed') time.sleep(0.1)
assertContains(response, oembed_url) return finished
# Test that the JSON exists
response = client.get(oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assertContains(response, event_embed_url)
# Should also work for non-existant events
response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assertContains(response, alt_event_embed_url)
def login(client, django_user_model):
pwd = 'testuser'
usr = 'TestUser'
user = django_user_model.objects.create_user(username=usr, email="TestUser@test.com", password=pwd,
is_superuser=True,
is_active=True, is_staff=True)
assert client.login(username=usr, password=pwd)
return user

View File

@@ -1,8 +1,8 @@
from pypom import Page from pypom import Page, Region
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver import Chrome
from selenium.common.exceptions import NoSuchElementException
from PyRIGS.tests import regions from PyRIGS.tests import regions
@@ -44,7 +44,6 @@ class FormPage(BasePage):
submit = self.find_element(*self._submit_locator) submit = self.find_element(*self._submit_locator)
ActionChains(self.driver).move_to_element(submit).perform() ActionChains(self.driver).move_to_element(submit).perform()
submit.click() submit.click()
self.wait.until(animation_is_finished())
self.wait.until(lambda x: self.errors != previous_errors or self.success) self.wait.until(lambda x: self.errors != previous_errors or self.success)
@property @property
@@ -74,13 +73,3 @@ class LoginPage(BasePage):
password_element.send_keys(password) password_element.send_keys(password)
self.find_element(*self._submit_locator).click() self.find_element(*self._submit_locator).click()
class animation_is_finished():
def __call__(self, driver):
number_animating = driver.execute_script('return $(":animated").length')
finished = number_animating == 0
if finished:
import time
time.sleep(0.1)
return finished

View File

@@ -1,12 +1,14 @@
import datetime
from django.conf import settings
from pypom import Region from pypom import Region
from selenium.common.exceptions import NoSuchElementException from django.utils import timezone
from django.conf import settings
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions from selenium.webdriver.support import expected_conditions
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.select import Select from selenium.webdriver.support.select import Select
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import NoSuchElementException
import datetime
def parse_bool_from_string(string): def parse_bool_from_string(string):
@@ -71,11 +73,11 @@ class BootstrapSelectElement(Region):
self.find_element(*self._deselect_all_locator).click() self.find_element(*self._deselect_all_locator).click()
def search(self, query): def search(self, query):
# self.wait.until(expected_conditions.visibility_of_element_located(self._status_locator))
search_box = self.find_element(*self._search_locator) search_box = self.find_element(*self._search_locator)
self.open() self.open()
search_box.clear() search_box.clear()
search_box.send_keys(query) search_box.send_keys(query)
status_text = self.find_element(*self._status_locator)
self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator)) self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator))
@property @property

View File

@@ -1,145 +0,0 @@
import pytest
from django.core.management import call_command
from django.template.defaultfilters import striptags
from django.urls import URLPattern, URLResolver
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from pytest_django.asserts import assertRedirects, assertContains, assertNotContains
from pytest_django.asserts import assertTemplateUsed, assertInHTML
from PyRIGS import urls
from RIGS.models import Event
from assets.models import Asset
from django.db import connection
import pytest
from django.core.management import call_command
from django.template.defaultfilters import striptags
from django.urls.exceptions import NoReverseMatch
from RIGS.models import Event
from assets.models import Asset
from django.db import connection
from django.test import TestCase
from django.test.utils import override_settings
def find_urls_recursive(patterns):
urls_to_check = []
for url in patterns:
if isinstance(url, URLResolver):
urls_to_check += find_urls_recursive(url.url_patterns)
elif isinstance(url, URLPattern):
# Skip some things that actually don't need auth (mainly OEmbed JSONs that are essentially just a redirect)
if url.name is not None and url.name != "closemodal" and "json" not in str(url):
urls_to_check.append(url)
return urls_to_check
def get_request_url(url):
pattern = str(url.pattern)
try:
kwargz = {}
if ":pk>" in pattern:
kwargz['pk'] = 1
if ":model>" in pattern:
kwargz['model'] = "event"
return reverse(url.name, kwargs=kwargz)
except NoReverseMatch:
print("Couldn't test url " + pattern)
@pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData',
'deleteSampleData'])
def test_production_exception(command):
from django.core.management.base import CommandError
with pytest.raises(CommandError, match=".*production"):
call_command(command)
class TestSampleDataGenerator(TestCase):
@override_settings(DEBUG=True)
def test_sample_data(self):
call_command('generateSampleData')
assert Asset.objects.all().count() > 50
assert Event.objects.all().count() > 100
call_command('deleteSampleData')
assert Asset.objects.all().count() == 0
assert Event.objects.all().count() == 0
class TestSampleDataGenerator(TestCase):
@override_settings(DEBUG=True)
def setUp(self):
call_command('generateSampleData')
def test_unauthenticated(self): # Nothing should be available to the unauthenticated
for url in find_urls_recursive(urls.urlpatterns):
request_url = get_request_url(url)
if request_url and 'user' not in request_url: # User module is full of edge cases
response = self.client.get(request_url, follow=True, HTTP_HOST='example.com')
assertContains(response, 'Login')
if 'application/json+oembed' in response.content.decode():
assertTemplateUsed(response, 'login_redirect.html')
else:
if "embed" in str(url):
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
else:
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
assertRedirects(response, expected_url)
def test_page_titles(self):
assert self.client.login(username='superuser', password='superuser')
for url in filter((lambda u: "embed" not in u.name), find_urls_recursive(urls.urlpatterns)):
request_url = get_request_url(url)
response = self.client.get(request_url)
if hasattr(response, "context_data") and "page_title" in response.context_data:
expected_title = striptags(response.context_data["page_title"])
assertInHTML('<title>{} | Rig Information Gathering System'.format(expected_title),
response.content.decode())
print("{} | {}".format(request_url, expected_title)) # If test fails, tell me where!
self.client.logout()
def test_basic_access(self):
assert self.client.login(username="basic", password="basic")
url = reverse('asset_list')
response = self.client.get(url)
# Check edit and duplicate buttons NOT shown in list
assertNotContains(response, 'Edit')
assertNotContains(response,
'Duplicate') # If this line is randomly failing, check the debug toolbar HTML hasn't crept in
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = self.client.get(url)
assertNotContains(response, 'Purchase Details')
assertNotContains(response, 'View Revision History')
urlz = {'asset_history', 'asset_update', 'asset_duplicate'}
for url_name in urlz:
request_url = reverse(url_name, kwargs={'pk': Asset.objects.first().asset_id})
response = self.client.get(request_url, follow=True)
assert response.status_code == 403
request_url = reverse('supplier_create')
response = self.client.get(request_url, follow=True)
assert response.status_code == 403
request_url = reverse('supplier_update', kwargs={'pk': 1})
response = self.client.get(request_url, follow=True)
assert response.status_code == 403
self.client.logout()
def test_keyholder_access(self):
assert self.client.login(username="keyholder", password="keyholder")
url = reverse('asset_list')
response = self.client.get(url)
# Check edit and duplicate buttons shown in list
assertContains(response, 'Edit')
assertContains(response, 'Duplicate')
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = self.client.get(url)
assertContains(response, 'Purchase Details')
assertContains(response, 'View Revision History')
self.client.logout()

View File

@@ -1,11 +1,16 @@
from django.conf import settings
from django.conf.urls import include
from django.contrib import admin
from django.contrib.auth.decorators import login_required
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path from django.urls import path
from django.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.views.generic import TemplateView from django.views.generic import TemplateView
from PyRIGS.decorators import permission_required_with_403
import RIGS
import users
import versioning
from PyRIGS import views from PyRIGS import views
urlpatterns = [ urlpatterns = [
@@ -22,19 +27,18 @@ urlpatterns = [
name="api_secure"), name="api_secure"),
path('closemodal/', views.CloseModal.as_view(), name='closemodal'), path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'), path('search_help/', views.SearchHelp.as_view(), name='search_help'),
path('', include('users.urls')), path('', include('users.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
] ]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += staticfiles_urlpatterns() urlpatterns += staticfiles_urlpatterns()
import debug_toolbar import debug_toolbar
urlpatterns += [ urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)), url(r'^__debug__/', include(debug_toolbar.urls)),
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")), path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
] ] + urlpatterns

View File

@@ -1,28 +1,31 @@
import datetime
import operator
from functools import reduce
import simplejson
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.core import serializers
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db.models import Q from django.http.response import HttpResponseRedirect
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy, reverse, NoReverseMatch from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.views import generic from django.views import generic
from django.views.decorators.clickjacking import xframe_options_exempt from django.contrib.auth.views import LoginView
from django.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 from RIGS import models, forms
from assets import models as asset_models 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
def is_ajax(request): # Displays the current rig count along with a few other bits and pieces
return request.headers.get('x-requested-with') == 'XMLHttpRequest' class Index(generic.TemplateView):
class Index(generic.TemplateView): # Displays the current rig count along with a few other bits and pieces
template_name = 'index.html' template_name = 'index.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@@ -148,7 +151,7 @@ class SecureAPIRequest(generic.View):
class ModalURLMixin: class ModalURLMixin:
def get_close_url(self, update, detail): def get_close_url(self, update, detail):
if is_ajax(self.request): if self.request.is_ajax():
url = reverse_lazy('closemodal') url = reverse_lazy('closemodal')
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk})) update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object])) messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
@@ -167,7 +170,7 @@ class GenericListView(generic.ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(GenericListView, self).get_context_data(**kwargs) context = super(GenericListView, self).get_context_data(**kwargs)
context['page_title'] = self.model.__name__ + "s" context['page_title'] = self.model.__name__ + "s"
if is_ajax(self.request): if self.request.is_ajax():
context['override'] = "base_ajax.html" context['override'] = "base_ajax.html"
return context return context
@@ -199,7 +202,7 @@ class GenericDetailView(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(GenericDetailView, self).get_context_data(**kwargs) context = super(GenericDetailView, self).get_context_data(**kwargs)
context['page_title'] = "{} | {}".format(self.model.__name__, self.object.name) context['page_title'] = "{} | {}".format(self.model.__name__, self.object.name)
if is_ajax(self.request): if self.request.is_ajax():
context['override'] = "base_ajax.html" context['override'] = "base_ajax.html"
return context return context
@@ -210,7 +213,7 @@ class GenericUpdateView(generic.UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(GenericUpdateView, self).get_context_data(**kwargs) context = super(GenericUpdateView, self).get_context_data(**kwargs)
context['page_title'] = "Edit {}".format(self.model.__name__) context['page_title'] = "Edit {}".format(self.model.__name__)
if is_ajax(self.request): if self.request.is_ajax():
context['override'] = "base_ajax.html" context['override'] = "base_ajax.html"
return context return context
@@ -221,7 +224,7 @@ class GenericCreateView(generic.CreateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(GenericCreateView, self).get_context_data(**kwargs) context = super(GenericCreateView, self).get_context_data(**kwargs)
context['page_title'] = "Create {}".format(self.model.__name__) context['page_title'] = "Create {}".format(self.model.__name__)
if is_ajax(self.request): if self.request.is_ajax():
context['override'] = "base_ajax.html" context['override'] = "base_ajax.html"
return context return context
@@ -230,29 +233,15 @@ class SearchHelp(generic.TemplateView):
template_name = 'search_help.html' template_name = 'search_help.html'
"""
Called from a modal window (e.g. when an item is submitted to an event/invoice).
May optionally also include some javascript in a success message to cause a load of
the new information onto the page.
"""
class CloseModal(generic.TemplateView): class CloseModal(generic.TemplateView):
"""
Called from a modal window (e.g. when an item is submitted to an event/invoice).
May optionally also include some javascript in a success message to cause a load of
the new information onto the page.
"""
template_name = 'closemodal.html' template_name = 'closemodal.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return {'messages': messages.get_messages(self.request)} return {'messages': messages.get_messages(self.request)}
class OEmbedView(generic.View):
def get(self, request, pk=None):
embed_url = reverse(self.url_name, args=[pk])
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
data = {
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
'version': '1.0',
'type': 'rich',
'height': '250'
}
json = simplejson.JSONEncoderForHTML().encode(data)
return HttpResponse(json, content_type="application/json")

View File

@@ -1,7 +1,6 @@
# TEC PA & Lighting - PyRIGS # # TEC PA & Lighting - PyRIGS #
![Build Status](https://github.com/nottinghamtec/PyRIGS/workflows/Django%20CI/badge.svg) ![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) [![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS)
[![Maintainability](https://api.codeclimate.com/v1/badges/79ca3b8106911a1d143f/maintainability)](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability)
Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets. Welcome to TEC PA & 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,18 +1,19 @@
from django.contrib import admin from django.contrib import admin
from django.contrib import messages from RIGS import models, forms
from django.contrib.admin import helpers from users import forms as user_forms
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import Count
from django.forms import ModelForm
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from reversion import revisions as reversion
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from RIGS import models from django.contrib.admin import helpers
from users import forms as user_forms from django.template.response import TemplateResponse
from django.contrib import messages
from django.db import transaction
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count
from django.forms import ModelForm
from reversion import revisions as reversion
# Register your models here. # Register your models here.
admin.site.register(models.VatRate, VersionAdmin) admin.site.register(models.VatRate, VersionAdmin)

View File

@@ -1,21 +1,25 @@
import datetime import datetime
import re import re
import reversion
from django import forms
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.urls import reverse_lazy
from django.db.models import Q
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse
from django.views import generic from django.views import generic
from django.db.models import Q
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
from django.db.models import Q
from django.db import transaction
import reversion
from RIGS import models from RIGS import models
from django import forms
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'}) forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
@@ -67,6 +71,12 @@ class InvoicePrint(generic.View):
context = { context = {
'object': object, 'object': object,
'fonts': {
'opensans': {
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'invoice': invoice, 'invoice': invoice,
'current_user': request.user, 'current_user': request.user,
'filename': 'Invoice {} for {} {}.pdf'.format(invoice.display_id, object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name)) 'filename': 'Invoice {} for {} {}.pdf'.format(invoice.display_id, object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name))
@@ -92,8 +102,8 @@ class InvoiceVoid(generic.View):
object.save() object.save()
if object.void: if object.void:
return HttpResponseRedirect(reverse('invoice_list')) return HttpResponseRedirect(reverse_lazy('invoice_list'))
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': object.pk})) return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
class InvoiceDelete(generic.DeleteView): class InvoiceDelete(generic.DeleteView):
@@ -104,14 +114,14 @@ class InvoiceDelete(generic.DeleteView):
obj = self.get_object() obj = self.get_object()
if obj.payment_set.all().count() > 0: if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.') messages.info(self.request, 'To delete an invoice, delete the payments first.')
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk})) return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
return super(InvoiceDelete, self).get(pk) return super(InvoiceDelete, self).get(pk)
def post(self, request, pk): def post(self, request, pk):
obj = self.get_object() obj = self.get_object()
if obj.payment_set.all().count() > 0: if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.') messages.info(self.request, 'To delete an invoice, delete the payments first.')
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk})) return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
return super(InvoiceDelete, self).post(pk) return super(InvoiceDelete, self).post(pk)
def get_success_url(self): def get_success_url(self):
@@ -166,17 +176,16 @@ class InvoiceWaiting(generic.ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(InvoiceWaiting, self).get_context_data(**kwargs) context = super(InvoiceWaiting, self).get_context_data(**kwargs)
total = 0 total = 0
objects = self.get_queryset() for obj in self.get_objects():
for obj in objects:
total += obj.sum_total total += obj.sum_total
context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(objects), total) context['page_title'] = "Events for Invoice ({} Events, £{:.2f})".format(len(self.get_objects()), total)
return context return context
def get_queryset(self): def get_queryset(self):
return self.get_objects() return self.get_objects()
def get_objects(self): def get_objects(self):
# TODO find a way to select items # @todo find a way to select items
events = self.model.objects.filter( events = self.model.objects.filter(
( (
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
@@ -211,7 +220,7 @@ class InvoiceEvent(generic.View):
invoice.save() invoice.save()
messages.warning(self.request, 'Invoice voided') messages.warning(self.request, 'Invoice voided')
return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': invoice.pk})) return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))
class PaymentCreate(generic.CreateView): class PaymentCreate(generic.CreateView):
@@ -237,7 +246,7 @@ class PaymentCreate(generic.CreateView):
def get_success_url(self): def get_success_url(self):
messages.info(self.request, "location.reload()") messages.info(self.request, "location.reload()")
return reverse('closemodal') return reverse_lazy('closemodal')
class PaymentDelete(generic.DeleteView): class PaymentDelete(generic.DeleteView):

View File

@@ -1,11 +1,17 @@
from datetime import datetime
import simplejson
from django import forms from django import forms
from django.utils import formats
from django.conf import settings from django.conf import settings
from django.core import serializers from django.core import serializers
from django.utils import timezone from django.core.mail import EmailMessage, EmailMultiAlternatives
from 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 from reversion import revisions as reversion
import simplejson
from datetime import datetime
from django.utils import timezone
from RIGS import models from RIGS import models

View File

@@ -1,11 +1,11 @@
from django.contrib import messages from RIGS import models, forms
from django.views import generic
from django.utils import timezone
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone
from django.views import generic
from reversion import revisions as reversion from reversion import revisions as reversion
from django.db.models import AutoField, ManyToOneRel
from RIGS import models, forms from django.contrib import messages
class EventRiskAssessmentCreate(generic.CreateView): class EventRiskAssessmentCreate(generic.CreateView):
@@ -76,9 +76,6 @@ class EventRiskAssessmentList(generic.ListView):
model = models.RiskAssessment model = models.RiskAssessment
template_name = 'hs_object_list.html' template_name = 'hs_object_list.html'
def get_queryset(self):
return self.model.objects.order_by('reviewed_at').select_related('event')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(EventRiskAssessmentList, self).get_context_data(**kwargs) context = super(EventRiskAssessmentList, self).get_context_data(**kwargs)
context['title'] = 'Risk Assessment' context['title'] = 'Risk Assessment'
@@ -86,6 +83,7 @@ class EventRiskAssessmentList(generic.ListView):
context['edit'] = 'ra_edit' context['edit'] = 'ra_edit'
context['review'] = 'ra_review' context['review'] = 'ra_review'
context['perm'] = 'perms.RIGS.review_riskassessment' context['perm'] = 'perms.RIGS.review_riskassessment'
context['fields'] = [n.name for n in list(self.model._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
return context return context
@@ -189,6 +187,7 @@ class EventChecklistList(generic.ListView):
context['edit'] = 'ec_edit' context['edit'] = 'ec_edit'
context['review'] = 'ec_review' context['review'] = 'ec_review'
context['perm'] = 'perms.RIGS.review_eventchecklist' context['perm'] = 'perms.RIGS.review_eventchecklist'
context['fields'] = [n.name for n in list(self.model._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
return context return context
@@ -210,7 +209,7 @@ class HSList(generic.ListView):
template_name = 'hs_list.html' template_name = 'hs_list.html'
def get_queryset(self): def get_queryset(self):
return models.Event.objects.all().order_by('-start_date').select_related('riskassessment').prefetch_related('checklists') return models.Event.objects.all().order_by('-start_date')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(HSList, self).get_context_data(**kwargs) context = super(HSList, self).get_context_data(**kwargs)

View File

@@ -1,11 +1,12 @@
import datetime from RIGS import models, forms
import pytz
from django.conf import settings
from django.db.models import Q
from django_ical.views import ICalFeed from django_ical.views import ICalFeed
from django.db.models import Q
from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.utils import timezone
from django.conf import settings
from RIGS import models import datetime
import pytz
class CalendarICS(ICalFeed): class CalendarICS(ICalFeed):

View File

@@ -1,37 +0,0 @@
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group
from assets import models
from RIGS import models as rigsmodels
class Command(BaseCommand):
help = 'Deletes testing sample data'
def handle(self, *args, **kwargs):
from django.conf import settings
if not settings.DEBUG:
raise CommandError('You cannot run this command in production')
self.delete_objects(models.AssetCategory)
self.delete_objects(models.AssetStatus)
self.delete_objects(models.Supplier)
self.delete_objects(models.Connector)
self.delete_objects(models.Asset)
self.delete_objects(rigsmodels.VatRate)
self.delete_objects(rigsmodels.Profile)
self.delete_objects(rigsmodels.Person)
self.delete_objects(rigsmodels.Organisation)
self.delete_objects(rigsmodels.Venue)
self.delete_objects(Group)
self.delete_objects(rigsmodels.Event)
self.delete_objects(rigsmodels.EventItem)
self.delete_objects(rigsmodels.Invoice)
self.delete_objects(rigsmodels.Payment)
self.delete_objects(rigsmodels.RiskAssessment)
self.delete_objects(rigsmodels.EventChecklist)
def delete_objects(self, model):
for obj in model.objects.all():
obj.delete()

View File

@@ -1,7 +1,5 @@
from django.core.management.base import BaseCommand, CommandError
from django.core.management import call_command from django.core.management import call_command
from django.core.management.base import BaseCommand
from RIGS import models
class Command(BaseCommand): class Command(BaseCommand):
@@ -9,6 +7,5 @@ class Command(BaseCommand):
can_import_settings = True can_import_settings = True
def handle(self, *args, **options): def handle(self, *args, **options):
call_command('generateSampleUserData')
call_command('generateSampleRIGSData') call_command('generateSampleRIGSData')
call_command('generateSampleAssetsData') call_command('generateSampleAssetsData')

View File

@@ -1,12 +1,11 @@
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group, Permission
from django.db import transaction
from reversion import revisions as reversion
import datetime import datetime
import random import random
from django.contrib.auth.models import Group, Permission
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
from reversion import revisions as reversion
from RIGS import models from RIGS import models
@@ -17,8 +16,11 @@ class Command(BaseCommand):
people = [] people = []
organisations = [] organisations = []
venues = [] venues = []
events = [] profiles = []
profiles = models.Profile.objects.all()
keyholder_group = None
finance_group = None
hs_group = None
def handle(self, *args, **options): def handle(self, *args, **options):
from django.conf import settings from django.conf import settings
@@ -31,12 +33,20 @@ class Command(BaseCommand):
with transaction.atomic(): with transaction.atomic():
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
self.setup_people()
self.setup_organisations()
self.setup_venues()
self.setup_events()
def setup_people(self): self.setupGenericProfiles()
self.setupPeople()
self.setupOrganisations()
self.setupVenues()
self.setupGroups()
self.setupEvents()
self.setupUsefulProfiles()
def setupPeople(self):
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe", names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe",
"Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore", "Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore",
"Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan", "Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan",
@@ -51,25 +61,25 @@ class Command(BaseCommand):
"Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa "Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
for i, name in enumerate(names): for i, name in enumerate(names):
with reversion.create_revision(): with reversion.create_revision():
reversion.set_user(random.choice(models.Profile.objects.all())) reversion.set_user(random.choice(self.profiles))
person = models.Person.objects.create(name=name)
newPerson = models.Person.objects.create(name=name)
if i % 3 == 0: if i % 3 == 0:
person.email = "address@person.com" newPerson.email = "address@person.com"
if i % 5 == 0: if i % 5 == 0:
person.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" newPerson.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0: if i % 7 == 0:
person.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567" newPerson.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0: if i % 9 == 0:
person.phone = "01234 567894" newPerson.phone = "01234 567894"
person.save() newPerson.save()
self.people.append(person) self.people.append(newPerson)
def setup_organisations(self): def setupOrganisations(self):
names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars",
"ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc",
"Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp",
@@ -98,28 +108,27 @@ class Command(BaseCommand):
"Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
for i, name in enumerate(names): for i, name in enumerate(names):
with reversion.create_revision(): with reversion.create_revision():
reversion.set_user(random.choice(models.Profile.objects.all())) reversion.set_user(random.choice(self.profiles))
new_organisation = models.Organisation.objects.create(name=name) newOrganisation = models.Organisation.objects.create(name=name)
if i % 2 == 0: if i % 2 == 0:
new_organisation.has_su_account = True newOrganisation.has_su_account = True
if i % 3 == 0: if i % 3 == 0:
new_organisation.email = "address@organisation.com" newOrganisation.email = "address@organisation.com"
if i % 5 == 0: if i % 5 == 0:
new_organisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0: if i % 7 == 0:
new_organisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567" newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0: if i % 9 == 0:
new_organisation.phone = "01234 567894" newOrganisation.phone = "01234 567894"
new_organisation.save() newOrganisation.save()
self.organisations.append(new_organisation) self.organisations.append(newOrganisation)
def setup_venues(self): def setupVenues(self):
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch", names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch",
"The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands", "The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands",
"The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins", "The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins",
@@ -135,27 +144,108 @@ class Command(BaseCommand):
for i, name in enumerate(names): for i, name in enumerate(names):
with reversion.create_revision(): with reversion.create_revision():
reversion.set_user(random.choice(self.profiles)) reversion.set_user(random.choice(self.profiles))
new_venue = models.Venue.objects.create(name=name) newVenue = models.Venue.objects.create(name=name)
if i % 2 == 0: if i % 2 == 0:
new_venue.three_phase_available = True newVenue.three_phase_available = True
if i % 3 == 0: if i % 3 == 0:
new_venue.email = "address@venue.com" newVenue.email = "address@venue.com"
if i % 5 == 0: if i % 5 == 0:
new_venue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" newVenue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0: if i % 7 == 0:
new_venue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567" newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0: if i % 9 == 0:
new_venue.phone = "01234 567894" newVenue.phone = "01234 567894"
new_venue.save() newVenue.save()
self.venues.append(new_venue) self.venues.append(newVenue)
def setup_events(self): def setupGroups(self):
self.keyholder_group = Group.objects.create(name='Keyholders')
self.finance_group = Group.objects.create(name='Finance')
self.hs_group = Group.objects.create(name='H&S')
keyholderPerms = ["add_event", "change_event", "view_event",
"add_eventitem", "change_eventitem", "delete_eventitem",
"add_organisation", "change_organisation", "view_organisation",
"add_person", "change_person", "view_person", "view_profile",
"add_venue", "change_venue", "view_venue",
"add_asset", "change_asset", "delete_asset",
"view_asset", "view_supplier", "change_supplier", "asset_finance",
"add_supplier", "view_cabletype", "change_cabletype",
"add_cabletype", "view_eventchecklist", "change_eventchecklist",
"add_eventchecklist", "view_riskassessment", "change_riskassessment",
"add_riskassessment", "add_eventchecklistcrew", "change_eventchecklistcrew",
"delete_eventchecklistcrew", "view_eventchecklistcrew", "add_eventchecklistvehicle",
"change_eventchecklistvehicle",
"delete_eventchecklistvehicle", "view_eventchecklistvehicle", ]
financePerms = keyholderPerms + ["add_invoice", "change_invoice", "view_invoice",
"add_payment", "change_payment", "delete_payment"]
hsPerms = keyholderPerms + ["review_riskassessment", "review_eventchecklist"]
for permId in keyholderPerms:
self.keyholder_group.permissions.add(Permission.objects.get(codename=permId))
for permId in financePerms:
self.finance_group.permissions.add(Permission.objects.get(codename=permId))
for permId in hsPerms:
self.hs_group.permissions.add(Permission.objects.get(codename=permId))
def setupGenericProfiles(self):
names = ["Clara Oswin Oswald", "Rory Williams", "Amy Pond", "River Song", "Martha Jones", "Donna Noble",
"Jack Harkness", "Mickey Smith", "Rose Tyler"]
for i, name in enumerate(names):
newProfile = models.Profile.objects.create(username=name.replace(" ", ""), first_name=name.split(" ")[0],
last_name=name.split(" ")[-1],
email=name.replace(" ", "") + "@example.com",
initials="".join([j[0].upper() for j in name.split()]))
if i % 2 == 0:
newProfile.phone = "01234 567894"
newProfile.save()
self.profiles.append(newProfile)
def setupUsefulProfiles(self):
superUser = models.Profile.objects.create(username="superuser", first_name="Super", last_name="User",
initials="SU",
email="superuser@example.com", is_superuser=True, is_active=True,
is_staff=True)
superUser.set_password('superuser')
superUser.save()
financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User",
initials="FU",
email="financeuser@example.com", is_active=True, is_approved=True)
financeUser.groups.add(self.finance_group)
financeUser.groups.add(self.keyholder_group)
financeUser.set_password('finance')
financeUser.save()
hsUser = models.Profile.objects.create(username="hs", first_name="HS", last_name="User",
initials="HSU",
email="hsuser@example.com", is_active=True, is_approved=True)
hsUser.groups.add(self.hs_group)
hsUser.groups.add(self.keyholder_group)
hsUser.set_password('hs')
hsUser.save()
keyholderUser = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User",
initials="KU",
email="keyholderuser@example.com", is_active=True, is_approved=True)
keyholderUser.groups.add(self.keyholder_group)
keyholderUser.set_password('keyholder')
keyholderUser.save()
basicUser = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User", initials="BU",
email="basicuser@example.com", is_active=True, is_approved=True)
basicUser.set_password('basic')
basicUser.save()
def setupEvents(self):
names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball", names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball",
"Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event", "Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show", "End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show",
@@ -166,7 +256,7 @@ class Command(BaseCommand):
notes = ["The client came into the office at some point", "Who knows if this will happen", notes = ["The client came into the office at some point", "Who knows if this will happen",
"Probably should check this event", "Maybe not happening", "Run away!"] "Probably should check this event", "Maybe not happening", "Run away!"]
item_options = [ itemOptions = [
{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2, {'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2,
'cost': 200.00}, 'cost': 200.00},
{'name': 'Projector', {'name': 'Projector',
@@ -183,7 +273,7 @@ class Command(BaseCommand):
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00}, {'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}] {'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
day_delta = -120 # start adding events from 4 months ago dayDelta = -120 # start adding events from 4 months ago
for i in range(150): # Let's add 100 events for i in range(150): # Let's add 100 events
with reversion.create_revision(): with reversion.create_revision():
@@ -191,100 +281,65 @@ class Command(BaseCommand):
name = names[i % len(names)] name = names[i % len(names)]
start_date = datetime.date.today() + datetime.timedelta(days=day_delta) startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
day_delta = day_delta + random.randint(0, 3) dayDelta = dayDelta + random.randint(0, 3)
new_event = models.Event.objects.create(name=name, start_date=start_date) newEvent = models.Event.objects.create(name=name, start_date=startDate)
if random.randint(0, 2) > 1: # 1 in 3 have a start time if random.randint(0, 2) > 1: # 1 in 3 have a start time
new_event.start_time = datetime.time(random.randint(15, 20)) newEvent.start_time = datetime.time(random.randint(15, 20))
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
new_event.end_time = datetime.time(random.randint(21, 23)) newEvent.end_time = datetime.time(random.randint(21, 23))
elif random.randint(0, 1) > 0: # half of the others finish early the next day elif random.randint(0, 1) > 0: # half of the others finish early the next day
new_event.end_date = new_event.start_date + datetime.timedelta(days=1) newEvent.end_date = newEvent.start_date + datetime.timedelta(days=1)
new_event.end_time = datetime.time(random.randint(0, 5)) newEvent.end_time = datetime.time(random.randint(0, 5))
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
new_event.end_date = new_event.start_date + datetime.timedelta(days=random.randint(1, 4)) newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1, 4))
if random.randint(0, 6) > 0: # 5 in 6 have MIC if random.randint(0, 6) > 0: # 5 in 6 have MIC
new_event.mic = random.choice(self.profiles) newEvent.mic = random.choice(self.profiles)
if random.randint(0, 6) > 0: # 5 in 6 have organisation if random.randint(0, 6) > 0: # 5 in 6 have organisation
new_event.organisation = random.choice(self.organisations) newEvent.organisation = random.choice(self.organisations)
if random.randint(0, 6) > 0: # 5 in 6 have person if random.randint(0, 6) > 0: # 5 in 6 have person
new_event.person = random.choice(self.people) newEvent.person = random.choice(self.people)
if random.randint(0, 6) > 0: # 5 in 6 have venue if random.randint(0, 6) > 0: # 5 in 6 have venue
new_event.venue = random.choice(self.venues) newEvent.venue = random.choice(self.venues)
# Could have any status, equally weighted # Could have any status, equally weighted
new_event.status = random.choice( newEvent.status = random.choice(
[models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED]) [models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
new_event.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire newEvent.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire
if random.randint(0, 1) > 0: # 1 in 2 have description if random.randint(0, 1) > 0: # 1 in 2 have description
new_event.description = random.choice(descriptions) newEvent.description = random.choice(descriptions)
if random.randint(0, 1) > 0: # 1 in 2 have notes if random.randint(0, 1) > 0: # 1 in 2 have notes
new_event.notes = random.choice(notes) newEvent.notes = random.choice(notes)
new_event.save() newEvent.save()
# Now add some items # Now add some items
for j in range(random.randint(1, 5)): for j in range(random.randint(1, 5)):
item_data = item_options[random.randint(0, len(item_options) - 1)] itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data) newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
new_item.save() newItem.save()
while new_event.sum_total < 0: while newEvent.sum_total < 0:
item_data = item_options[random.randint(0, len(item_options) - 1)] itemData = itemOptions[random.randint(0, len(itemOptions) - 1)]
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data) newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
new_item.save() newItem.save()
with reversion.create_revision(): with reversion.create_revision():
reversion.set_user(random.choice(self.profiles)) reversion.set_user(random.choice(self.profiles))
if new_event.start_date < datetime.date.today(): # think about adding an invoice if newEvent.start_date < datetime.date.today(): # think about adding an invoice
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
new_invoice = models.Invoice.objects.create(event=new_event) newInvoice = models.Invoice.objects.create(event=newEvent)
if new_event.status is models.Event.CANCELLED: # void cancelled events if newEvent.status is models.Event.CANCELLED: # void cancelled events
new_invoice.void = True newInvoice.void = True
elif random.randint(0, 2) > 1: # 1 in 3 have been paid elif random.randint(0, 2) > 1: # 1 in 3 have been paid
models.Payment.objects.create(invoice=new_invoice, amount=new_invoice.balance, models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance,
date=datetime.date.today()) date=datetime.date.today())
if i == 1 or random.randint(0, 5) > 0: # Event 1 and 1 in 5 have a RA
models.RiskAssessment.objects.create(event=new_event, supervisor_consulted=bool(random.getrandbits(1)),
nonstandard_equipment=bool(random.getrandbits(1)),
nonstandard_use=bool(random.getrandbits(1)),
contractors=bool(random.getrandbits(1)),
other_companies=bool(random.getrandbits(1)),
crew_fatigue=bool(random.getrandbits(1)),
big_power=bool(random.getrandbits(1)),
generators=bool(random.getrandbits(1)),
other_companies_power=bool(random.getrandbits(1)),
nonstandard_equipment_power=bool(random.getrandbits(1)),
multiple_electrical_environments=bool(random.getrandbits(1)),
noise_monitoring=bool(random.getrandbits(1)),
known_venue=bool(random.getrandbits(1)),
safe_loading=bool(random.getrandbits(1)),
safe_storage=bool(random.getrandbits(1)),
area_outside_of_control=bool(random.getrandbits(1)),
barrier_required=bool(random.getrandbits(1)),
nonstandard_emergency_procedure=bool(random.getrandbits(1)),
special_structures=bool(random.getrandbits(1)),
suspended_structures=bool(random.getrandbits(1)),
outside=bool(random.getrandbits(1)))
if i == 0 or random.randint(0, 1) > 0: # Event 1 and 1 in 10 have a Checklist
models.EventChecklist.objects.create(event=new_event, 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,5 +1,5 @@
# Generated by Django 2.0.13 on 2020-01-11 18:29 # Generated by Django 2.0.13 on 2020-01-11 18:29
# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved # This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
from django.db import migrations from django.db import migrations
def approve_legacy(apps, schema_editor): def approve_legacy(apps, schema_editor):
@@ -15,5 +15,5 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RunPython(approve_legacy, migrations.RunPython.noop) migrations.RunPython(approve_legacy)
] ]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.1.5 on 2021-02-06 10:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0039_auto_20210123_1910'),
]
operations = [
migrations.AddField(
model_name='profile',
name='dark_theme',
field=models.BooleanField(default=False),
),
]

View File

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

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.1.7 on 2021-03-02 11:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0041_auto_20210208_1603'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='phone',
field=models.CharField(blank=True, default='', max_length=13),
),
]

View File

@@ -1,32 +1,34 @@
import datetime import datetime
import hashlib import hashlib
import random
import string
from collections import Counter
from decimal import Decimal
from urllib.parse import urlparse
import pytz import pytz
from django import forms from django import forms
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.contrib.auth.models import AbstractUser
from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from reversion import revisions as reversion from reversion import revisions as reversion
from reversion.models import Version from reversion.models import Version
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): class Profile(AbstractUser):
initials = models.CharField(max_length=5, unique=True, null=True, blank=False) initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
phone = models.CharField(max_length=13, blank=True, default='') phone = models.CharField(max_length=13, null=True, blank=True)
api_key = models.CharField(max_length=40, blank=True, editable=False, default='') api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
is_approved = models.BooleanField(default=False) is_approved = models.BooleanField(default=False)
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that... last_emailed = models.DateTimeField(blank=True,
last_emailed = models.DateTimeField(blank=True, null=True) null=True) # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
dark_theme = models.BooleanField(default=False)
@classmethod @classmethod
def make_api_key(cls): def make_api_key(cls):
@@ -52,7 +54,7 @@ class Profile(AbstractUser):
@property @property
def latest_events(self): def latest_events(self):
return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists') return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
@classmethod @classmethod
def admins(cls): def admins(cls):
@@ -103,12 +105,12 @@ class RevisionMixin(object):
class Person(models.Model, RevisionMixin): class Person(models.Model, RevisionMixin):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, default='') phone = models.CharField(max_length=15, blank=True, null=True)
email = models.EmailField(blank=True, default='') email = models.EmailField(blank=True, null=True)
address = models.TextField(blank=True, default='') address = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, null=True)
def __str__(self): def __str__(self):
string = self.name string = self.name
@@ -134,17 +136,17 @@ class Person(models.Model, RevisionMixin):
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic') return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('person_detail', kwargs={'pk': self.pk}) return reverse_lazy('person_detail', kwargs={'pk': self.pk})
class Organisation(models.Model, RevisionMixin): class Organisation(models.Model, RevisionMixin):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, default='') phone = models.CharField(max_length=15, blank=True, null=True)
email = models.EmailField(blank=True, default='') email = models.EmailField(blank=True, null=True)
address = models.TextField(blank=True, default='') address = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, null=True)
union_account = models.BooleanField(default=False) union_account = models.BooleanField(default=False)
def __str__(self): def __str__(self):
@@ -171,7 +173,7 @@ class Organisation(models.Model, RevisionMixin):
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic') return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('organisation_detail', kwargs={'pk': self.pk}) return reverse_lazy('organisation_detail', kwargs={'pk': self.pk})
class VatManager(models.Manager): class VatManager(models.Manager):
@@ -179,6 +181,7 @@ class VatManager(models.Manager):
return self.find_rate(timezone.now()) return self.find_rate(timezone.now())
def find_rate(self, date): def find_rate(self, date):
# return self.filter(startAt__lte=date).latest()
try: try:
return self.filter(start_at__lte=date).latest() return self.filter(start_at__lte=date).latest()
except VatRate.DoesNotExist: except VatRate.DoesNotExist:
@@ -211,12 +214,12 @@ class VatRate(models.Model, RevisionMixin):
class Venue(models.Model, RevisionMixin): class Venue(models.Model, RevisionMixin):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
phone = models.CharField(max_length=15, blank=True, default='') phone = models.CharField(max_length=15, blank=True, null=True)
email = models.EmailField(blank=True, default='') email = models.EmailField(blank=True, null=True)
three_phase_available = models.BooleanField(default=False) three_phase_available = models.BooleanField(default=False)
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, null=True)
address = models.TextField(blank=True, default='') address = models.TextField(blank=True, null=True)
def __str__(self): def __str__(self):
string = self.name string = self.name
@@ -229,23 +232,24 @@ class Venue(models.Model, RevisionMixin):
return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic') return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('venue_detail', kwargs={'pk': self.pk}) return reverse_lazy('venue_detail', kwargs={'pk': self.pk})
class EventManager(models.Manager): class EventManager(models.Manager):
def current_events(self): def current_events(self):
events = self.filter( events = self.filter(
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q( (models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q( (models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
status=Event.CANCELLED)) | # Ends after status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q( (models.Q(dry_hire=True, start_date__gte=timezone.now().date()) & ~models.Q(
status=Event.CANCELLED)) | # Active dry hire status=Event.CANCELLED)) | # Active dry hire
(models.Q(dry_hire=True, checked_in_by__isnull=True) & ( (models.Q(dry_hire=True, checked_in_by__isnull=True) & (
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
models.Q(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started models.Q(status=Event.CANCELLED, start_date__gte=timezone.now().date()) # Canceled but not started
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic') ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
'organisation',
'venue', 'mic')
return events return events
def events_in_bounds(self, start, end): def events_in_bounds(self, start, end):
@@ -268,12 +272,12 @@ class EventManager(models.Manager):
def rig_count(self): def rig_count(self):
event_count = self.filter( event_count = self.filter(
(models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False, (models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False,
is_rig=True) & ~models.Q( is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q( (models.Q(end_date__gte=timezone.now().date(), dry_hire=False, is_rig=True) & ~models.Q(
status=Event.CANCELLED)) | # Ends after status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q( (models.Q(dry_hire=True, start_date__gte=timezone.now().date(), is_rig=True) & ~models.Q(
status=Event.CANCELLED)) # Active dry hire status=Event.CANCELLED)) # Active dry hire
).count() ).count()
return event_count return event_count
@@ -297,8 +301,8 @@ class Event(models.Model, RevisionMixin):
person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE) person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE) organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE) venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
description = models.TextField(blank=True, default='') description = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, default='') notes = models.TextField(blank=True, null=True)
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL) status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
dry_hire = models.BooleanField(default=False) dry_hire = models.BooleanField(default=False)
is_rig = models.BooleanField(default=True) is_rig = models.BooleanField(default=True)
@@ -312,7 +316,7 @@ class Event(models.Model, RevisionMixin):
end_time = models.TimeField(blank=True, null=True) end_time = models.TimeField(blank=True, null=True)
access_at = models.DateTimeField(blank=True, null=True) access_at = models.DateTimeField(blank=True, null=True)
meet_at = models.DateTimeField(blank=True, null=True) meet_at = models.DateTimeField(blank=True, null=True)
meet_info = models.CharField(max_length=255, blank=True, default='') meet_info = models.CharField(max_length=255, blank=True, null=True)
# Crew management # Crew management
checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True, checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
@@ -321,15 +325,15 @@ class Event(models.Model, RevisionMixin):
verbose_name="MIC", on_delete=models.CASCADE) verbose_name="MIC", on_delete=models.CASCADE)
# Monies # Monies
payment_method = models.CharField(max_length=255, blank=True, default='') payment_method = models.CharField(max_length=255, blank=True, null=True)
payment_received = models.CharField(max_length=255, blank=True, default='') payment_received = models.CharField(max_length=255, blank=True, null=True)
purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO') purchase_order = models.CharField(max_length=255, blank=True, null=True, verbose_name='PO')
collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by') collector = models.CharField(max_length=255, blank=True, null=True, verbose_name='collected by')
# Authorisation request details # Authorisation request details
auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE) auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
auth_request_at = models.DateTimeField(null=True, blank=True) auth_request_at = models.DateTimeField(null=True, blank=True)
auth_request_to = models.EmailField(blank=True, default='') auth_request_to = models.EmailField(null=True, blank=True)
@property @property
def display_id(self): def display_id(self):
@@ -345,7 +349,7 @@ class Event(models.Model, RevisionMixin):
@property @property
def sum_total(self): def sum_total(self):
total = self.items.aggregate( total = EventItem.objects.filter(event=self).aggregate(
sum_total=models.Sum(models.F('cost') * models.F('quantity'), sum_total=models.Sum(models.F('cost') * models.F('quantity'),
output_field=models.DecimalField(max_digits=10, decimal_places=2)) output_field=models.DecimalField(max_digits=10, decimal_places=2))
)['sum_total'] )['sum_total']
@@ -455,7 +459,7 @@ class Event(models.Model, RevisionMixin):
objects = EventManager() objects = EventManager()
def get_absolute_url(self): def get_absolute_url(self):
return reverse('event_detail', kwargs={'pk': self.pk}) return reverse_lazy('event_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return "{}: {}".format(self.display_id, self.name) return "{}: {}".format(self.display_id, self.name)
@@ -489,7 +493,7 @@ class Event(models.Model, RevisionMixin):
class EventItem(models.Model, RevisionMixin): class EventItem(models.Model, RevisionMixin):
event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE) event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
description = models.TextField(blank=True, default='') description = models.TextField(blank=True, null=True)
quantity = models.IntegerField() quantity = models.IntegerField()
cost = models.DecimalField(max_digits=10, decimal_places=2) cost = models.DecimalField(max_digits=10, decimal_places=2)
order = models.IntegerField() order = models.IntegerField()
@@ -504,7 +508,7 @@ class EventItem(models.Model, RevisionMixin):
ordering = ['order'] ordering = ['order']
def __str__(self): def __str__(self):
return "{}.{}: {} | {}".format(self.event_id, self.order, self.event.name, self.name) return str(self.event.pk) + "." + str(self.order) + ": " + self.event.name + " | " + self.name
@property @property
def activity_feed_string(self): def activity_feed_string(self):
@@ -516,13 +520,13 @@ class EventAuthorisation(models.Model, RevisionMixin):
event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE) event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
email = models.EmailField() email = models.EmailField()
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID") uni_id = models.CharField(max_length=10, blank=True, null=True, verbose_name="University ID")
account_code = models.CharField(max_length=50, default='', blank=True) account_code = models.CharField(max_length=50, blank=True, null=True)
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount") amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE) sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('event_detail', kwargs={'pk': self.event_id}) return reverse_lazy('event_detail', kwargs={'pk': self.event.pk})
@property @property
def activity_feed_string(self): def activity_feed_string(self):
@@ -561,11 +565,11 @@ class Invoice(models.Model, RevisionMixin):
return self.balance == 0 or self.void return self.balance == 0 or self.void
def get_absolute_url(self): def get_absolute_url(self):
return reverse('invoice_detail', kwargs={'pk': self.pk}) return reverse_lazy('invoice_detail', kwargs={'pk': self.pk})
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return "#{} for Event {}".format(self.display_id, self.event.display_id) return "#{} for Event {}".format(self.display_id, "N%05d" % self.event.pk)
def __str__(self): def __str__(self):
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance) return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)
@@ -596,7 +600,7 @@ class Payment(models.Model, RevisionMixin):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE) invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
date = models.DateField() date = models.DateField()
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT') amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
method = models.CharField(max_length=2, choices=METHODS, default='', blank=True) method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True)
reversion_hide = True reversion_hide = True
@@ -631,9 +635,10 @@ class RiskAssessment(models.Model, RevisionMixin):
contractors = models.BooleanField(help_text="Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>") contractors = models.BooleanField(help_text="Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>")
other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>") other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>")
crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?") crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?")
general_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?") general_notes = models.TextField(blank=True, null=True, help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
# Power # Power
# event_size = models.IntegerField(blank=True, null=True, choices=SIZES)
big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?") big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
# If yes to the above two, you must answer... # If yes to the above two, you must answer...
power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True, power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
@@ -643,12 +648,12 @@ class RiskAssessment(models.Model, RevisionMixin):
other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?") other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?")
nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?") nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?")
multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?") multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?")
power_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?") power_notes = models.TextField(blank=True, null=True, help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
power_plan = models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url]) power_plan = models.URLField(blank=True, null=True, help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
# Sound # Sound
noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?") noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?")
sound_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?") sound_notes = models.TextField(blank=True, null=True, help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
# Site # Site
known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?") known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
@@ -661,8 +666,8 @@ class RiskAssessment(models.Model, RevisionMixin):
# Structures # Structures
special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?") special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?")
suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?") suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")
persons_responsible_structures = models.TextField(blank=True, default='', help_text="Who are the persons on site responsible for their use?") persons_responsible_structures = models.TextField(blank=True, null=True, help_text="Who are the persons on site responsible for their use?")
rigging_plan = models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url]) rigging_plan = models.URLField(blank=True, null=True, help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[validate_url])
# Blimey that was a lot of options # Blimey that was a lot of options
@@ -706,10 +711,6 @@ class RiskAssessment(models.Model, RevisionMixin):
('review_riskassessment', 'Can review Risk Assessments') ('review_riskassessment', 'Can review Risk Assessments')
] ]
@cached_property
def fieldz(self):
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
@property @property
def event_size(self): def event_size(self):
# Confirm event size. Check all except generators, since generators entails outside # Confirm event size. Check all except generators, since generators entails outside
@@ -725,7 +726,7 @@ class RiskAssessment(models.Model, RevisionMixin):
return str(self.event) return str(self.event)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('ra_detail', kwargs={'pk': self.pk}) return reverse_lazy('ra_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return "%i - %s" % (self.pk, self.event) return "%i - %s" % (self.pk, self.event)
@@ -748,8 +749,8 @@ class EventChecklist(models.Model, RevisionMixin):
trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?") trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?")
warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>") warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>")
ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?") ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?")
hs_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of Safety Bag/Box") hs_location = models.CharField(blank=True, null=True, max_length=255, help_text="Location of Safety Bag/Box")
extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers") extinguishers_location = models.CharField(blank=True, null=True, max_length=255, help_text="Location of fire extinguishers")
# Small Electrical Checks # Small Electrical Checks
rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?") rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
@@ -770,15 +771,15 @@ class EventChecklist(models.Model, RevisionMixin):
fd_earth_fault = models.IntegerField(blank=True, null=True, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)") fd_earth_fault = models.IntegerField(blank=True, null=True, verbose_name="Earth Fault Loop Impedance", help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current") fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text="Prospective Short Circuit Current")
# Worst case points # Worst case points
w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description") w1_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?") w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage") w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
w1_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)") w1_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description") w2_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?") w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage") w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
w2_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)") w2_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description") w3_description = models.CharField(blank=True, null=True, max_length=255, help_text="Description")
w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?") w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage") w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage")
w3_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)") w3_earth_fault = models.IntegerField(blank=True, null=True, help_text="Earth Fault Loop Impedance (Z<small>S</small>)")
@@ -798,16 +799,12 @@ class EventChecklist(models.Model, RevisionMixin):
('review_eventchecklist', 'Can review Event Checklists') ('review_eventchecklist', 'Can review Event Checklists')
] ]
@cached_property
def fieldz(self):
return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
@property @property
def activity_feed_string(self): def activity_feed_string(self):
return str(self.event) return str(self.event)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('ec_detail', kwargs={'pk': self.pk}) return reverse_lazy('ec_detail', kwargs={'pk': self.pk})
def __str__(self): def __str__(self):
return "%i - %s" % (self.pk, self.event) return "%i - %s" % (self.pk, self.event)

View File

@@ -1,34 +1,36 @@
import copy from io import BytesIO
import datetime import urllib.request
import re
import urllib.error import urllib.error
import urllib.parse import urllib.parse
import urllib.request
from io import BytesIO
import premailer from django.contrib.staticfiles.storage import staticfiles_storage
import simplejson from django.core.mail import EmailMessage, EmailMultiAlternatives
from PyPDF2 import PdfFileMerger, PdfFileReader from django.views import generic
from django.conf import settings
from django.contrib import messages
from django.contrib.staticfiles import finders
from django.core import signing
from django.core.exceptions import SuspiciousOperation
from django.core.mail import EmailMultiAlternatives
from django.db.models import Q
from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.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.template.loader import get_template
from django.conf import settings
from django.urls import reverse from django.urls import reverse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone 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.utils.decorators import method_decorator
from django.views import generic from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
from PyPDF2 import PdfFileMerger, PdfFileReader
import simplejson
import premailer
from PyRIGS import decorators
from PyRIGS.views import OEmbedView, is_ajax
from RIGS import models, forms from RIGS import models, forms
from PyRIGS import decorators
import datetime
import re
import copy
__author__ = 'ghost' __author__ = 'ghost'
@@ -41,7 +43,7 @@ class RigboardIndex(generic.TemplateView):
context = super(RigboardIndex, self).get_context_data(**kwargs) context = super(RigboardIndex, self).get_context_data(**kwargs)
# call out method to get current events # call out method to get current events
context['events'] = models.Event.objects.current_events().select_related('riskassessment', 'invoice').prefetch_related('checklists') context['events'] = models.Event.objects.current_events()
context['page_title'] = "Rigboard" context['page_title'] = "Rigboard"
return context return context
@@ -60,24 +62,29 @@ class EventDetail(generic.DetailView):
template_name = 'event_detail.html' template_name = 'event_detail.html'
model = models.Event model = models.Event
def get_context_data(self, **kwargs):
context = super(EventDetail, self).get_context_data(**kwargs) class EventOembed(generic.View):
title = "{} | {}".format(self.object.display_id, self.object.name) model = models.Event
if self.object.dry_hire:
title += " <span class='badge badge-secondary'>Dry Hire</span>" def get(self, request, pk=None):
context['page_title'] = title embed_url = reverse('event_embed', args=[pk])
return context full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
data = {
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
'version': '1.0',
'type': 'rich',
'height': '250'
}
json = simplejson.JSONEncoderForHTML().encode(data)
return HttpResponse(json, content_type="application/json")
class EventEmbed(EventDetail): class EventEmbed(EventDetail):
template_name = 'event_embed.html' template_name = 'event_embed.html'
class EventOEmbed(OEmbedView):
model = models.Event
url_name = 'event_embed'
class EventCreate(generic.CreateView): class EventCreate(generic.CreateView):
model = models.Event model = models.Event
form_class = forms.EventForm form_class = forms.EventForm
@@ -153,7 +160,7 @@ class EventDuplicate(EventUpdate):
new.checked_in_by = None new.checked_in_by = None
# Remove all the authorisation information from the new event # Remove all the authorisation information from the new event
new.auth_request_to = '' new.auth_request_to = None
new.auth_request_by = None new.auth_request_by = None
new.auth_request_at = None new.auth_request_at = None
@@ -181,9 +188,15 @@ class EventPrint(generic.View):
context = { context = {
'object': object, 'object': object,
'fonts': {
'opensans': {
'regular': 'static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'static/fonts/OPENSANS-BOLD.TTF',
}
},
'quote': True, 'quote': True,
'current_user': request.user, 'current_user': request.user,
'filename': 'Event_{}_{}_{}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date) 'filename': 'Event {} {} {}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
} }
rml = template.render(context) rml = template.render(context)
@@ -274,7 +287,6 @@ class EventArchive(generic.ListView):
class EventAuthorise(generic.UpdateView): class EventAuthorise(generic.UpdateView):
template_name = 'eventauthorisation_form.html' template_name = 'eventauthorisation_form.html'
success_template = 'eventauthorisation_success.html' success_template = 'eventauthorisation_success.html'
preview = False
def form_valid(self, form): def form_valid(self, form):
self.object = form.save() self.object = form.save()
@@ -282,7 +294,7 @@ class EventAuthorise(generic.UpdateView):
self.template_name = self.success_template self.template_name = self.success_template
messages.add_message(self.request, messages.SUCCESS, messages.add_message(self.request, messages.SUCCESS,
'Success! Your event has been authorised. ' + 'Success! Your event has been authorised. ' +
'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()) return self.render_to_response(self.get_context_data())
@property @property
@@ -302,7 +314,6 @@ class EventAuthorise(generic.UpdateView):
context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name) context['page_title'] = "{}: {}".format(self.event.display_id, self.event.name)
if self.event.dry_hire: if self.event.dry_hire:
context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>' context['page_title'] += ' <span class="badge badge-secondary align-top">Dry Hire</span>'
context['preview'] = self.preview
return context return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@@ -351,7 +362,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
return self.get_object() return self.get_object()
def get_success_url(self): def get_success_url(self):
if is_ajax(self.request): if self.request.is_ajax():
url = reverse_lazy('closemodal') url = reverse_lazy('closemodal')
messages.info(self.request, "location.reload()") messages.info(self.request, "location.reload()")
else: else:
@@ -389,7 +400,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
to=[email], to=[email],
reply_to=[self.request.user.email], reply_to=[self.request.user.email],
) )
css = finders.find('css/email.css') css = staticfiles_storage.path('css/email.css')
html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context), html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
external_styles=css).transform() external_styles=css).transform()
msg.attach_alternative(html, 'text/html') msg.attach_alternative(html, 'text/html')
@@ -404,7 +415,8 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
model = models.Event model = models.Event
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
css = finders.find('css/email.css') from django.contrib.staticfiles.storage import staticfiles_storage
css = staticfiles_storage.path('css/email.css')
response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs) response = super(EventAuthoriseRequestEmailPreview, self).render_to_response(context, **response_kwargs)
assert isinstance(response, HttpResponse) assert isinstance(response, HttpResponse)
response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform() response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform()
@@ -418,5 +430,4 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
'sent_by': self.request.user.pk, 'sent_by': self.request.user.pk,
}) })
context['to_name'] = self.request.GET.get('to_name', None) context['to_name'] = self.request.GET.get('to_name', None)
context['target'] = 'event_authorise_form_preview'
return context return context

View File

@@ -1,30 +1,37 @@
import datetime
import re import re
import urllib.request
import urllib.error import urllib.error
import urllib.parse import urllib.parse
import urllib.request
from io import BytesIO from io import BytesIO
from django.db.models.signals import post_save
from PyPDF2 import PdfFileReader, PdfFileMerger from PyPDF2 import PdfFileReader, PdfFileMerger
from django.conf import settings from django.conf import settings
from django.contrib.staticfiles import finders from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.cache import cache
from django.core.mail import EmailMessage, EmailMultiAlternatives from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.db.models.signals import post_save from django.core.cache import cache
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from premailer import Premailer
from registration.signals import user_activated from registration.signals import user_activated
from reversion import revisions as reversion from premailer import Premailer
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
from RIGS import models from RIGS import models
from reversion import revisions as reversion
def send_eventauthorisation_success_email(instance): def send_eventauthorisation_success_email(instance):
# Generate PDF first to prevent context conflicts # Generate PDF first to prevent context conflicts
context = { context = {
'object': instance.event, 'object': instance.event,
'fonts': {
'opensans': {
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'receipt': True, 'receipt': True,
'current_user': False, 'current_user': False,
} }
@@ -63,7 +70,7 @@ def send_eventauthorisation_success_email(instance):
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS], reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
) )
css = finders.find('css/email.css') css = staticfiles_storage.path('css/email.css')
html = Premailer(get_template("eventauthorisation_client_success.html").render(context), html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
external_styles=css).transform() external_styles=css).transform()
client_email.attach_alternative(html, 'text/html') client_email.attach_alternative(html, 'text/html')
@@ -121,7 +128,7 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
to=[admin.email], to=[admin.email],
reply_to=[user.email], reply_to=[user.email],
) )
css = finders.find('css/email.css') css = staticfiles_storage.path('css/email.css')
html = Premailer(get_template("admin_awaiting_approval.html").render(context), html = Premailer(get_template("admin_awaiting_approval.html").render(context),
external_styles=css).transform() external_styles=css).transform()
email.attach_alternative(html, 'text/html') email.attach_alternative(html, 'text/html')

View File

@@ -0,0 +1,28 @@
/*!
* Ajax Bootstrap Select
*
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
*
* @version 1.4.5
* @author Adam Heim - https://github.com/truckingsim
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
* @copyright 2019 Adam Heim
* @license Released under the MIT license.
*
* Contributors:
* Mark Carver - https://github.com/markcarver
*
* Last build: 2019-04-23 12:18:56 PM EDT
*/
.bootstrap-select .status {
background: #f0f0f0;
clear: both;
color: #999;
font-size: 11px;
font-style: italic;
font-weight: 500;
line-height: 1;
margin-bottom: -5px;
padding: 10px 20px; }
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7OztFQWVFO0FBQ0Y7RUFDRSxtQkFBbUI7RUFDbkIsV0FBVztFQUNYLFdBQVc7RUFDWCxlQUFlO0VBQ2Ysa0JBQWtCO0VBQ2xCLGdCQUFnQjtFQUNoQixjQUFjO0VBQ2QsbUJBQW1CO0VBQ25CLGtCQUFrQixFQUFBIiwiZmlsZSI6ImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyIvKiFcbiAqIEFqYXggQm9vdHN0cmFwIFNlbGVjdFxuICpcbiAqIEV4dGVuZHMgZXhpc3RpbmcgW0Jvb3RzdHJhcCBTZWxlY3RdIGltcGxlbWVudGF0aW9ucyBieSBhZGRpbmcgdGhlIGFiaWxpdHkgdG8gc2VhcmNoIHZpYSBBSkFYIHJlcXVlc3RzIGFzIHlvdSB0eXBlLiBPcmlnaW5hbGx5IGZvciBDUk9TQ09OLlxuICpcbiAqIEB2ZXJzaW9uIDEuNC41XG4gKiBAYXV0aG9yIEFkYW0gSGVpbSAtIGh0dHBzOi8vZ2l0aHViLmNvbS90cnVja2luZ3NpbVxuICogQGxpbmsgaHR0cHM6Ly9naXRodWIuY29tL3RydWNraW5nc2ltL0FqYXgtQm9vdHN0cmFwLVNlbGVjdFxuICogQGNvcHlyaWdodCAyMDE5IEFkYW0gSGVpbVxuICogQGxpY2Vuc2UgUmVsZWFzZWQgdW5kZXIgdGhlIE1JVCBsaWNlbnNlLlxuICpcbiAqIENvbnRyaWJ1dG9yczpcbiAqICAgTWFyayBDYXJ2ZXIgLSBodHRwczovL2dpdGh1Yi5jb20vbWFya2NhcnZlclxuICpcbiAqIExhc3QgYnVpbGQ6IDIwMTktMDQtMjMgMTI6MTg6NTYgUE0gRURUXG4gKi9cbi5ib290c3RyYXAtc2VsZWN0IC5zdGF0dXMge1xuICBiYWNrZ3JvdW5kOiAjZjBmMGYwO1xuICBjbGVhcjogYm90aDtcbiAgY29sb3I6ICM5OTk7XG4gIGZvbnQtc2l6ZTogMTFweDtcbiAgZm9udC1zdHlsZTogaXRhbGljO1xuICBmb250LXdlaWdodDogNTAwO1xuICBsaW5lLWhlaWdodDogMTtcbiAgbWFyZ2luLWJvdHRvbTogLTVweDtcbiAgcGFkZGluZzogMTBweCAyMHB4O1xufVxuIl19 */

View File

@@ -0,0 +1,28 @@
/*!
* Ajax Bootstrap Select
*
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
*
* @version 1.4.5
* @author Adam Heim - https://github.com/truckingsim
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
* @copyright 2019 Adam Heim
* @license Released under the MIT license.
*
* Contributors:
* Mark Carver - https://github.com/markcarver
*
* Last build: 2019-04-23 12:18:56 PM EDT
*/
.bootstrap-select .status {
background: #f0f0f0;
clear: both;
color: #999;
font-size: 11px;
font-style: italic;
font-weight: 500;
line-height: 1;
margin-bottom: -5px;
padding: 10px 20px; }
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5taW4uY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7RUFlRTtBQUFDO0VBQTBCLG1CQUFrQjtFQUFDLFdBQVU7RUFBQyxXQUFVO0VBQUMsZUFBYztFQUFDLGtCQUFpQjtFQUFDLGdCQUFlO0VBQUMsY0FBYTtFQUFDLG1CQUFrQjtFQUFDLGtCQUFpQixFQUFBIiwiZmlsZSI6ImFqYXgtYm9vdHN0cmFwLXNlbGVjdC5taW4uY3NzIiwic291cmNlc0NvbnRlbnQiOlsiLyohXG4gKiBBamF4IEJvb3RzdHJhcCBTZWxlY3RcbiAqXG4gKiBFeHRlbmRzIGV4aXN0aW5nIFtCb290c3RyYXAgU2VsZWN0XSBpbXBsZW1lbnRhdGlvbnMgYnkgYWRkaW5nIHRoZSBhYmlsaXR5IHRvIHNlYXJjaCB2aWEgQUpBWCByZXF1ZXN0cyBhcyB5b3UgdHlwZS4gT3JpZ2luYWxseSBmb3IgQ1JPU0NPTi5cbiAqXG4gKiBAdmVyc2lvbiAxLjQuNVxuICogQGF1dGhvciBBZGFtIEhlaW0gLSBodHRwczovL2dpdGh1Yi5jb20vdHJ1Y2tpbmdzaW1cbiAqIEBsaW5rIGh0dHBzOi8vZ2l0aHViLmNvbS90cnVja2luZ3NpbS9BamF4LUJvb3RzdHJhcC1TZWxlY3RcbiAqIEBjb3B5cmlnaHQgMjAxOSBBZGFtIEhlaW1cbiAqIEBsaWNlbnNlIFJlbGVhc2VkIHVuZGVyIHRoZSBNSVQgbGljZW5zZS5cbiAqXG4gKiBDb250cmlidXRvcnM6XG4gKiAgIE1hcmsgQ2FydmVyIC0gaHR0cHM6Ly9naXRodWIuY29tL21hcmtjYXJ2ZXJcbiAqXG4gKiBMYXN0IGJ1aWxkOiAyMDE5LTA0LTIzIDEyOjE4OjU2IFBNIEVEVFxuICovLmJvb3RzdHJhcC1zZWxlY3QgLnN0YXR1c3tiYWNrZ3JvdW5kOiNmMGYwZjA7Y2xlYXI6Ym90aDtjb2xvcjojOTk5O2ZvbnQtc2l6ZToxMXB4O2ZvbnQtc3R5bGU6aXRhbGljO2ZvbnQtd2VpZ2h0OjUwMDtsaW5lLWhlaWdodDoxO21hcmdpbi1ib3R0b206LTVweDtwYWRkaW5nOjEwcHggMjBweH0iXX0= */

View File

@@ -0,0 +1,23 @@
.autocomplete {
background: white;
z-index: 1000;
font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
overflow: auto;
box-sizing: border-box;
border: 1px solid rgba(50, 50, 50, 0.6); }
.autocomplete * {
font: inherit; }
.autocomplete > div {
padding: 0 4px; }
.autocomplete .group {
background: #eee; }
.autocomplete > div:hover:not(.group),
.autocomplete > div.selected {
background: #81ca91;
cursor: pointer; }
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImF1dG9jb21wbGV0ZS5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0E7RUFDSSxpQkFBaUI7RUFDakIsYUFBYTtFQUNiLDRHQUE0RztFQUM1RyxjQUFjO0VBQ2Qsc0JBQXNCO0VBQ3RCLHVDQUF1QyxFQUFBOztBQUczQztFQUNJLGFBQWEsRUFBQTs7QUFHakI7RUFDSSxjQUFjLEVBQUE7O0FBR2xCO0VBQ0ksZ0JBQWdCLEVBQUE7O0FBR3BCOztFQUVJLG1CQUFtQjtFQUNuQixlQUFlLEVBQUEiLCJmaWxlIjoiYXV0b2NvbXBsZXRlLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIlxyXG4uYXV0b2NvbXBsZXRlIHtcclxuICAgIGJhY2tncm91bmQ6IHdoaXRlO1xyXG4gICAgei1pbmRleDogMTAwMDtcclxuICAgIGZvbnQ6IDE0cHgvMjJweCBcIi1hcHBsZS1zeXN0ZW1cIiwgQmxpbmtNYWNTeXN0ZW1Gb250LCBcIlNlZ29lIFVJXCIsIFJvYm90bywgXCJIZWx2ZXRpY2EgTmV1ZVwiLCBBcmlhbCwgc2Fucy1zZXJpZjtcclxuICAgIG92ZXJmbG93OiBhdXRvO1xyXG4gICAgYm94LXNpemluZzogYm9yZGVyLWJveDtcclxuICAgIGJvcmRlcjogMXB4IHNvbGlkIHJnYmEoNTAsIDUwLCA1MCwgMC42KTtcclxufVxyXG5cclxuLmF1dG9jb21wbGV0ZSAqIHtcclxuICAgIGZvbnQ6IGluaGVyaXQ7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgPiBkaXYge1xyXG4gICAgcGFkZGluZzogMCA0cHg7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgLmdyb3VwIHtcclxuICAgIGJhY2tncm91bmQ6ICNlZWU7XHJcbn1cclxuXHJcbi5hdXRvY29tcGxldGUgPiBkaXY6aG92ZXI6bm90KC5ncm91cCksXHJcbi5hdXRvY29tcGxldGUgPiBkaXYuc2VsZWN0ZWQge1xyXG4gICAgYmFja2dyb3VuZDogIzgxY2E5MTtcclxuICAgIGN1cnNvcjogcG9pbnRlcjtcclxufVxyXG5cclxuIl19 */

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

@@ -0,0 +1,39 @@
body {
margin: 0px; }
.main-table {
width: 100%;
border-collapse: collapse; }
.client-header {
background-image: url("https://www.nottinghamtec.co.uk/imgs/wof2014-1.jpg");
background-color: #222;
background-repeat: no-repeat;
background-position: center;
width: 100%;
margin-bottom: 28px; }
.client-header .logos {
width: 100%;
max-width: 640px; }
.client-header img {
height: 110px; }
.content-container {
width: 100%; }
.content-container .content {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
width: 100%;
max-width: 600px;
padding: 10px;
text-align: left; }
.content-container .content .button-container {
width: 100%; }
.content-container .content .button-container .button {
padding: 6px 12px;
background-color: #357ebf;
border-radius: 4px; }
.content-container .content .button-container .button a {
color: #fff;
text-decoration: none; }
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImVtYWlsLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUE7RUFDRSxXQUFXLEVBQUE7O0FBR2I7RUFDRSxXQUFXO0VBQ1gseUJBQXlCLEVBQUE7O0FBSTNCO0VBQ0UsMkVBQTJFO0VBQzNFLHNCQUFzQjtFQUN0Qiw0QkFBNEI7RUFDNUIsMkJBQTJCO0VBRTNCLFdBQVc7RUFFWCxtQkFBbUIsRUFBQTtFQVJyQjtJQVdJLFdBQVc7SUFDWCxnQkFBZ0IsRUFBQTtFQVpwQjtJQWdCSSxhQUFhLEVBQUE7O0FBSWpCO0VBQ0UsV0FBVyxFQUFBO0VBRGI7SUFJSSx3RUFBd0U7SUFFeEUsV0FBVztJQUNYLGdCQUFnQjtJQUNoQixhQUFhO0lBQ2IsZ0JBQWdCLEVBQUE7SUFUcEI7TUFZTSxXQUFXLEVBQUE7TUFaakI7UUFlUSxpQkFBaUI7UUFDakIseUJBaERjO1FBaURkLGtCQUFrQixFQUFBO1FBakIxQjtVQW9CVSxXQUFXO1VBQ1gscUJBQXFCLEVBQUEiLCJmaWxlIjoiZW1haWwuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiJGJ1dHRvbl9jb2xvcjogIzM1N2ViZjtcblxuYm9keXtcbiAgbWFyZ2luOiAwcHg7XG59XG5cbi5tYWluLXRhYmxle1xuICB3aWR0aDogMTAwJTtcbiAgYm9yZGVyLWNvbGxhcHNlOiBjb2xsYXBzZTtcblxufVxuXG4uY2xpZW50LWhlYWRlciB7XG4gIGJhY2tncm91bmQtaW1hZ2U6IHVybChcImh0dHBzOi8vd3d3Lm5vdHRpbmdoYW10ZWMuY28udWsvaW1ncy93b2YyMDE0LTEuanBnXCIpO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMjIyO1xuICBiYWNrZ3JvdW5kLXJlcGVhdDogbm8tcmVwZWF0O1xuICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBjZW50ZXI7XG5cbiAgd2lkdGg6IDEwMCU7XG5cbiAgbWFyZ2luLWJvdHRvbTogMjhweDtcblxuICAubG9nb3N7XG4gICAgd2lkdGg6IDEwMCU7XG4gICAgbWF4LXdpZHRoOiA2NDBweDtcbiAgfVxuXG4gIGltZyB7XG4gICAgaGVpZ2h0OiAxMTBweDtcbiAgfVxufVxuXG4uY29udGVudC1jb250YWluZXJ7XG4gIHdpZHRoOiAxMDAlO1xuXG4gIC5jb250ZW50IHtcbiAgICBmb250LWZhbWlseTogXCJPcGVuIFNhbnNcIiwgXCJIZWx2ZXRpY2EgTmV1ZVwiLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmO1xuXG4gICAgd2lkdGg6IDEwMCU7XG4gICAgbWF4LXdpZHRoOiA2MDBweDtcbiAgICBwYWRkaW5nOiAxMHB4O1xuICAgIHRleHQtYWxpZ246IGxlZnQ7XG5cbiAgICAuYnV0dG9uLWNvbnRhaW5lcntcbiAgICAgIHdpZHRoOiAxMDAlO1xuXG4gICAgICAuYnV0dG9uIHtcbiAgICAgICAgcGFkZGluZzogNnB4IDEycHg7XG4gICAgICAgIGJhY2tncm91bmQtY29sb3I6ICRidXR0b25fY29sb3I7XG4gICAgICAgIGJvcmRlci1yYWRpdXM6IDRweDtcblxuICAgICAgICBhIHtcbiAgICAgICAgICBjb2xvcjogI2ZmZjtcbiAgICAgICAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7XG4gICAgICAgIH1cblxuICAgICAgfVxuXG4gICAgfVxuXG4gIH1cbn1cblxuIl19 */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

269
RIGS/static/css/main.css Normal file

File diff suppressed because one or more lines are too long

1047
RIGS/static/css/main.min.css vendored Normal file

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

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

@@ -0,0 +1,6 @@
/*!
* Bootstrap alert.js v4.6.0 (https://getbootstrap.com/)
* Copyright 2011-2021 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"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Alert=t(e.jQuery,e.Util)}(this,(function(e,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var r=n(e),l=n(t);function u(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)}}var a="bs.alert",o=r.default.fn.alert,i=function(){function e(e){this._element=e}var t,n,o,i=e.prototype;return i.close=function(e){var t=this._element;e&&(t=this._getRootElement(e)),this._triggerCloseEvent(t).isDefaultPrevented()||this._removeElement(t)},i.dispose=function(){r.default.removeData(this._element,a),this._element=null},i._getRootElement=function(e){var t=l.default.getSelectorFromElement(e),n=!1;return t&&(n=document.querySelector(t)),n||(n=r.default(e).closest(".alert")[0]),n},i._triggerCloseEvent=function(e){var t=r.default.Event("close.bs.alert");return r.default(e).trigger(t),t},i._removeElement=function(e){var t=this;if(r.default(e).removeClass("show"),r.default(e).hasClass("fade")){var n=l.default.getTransitionDurationFromElement(e);r.default(e).one(l.default.TRANSITION_END,(function(n){return t._destroyElement(e,n)})).emulateTransitionEnd(n)}else this._destroyElement(e)},i._destroyElement=function(e){r.default(e).detach().trigger("closed.bs.alert").remove()},e._jQueryInterface=function(t){return this.each((function(){var n=r.default(this),l=n.data(a);l||(l=new e(this),n.data(a,l)),"close"===t&&l[t](this)}))},e._handleDismiss=function(e){return function(t){t&&t.preventDefault(),e.close(this)}},t=e,o=[{key:"VERSION",get:function(){return"4.6.0"}}],(n=null)&&u(t.prototype,n),o&&u(t,o),e}();return r.default(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',i._handleDismiss(new i)),r.default.fn.alert=i._jQueryInterface,r.default.fn.alert.Constructor=i,r.default.fn.alert.noConflict=function(){return r.default.fn.alert=o,i._jQueryInterface},i}));

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View File

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

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

File diff suppressed because one or more lines are too long

7
RIGS/static/js/clipboard.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

@@ -0,0 +1 @@
$(document).ready((function(){var t;(t=document.createElement("input")).setAttribute("type","datetime-local"),("text"===t.type||navigator.userAgent.toLowerCase().indexOf("firefox")>-1)&&($("<link>").appendTo("head").attr({type:"text/css",rel:"stylesheet"}).attr("href",'{% static "css/flatpickr.css" %}'),$.when($.getScript('{% static "js/flatpickr.min.js" %}'),$.Deferred((function(t){$(t.resolve)}))).done((function(){$("input[type=datetime-local]").attr("type","text").flatpickr({dateFormat:"Y-m-dTH:m",enableTime:!0,altInput:!0,altFormat:"d/m/y H:m"})})))}));

File diff suppressed because one or more lines are too long

2
RIGS/static/js/flatpickr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
function setupItemTable(t){objectitems=JSON.parse(t),$.each(objectitems,(function(t,e){objectitems[t]=JSON.parse(e)})),newitem=-1}function nl2br(t,e){return(t+"").replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g,"$1"+(e||void 0===e?"<br />":"<br>")+"$2")}function escapeHtml(t){return $("<div/>").text(t).html()}function updatePrices(){var t=0;for(var e in objectitems){var i=objectitems[e].fields,a=i.cost*i.quantity;$("#item-"+e+" .sub-total").html(parseFloat(a).toFixed(2)).data("subtotal",a),t+=Number(a)}$("#sumtotal").text(parseFloat(t).toFixed(2));var o=t*Number($("#vat-rate").data("rate"));$("#vat").text(parseFloat(o).toFixed(2)),$("#total").text(parseFloat(t+o).toFixed(2))}$("#item-table").on("click",".item-delete",(function(){delete objectitems[$(this).data("pk")],$("#item-"+$(this).data("pk")).remove(),updatePrices()})),$("#item-table").on("click",".item-add",(function(){$("#item-form").data("pk",newitem),$("#item_name").val(""),$("#item_description").val(""),$("#item_quantity").val(""),$("#item_cost").val(""),$($(this).data("target")).modal("show")})),$("#item-table").on("click",".item-edit",(function(){var t=$(this).data("pk");$("#item-form").data("pk",t);var e=objectitems[t].fields;$("#item_name").val(e.name),$("#item_description").val(e.description),$("#item_quantity").val(e.quantity),$("#item_cost").val(e.cost),$($(this).data("target")).modal("show")})),$("body").on("submit","#item-form",(function(t){t.preventDefault();var e,i=$(this).data("pk");if($("#itemModal").modal("hide"),i==newitem--){(e=new Object).name=$("#item_name").val(),e.description=$("#item_description").val(),e.cost=$("#item_cost").val(),e.quantity=$("#item_quantity").val();var a=0;for(item in objectitems)a++;e.order=a,objectitems[i]=new Object,objectitems[i].fields=e,$("#new-item-row").clone().attr("id","item-"+i).data("pk",i).appendTo("#item-table-body"),$("#item-"+i+" .item-delete, #item-"+i+" .item-edit").data("pk",i)}else(e=objectitems[i].fields).name=$("#item_name").val(),e.description=$("#item_description").val(),e.cost=$("#item_cost").val(),e.quantity=$("#item_quantity").val(),objectitems[i].fields=e;$row=$("#item-"+i),$row.find(".name").html(escapeHtml(e.name)),$row.find(".description").html(nl2br(escapeHtml(e.description))),$row.find(".cost").html(parseFloat(e.cost).toFixed(2)),$row.find(".quantity").html(e.quantity),updatePrices()})),$("body").on("submit",".itemised_form",(function(t){$("#id_items_json").val(JSON.stringify(objectitems))}));var fixHelper=function(t,e){return e.children().each((function(){$(this).width($(this).width())})),e};$("#item-table tbody").sortable({helper:fixHelper,update:function(t,e){info=$(this).sortable("toArray"),itemorder=new Array,$.each(info,(function(t,e){pk=$("#"+e).data("pk"),objectitems[pk].fields.order=t}))}});

186
RIGS/static/js/jquery-ui.js vendored Normal file

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

@@ -0,0 +1 @@
var Konami=function(t){var e={addEvent:function(t,e,n,o){t.addEventListener?t.addEventListener(e,n,!1):t.attachEvent&&(t["e"+e+n]=n,t[e+n]=function(){t["e"+e+n](window.event,o)},t.attachEvent("on"+e,t[e+n]))},removeEvent:function(t,e,n){t.removeEventListener?t.removeEventListener(e,n):t.attachEvent&&t.detachEvent(e)},input:"",pattern:"38384040373937396665",keydownHandler:function(t,n){if(n&&(e=n),e.input+=t?t.keyCode:event.keyCode,e.input.length>e.pattern.length&&(e.input=e.input.substr(e.input.length-e.pattern.length)),e.input===e.pattern)return e.code(e._currentLink),e.input="",t.preventDefault(),!1},load:function(t){this._currentLink=t,this.addEvent(document,"keydown",this.keydownHandler,this),this.iphone.load(t)},unload:function(){this.removeEvent(document,"keydown",this.keydownHandler),this.iphone.unload()},code:function(t){window.location=t},iphone:{start_x:0,start_y:0,stop_x:0,stop_y:0,tap:!1,capture:!1,orig_keys:"",keys:["UP","UP","DOWN","DOWN","LEFT","RIGHT","LEFT","RIGHT","TAP","TAP"],input:[],code:function(t){e.code(t)},touchmoveHandler:function(t){if(1===t.touches.length&&!0===e.iphone.capture){var n=t.touches[0];e.iphone.stop_x=n.pageX,e.iphone.stop_y=n.pageY,e.iphone.tap=!1,e.iphone.capture=!1,e.iphone.check_direction()}},touchendHandler:function(){if(e.iphone.input.push(e.iphone.check_direction()),e.iphone.input.length>e.iphone.keys.length&&e.iphone.input.shift(),e.iphone.input.length===e.iphone.keys.length){for(var t=!0,n=0;n<e.iphone.keys.length;n++)e.iphone.input[n]!==e.iphone.keys[n]&&(t=!1);t&&e.iphone.code(e._currentLink)}},touchstartHandler:function(t){e.iphone.start_x=t.changedTouches[0].pageX,e.iphone.start_y=t.changedTouches[0].pageY,e.iphone.tap=!0,e.iphone.capture=!0},load:function(t){this.orig_keys=this.keys,e.addEvent(document,"touchmove",this.touchmoveHandler),e.addEvent(document,"touchend",this.touchendHandler,!1),e.addEvent(document,"touchstart",this.touchstartHandler)},unload:function(){e.removeEvent(document,"touchmove",this.touchmoveHandler),e.removeEvent(document,"touchend",this.touchendHandler),e.removeEvent(document,"touchstart",this.touchstartHandler)},check_direction:function(){return x_magnitude=Math.abs(this.start_x-this.stop_x),y_magnitude=Math.abs(this.start_y-this.stop_y),x=this.start_x-this.stop_x<0?"RIGHT":"LEFT",y=this.start_y-this.stop_y<0?"DOWN":"UP",result=x_magnitude>y_magnitude?x:y,result=!0===this.tap?"TAP":result,result}}};return"string"==typeof t&&e.load(t),"function"==typeof t&&(e.code=t,e.load()),e};"undefined"!=typeof module&&void 0!==module.exports?module.exports=Konami:"function"==typeof define&&define.amd?define([],(function(){return Konami})):window.Konami=Konami;

6
RIGS/static/js/main.js Normal file

File diff suppressed because one or more lines are too long

6
RIGS/static/js/main.min.js vendored Normal file

File diff suppressed because one or more lines are too long

6
RIGS/static/js/modal.js Normal file

File diff suppressed because one or more lines are too long

8
RIGS/static/js/moment.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
/*!
* Bootstrap popover.js v4.6.0 (https://getbootstrap.com/)
* Copyright 2011-2021 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"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).Popover=e(t.jQuery,t.Tooltip)}(this,(function(t,e){"use strict";function n(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var o=n(t),r=n(e);function i(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 u(){return(u=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)}var f="popover",a="bs.popover",l="."+a,s=o.default.fn[f],c=new RegExp("(^|\\s)bs-popover\\S+","g"),p=u({},r.default.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>'}),d=u({},r.default.DefaultType,{content:"(string|element|function)"}),h={HIDE:"hide"+l,HIDDEN:"hidden"+l,SHOW:"show"+l,SHOWN:"shown"+l,INSERTED:"inserted"+l,CLICK:"click"+l,FOCUSIN:"focusin"+l,FOCUSOUT:"focusout"+l,MOUSEENTER:"mouseenter"+l,MOUSELEAVE:"mouseleave"+l},y=function(t){var e,n;function r(){return t.apply(this,arguments)||this}n=t,(e=r).prototype=Object.create(n.prototype),e.prototype.constructor=e,e.__proto__=n;var u,s,y,v=r.prototype;return v.isWithContent=function(){return this.getTitle()||this._getContent()},v.addAttachmentClass=function(t){o.default(this.getTipElement()).addClass("bs-popover-"+t)},v.getTipElement=function(){return this.tip=this.tip||o.default(this.config.template)[0],this.tip},v.setContent=function(){var t=o.default(this.getTipElement());this.setElementContent(t.find(".popover-header"),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(".popover-body"),e),t.removeClass("fade show")},v._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},v._cleanTipClass=function(){var t=o.default(this.getTipElement()),e=t.attr("class").match(c);null!==e&&e.length>0&&t.removeClass(e.join(""))},r._jQueryInterface=function(t){return this.each((function(){var e=o.default(this).data(a),n="object"==typeof t?t:null;if((e||!/dispose|hide/.test(t))&&(e||(e=new r(this,n),o.default(this).data(a,e)),"string"==typeof t)){if(void 0===e[t])throw new TypeError('No method named "'+t+'"');e[t]()}}))},u=r,y=[{key:"VERSION",get:function(){return"4.6.0"}},{key:"Default",get:function(){return p}},{key:"NAME",get:function(){return f}},{key:"DATA_KEY",get:function(){return a}},{key:"Event",get:function(){return h}},{key:"EVENT_KEY",get:function(){return l}},{key:"DefaultType",get:function(){return d}}],(s=null)&&i(u.prototype,s),y&&i(u,y),r}(r.default);return o.default.fn[f]=y._jQueryInterface,o.default.fn[f].Constructor=y,o.default.fn[f].noConflict=function(){return o.default.fn[f]=s,y._jQueryInterface},y}));

25
RIGS/static/js/popper.js Normal file

File diff suppressed because one or more lines are too long

2
RIGS/static/js/raven.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -117,9 +117,23 @@ $('body').on('submit', '.itemised_form', function (e) {
$('#id_items_json').val(JSON.stringify(objectitems)); $('#id_items_json').val(JSON.stringify(objectitems));
}); });
sortable("#item-table tbody")[0].addEventListener('sortupdate', function (e) { // Return a helper with preserved width of cells
var items = e.detail.destination.items; var fixHelper = function (e, ui) {
for(var i in items) { ui.children().each(function () {
objectitems[items[i].dataset.pk].fields.order = i; $(this).width($(this).width());
});
return ui;
};
$("#item-table tbody").sortable({
helper: fixHelper,
update: function (e, ui) {
info = $(this).sortable("toArray");
itemorder = new Array();
$.each(info, function (key, value) {
pk = $('#' + value).data('pk');
objectitems[pk].fields.order = key;
});
} }
}); });

File diff suppressed because one or more lines are too long

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

@@ -0,0 +1,6 @@
/*!
* Bootstrap util.js v4.6.0 (https://getbootstrap.com/)
* Copyright 2011-2021 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";function e(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var n=e(t),r="transitionend";function o(t){var e=this,r=!1;return n.default(this).one(i.TRANSITION_END,(function(){r=!0})),setTimeout((function(){r||i.triggerTransitionEnd(e)}),t),this}var i={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(t){if(!t)return 0;var e=n.default(t).css("transition-duration"),r=n.default(t).css("transition-delay"),o=parseFloat(e),i=parseFloat(r);return o||i?(e=e.split(",")[0],r=r.split(",")[0],1e3*(parseFloat(e)+parseFloat(r))):0},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(t){n.default(t).trigger(r)},supportsTransitionEnd:function(){return Boolean(r)},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,n){for(var r in n)if(Object.prototype.hasOwnProperty.call(n,r)){var o=n[r],a=e[r],u=a&&i.isElement(a)?"element":null==(l=a)?""+l:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(o).test(u))throw new Error(t.toUpperCase()+': Option "'+r+'" provided type "'+u+'" but expected type "'+o+'".')}var l},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?i.findShadowRoot(t.parentNode):null},jQueryDetection:function(){if(void 0===n.default)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var t=n.default.fn.jquery.split(" ")[0].split(".");if(t[0]<2&&t[1]<9||1===t[0]&&9===t[1]&&t[2]<1||t[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};return i.jQueryDetection(),n.default.fn.emulateTransitionEnd=o,n.default.event.special[i.TRANSITION_END]={bindType:r,delegateType:r,handle:function(t){if(n.default(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}},i}));

View File

@@ -111,12 +111,4 @@
.custom-control-input:focus ~ .custom-control-label::before { .custom-control-input:focus ~ .custom-control-label::before {
box-shadow: 0 0 0 $input-focus-width rgba($primary, 0.7) !important; box-shadow: 0 0 0 $input-focus-width rgba($primary, 0.7) !important;
} }
.source {
color: white !important;
}
.embed_container {
border-color: #3853a4 !important;
background: #222;
color: $gray-100;
}
} }

View File

@@ -0,0 +1 @@
@import "node_modules/bootstrap/scss/bootstrap";

View File

@@ -1,37 +1,6 @@
@import "dark_screen";
@import "custom-variables"; @import "custom-variables";
//Required @import "node_modules/bootstrap/scss/bootstrap";
@import "node_modules/bootstrap/scss/bootstrap-reboot";
@import "node_modules/bootstrap/scss/bootstrap-grid";
//Optional
@import "node_modules/bootstrap/scss/root";
@import "node_modules/bootstrap/scss/type";
@import "node_modules/bootstrap/scss/images";
@import "node_modules/bootstrap/scss/tables";
@import "node_modules/bootstrap/scss/forms";
@import "node_modules/bootstrap/scss/buttons";
@import "node_modules/bootstrap/scss/transitions";
@import "node_modules/bootstrap/scss/dropdown";
@import "node_modules/bootstrap/scss/button-group";
@import "node_modules/bootstrap/scss/input-group";
@import "node_modules/bootstrap/scss/custom-forms";
@import "node_modules/bootstrap/scss/nav";
@import "node_modules/bootstrap/scss/navbar";
@import "node_modules/bootstrap/scss/card";
@import "node_modules/bootstrap/scss/pagination";
@import "node_modules/bootstrap/scss/badge";
@import "node_modules/bootstrap/scss/alert";
@import "node_modules/bootstrap/scss/media";
@import "node_modules/bootstrap/scss/list-group";
@import "node_modules/bootstrap/scss/close";
@import "node_modules/bootstrap/scss/modal";
@import "node_modules/bootstrap/scss/tooltip";
@import "node_modules/bootstrap/scss/popover";
@import "node_modules/bootstrap/scss/spinners";
@import "node_modules/bootstrap/scss/utilities";
//FontAwesome
$fa-font-path: '/static/fonts';
@import "node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
@import "node_modules/@fortawesome/fontawesome-free/scss/solid";
@media screen and @media screen and
(prefers-reduced-motion: reduce), (prefers-reduced-motion: reduce),
@@ -113,6 +82,9 @@ textarea {
.dont-break-out { .dont-break-out {
overflow-wrap: break-word; overflow-wrap: break-word;
word-wrap: break-word; word-wrap: break-word;
-webkit-hyphens: auto;
-ms-hyphens: auto;
-moz-hyphens: auto;
hyphens: auto; hyphens: auto;
} }

View File

@@ -74,7 +74,6 @@
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ block.super }}
<script src="{% static 'js/tooltip.js' %}"></script> <script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/popover.js' %}"></script> <script src="{% static 'js/popover.js' %}"></script>
<script> <script>

View File

@@ -4,13 +4,21 @@
{% block title %}Calendar{% endblock %} {% block title %}Calendar{% endblock %}
{% block css %} {% block css %}
<link href="{% static 'css/main.css' %}" rel='stylesheet' /> <link href="{% static 'css/main.min.css' %}" rel='stylesheet' />
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{% static 'js/moment.js' %}"></script> <script src="{% static 'js/moment.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script> <script src="{% static 'js/main.min.js' %}"></script>
<script> <script>
function getUrlVars() {
var vars = {};
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
vars[key] = value;
});
return vars;
}
viewToUrl = { viewToUrl = {
'timeGridWeek':'week', 'timeGridWeek':'week',
'timeGridDay':'day', 'timeGridDay':'day',
@@ -70,7 +78,7 @@
'url': $(this).attr('url') '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')]; thisEvent['color'] = colours[$(this).attr('status')];
}else{ }else{
thisEvent['color'] = colours['non-rig']; thisEvent['color'] = colours['non-rig'];
@@ -87,35 +95,31 @@
$('#calendar-header').text(view.title); $('#calendar-header').text(view.title);
// Enable/Disable "Today" button as required // Enable/Disable "Today" button as required
let $today = $('#today-button');
if(moment().isBetween(view.currentStart, view.currentEnd)){ if(moment().isBetween(view.currentStart, view.currentEnd)){
//Today is within the current view //Today is within the current view
$today.prop('disabled', true); $('#today-button').prop('disabled', true);
}else{ }else{
$today.prop('disabled', false); $('#today-button').prop('disabled', false);
} }
// Set active view select button // Set active view select button
let $month = $('#month-button');
let $week = $('#week-button');
let $day = $('#day-button');
switch(view.type){ switch(view.type){
case 'dayGridMonth': case 'dayGridMonth':
$month.addClass('active'); $('#month-button').addClass('active');
$week.removeClass('active'); $('#week-button').removeClass('active');
$day.removeClass('active'); $('#day-button').removeClass('active');
break; break;
case 'timeGridWeek': case 'timeGridWeek':
$month.removeClass('active'); $('#month-button').removeClass('active');
$week.addClass('active'); $('#week-button').addClass('active');
$day.removeClass('active'); $('#day-button').removeClass('active');
break; break;
case 'timeGridDay': case 'timeGridDay':
$month.removeClass('active'); $('#month-button').removeClass('active');
$week.removeClass('active'); $('#week-button').removeClass('active');
$day.addClass('active'); $('#day-button').addClass('active');
break; break;
} }
history.replaceState(null,null,"{% url 'web_calendar' %}"+viewToUrl[view.type]+'/'+moment(view.currentStart).format('YYYY-MM-DD')+'/'); history.replaceState(null,null,"{% url 'web_calendar' %}"+viewToUrl[view.type]+'/'+moment(view.currentStart).format('YYYY-MM-DD')+'/');

View File

@@ -5,13 +5,11 @@
{% load static %} {% load static %}
{% block css %} {% block css %}
{{ block.super }} <link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
{% endblock %} {% endblock %}
{% block preload_js %} {% block preload_js %}
{{ block.super }} <script src="{% static 'js/bootstrap-select.js' %}"></script>
<script src="{% static 'js/selects.js' %}" async></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

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

View File

@@ -7,28 +7,34 @@
{% block css %} {% block css %}
{{ block.super }} {{ block.super }}
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/> <link rel="stylesheet" href="{% static 'css/bootstrap-select.css' %}"/>
<link rel="stylesheet" href="{% static 'css/ajax-bootstrap-select.css' %}"/>
{% endblock %} {% endblock %}
{% block preload_js %} {% block preload_js %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/selects.js' %}"></script> <script src="{% static 'js/bootstrap-select.js' %}"></script>
<script src="{% static 'js/ajax-bootstrap-select.js' %}"></script>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ block.super }} {{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script> <script src="{% static 'js/jquery-ui.js' %}"></script><!--TODO optimise-->
<script src="{% static 'js/interaction.js' %}"></script>
<script src="{% static 'js/modal.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script> <script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/autocompleter.js' %}"></script>
{% include 'partials/datetime-fix.html' %} {% include 'partials/datetime-fix.html' %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$('button[data-action=add]').on('click', function (event) { $('button[data-action=add]').on('click', function (event) {
event.preventDefault(); event.preventDefault();
let target = $($(this).attr('data-target')); var target = $($(this).attr('data-target'));
let newID = Number(target.attr('data-pk')); var newID = Number(target.attr('data-pk'));
let newRow = $($(this).attr('data-clone')) var newRow = $($(this).attr('data-clone'))
.clone().attr('style', "") .clone().attr('style', "")
.attr('id', function(i, val){ .attr('id', function(i, val){
return val.split("_")[0] + '_' + newID; return val.split("_")[0] + '_' + newID;
@@ -128,20 +134,20 @@
<tbody id="vehiclest" data-pk="-1"> <tbody id="vehiclest" data-pk="-1">
<tr id="vehicles_new" style="display: none;"> <tr id="vehicles_new" style="display: none;">
<td><input type="text" class="form-control" name="vehicle_new" disabled="true"/></td> <td><input type="text" class="form-control" name="vehicle_new" disabled="true"/></td>
<td><select data-container="body" 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><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> </tr>
{% for i in object.vehicles.all %} {% for i in object.vehicles.all %}
<tr id="vehicles_{{i.pk}}"> <tr id="vehicles_{{i.pk}}">
<td><input name="vehicle_{{i.pk}}" type="text" class="form-control" value="{{ i.vehicle }}"/></td> <td><input name="vehicle_{{i.pk}}" type="text" class="form-control" value="{{ i.vehicle }}"/></td>
<td> <td>
<select data-container="body" name="driver_{{i.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials"> <select name="driver_{{i.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if i.driver != '' %} {% if i.driver != '' %}
<option value="{{i.driver.pk}}" selected="selected">{{ i.driver.name }}</option> <option value="{{i.driver.pk}}" selected="selected">{{ i.driver.name }}</option>
{% endif %} {% endif %}
</select> </select>
</td> </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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -196,17 +202,17 @@
<tbody id="crewmemberst" data-pk="-1"> <tbody id="crewmemberst" data-pk="-1">
<tr id="crew_new" style="display: none;"> <tr id="crew_new" style="display: none;">
<td> <td>
<select name="crewmember_new" class="form-control" data-container="body" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" disabled="true"></select> <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>
<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="start_new" type="datetime-local" class="form-control" value="{{ i.start }}" disabled="true"/></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="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 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> </tr>
{% for crew in object.crew.all %} {% for crew in object.crew.all %}
<tr id="crew_{{crew.pk}}"> <tr id="crew_{{crew.pk}}">
<td> <td>
<select data-container="body" name="crewmember_{{crew.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials"> <select name="crewmember_{{crew.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if crew.crewmember != '' %} {% if crew.crewmember != '' %}
<option value="{{crew.crewmember.pk}}" selected="selected">{{ crew.crewmember.name }}</option> <option value="{{crew.crewmember.pk}}" selected="selected">{{ crew.crewmember.name }}</option>
{% endif %} {% endif %}
@@ -215,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="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="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><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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -234,7 +240,7 @@
<div class="row my-3" id="size-0"> <div class="row my-3" id="size-0">
<div class="col-12"> <div class="col-12">
<div class="card border-success"> <div class="card border-success">
<div class="card-header">Electrical Checks <small>for Small TEC Events <6kVA (approx. 26A)</small></div> <div class="card-header">Electrical Checks <small>for Small TEC Events <6kVA (aprox. 26A)</small></div>
<div class="card-body"> <div class="card-body">
{% include 'partials/checklist_checkbox.html' with formitem=form.rcds %} {% include 'partials/checklist_checkbox.html' with formitem=form.rcds %}
{% include 'partials/checklist_checkbox.html' with formitem=form.supply_test %} {% include 'partials/checklist_checkbox.html' with formitem=form.supply_test %}
@@ -331,7 +337,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<hr/> <hr>
{% include 'partials/checklist_checkbox.html' with formitem=form.all_rcds_tested %} {% include 'partials/checklist_checkbox.html' with formitem=form.all_rcds_tested %}
{% include 'partials/checklist_checkbox.html' with formitem=form.public_sockets_tested %} {% include 'partials/checklist_checkbox.html' with formitem=form.public_sockets_tested %}
{% include 'partials/ec_power_info.html' %} {% include 'partials/ec_power_info.html' %}

View File

@@ -2,12 +2,20 @@
{% load linkornone from filters %} {% load linkornone from filters %}
{% load namewithnotes from filters %} {% load namewithnotes from filters %}
{% block title %}{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %} | {{object.name}}{% endblock %}
{% block content %} {% block content %}
<div class="row my-3 py-3"> <div class="row my-3 py-3">
{% if not request.is_ajax %} {% if not request.is_ajax %}
<div class="col-sm-12">
<h1>
{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
| {{ object.name }} {% if event.dry_hire %}<span class="badge badge-secondary">Dry Hire</span>{% endif %}
</h1>
</div>
{% if perms.RIGS.view_event %} {% if perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %} {% include 'event_detail_buttons.html' %}
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
@@ -30,7 +38,7 @@
<dt class="col-sm-6">Email</dt> <dt class="col-sm-6">Email</dt>
<dd class="col-sm-6">{{ object.person.email|linkornone:'mailto' }}</dd> <dd class="col-sm-6">{{ object.person.email|linkornone:'mailto' }}</dd>
<dt class="col-sm-6">Phone Number</dt> <dt class="col-sm-6">Phone Number</dt>
<dd class="col-sm-6">{{ object.person.phone|linkornone:'tel' }}</dd> <dd class="col-sm-6">{{ object.person.phone|linkornone:'tel' }}</a></dd>
</dl> </dl>
</div> </div>
</div> </div>
@@ -51,7 +59,7 @@
<dt class="col-sm-6">Email</dt> <dt class="col-sm-6">Email</dt>
<dd class="col-sm-6">{{ object.organisation.email|linkornone:'mailto' }}</dd> <dd class="col-sm-6">{{ object.organisation.email|linkornone:'mailto' }}</dd>
<dt class="col-sm-6">Phone Number</dt> <dt class="col-sm-6">Phone Number</dt>
<dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</dd> <dd class="col-sm-6">{{ object.organisation.phone|linkornone:'tel' }}</a></dd>
<dt class="col-sm-6">Has SU Account</dt> <dt class="col-sm-6">Has SU Account</dt>
<dd class="col-sm-6">{{ event.organisation.union_account|yesno|capfirst }}</dd> <dd class="col-sm-6">{{ event.organisation.union_account|yesno|capfirst }}</dd>
</dl> </dl>
@@ -77,7 +85,7 @@
{% endif %} {% endif %}
{% if not request.is_ajax and perms.RIGS.view_event %} {% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %} {% include 'event_detail_buttons.html' %}
</div> </div>
{% endif %} {% endif %}
{% if event.is_rig %} {% if event.is_rig %}
@@ -97,7 +105,7 @@
</div> </div>
{% if not request.is_ajax and perms.RIGS.view_event %} {% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %} {% include 'event_detail_buttons.html' %}
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@@ -9,7 +9,7 @@
{% if event.internal %} {% if event.internal %}
<a class="btn item-add modal-href event-authorise-request <a class="btn item-add modal-href event-authorise-request
{% if event.authorised %} {% if event.authorised %}
btn-success active btn-success
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %} {% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
btn-warning btn-warning
{% elif event.auth_request_to %} {% elif event.auth_request_to %}
@@ -19,7 +19,7 @@
{% endif %} {% endif %}
" "
href="{% url 'event_authorise_request' object.pk %}"> href="{% url 'event_authorise_request' object.pk %}">
<span class="fas fa-paper-plane"></span> <i class="fas fa-paper-plane"></i>
<span class="d-none d-sm-inline"> <span class="d-none d-sm-inline">
{% if event.authorised %} {% if event.authorised %}
Authorised Authorised

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