Compare commits

..

31 Commits

Author SHA1 Message Date
David Taylor
56e833aed0 Improvements to form printing 2016-05-23 01:04:57 +01:00
David Taylor
cf82ba9680 Used correct values in event_detail 2016-05-23 01:04:57 +01:00
David Taylor
27eb7cb64b Versioning now uses object string representation rather than the ridiculous chain of if statements. Have improved __str methods accordingly 2016-05-23 01:04:54 +01:00
David Taylor
ece075e948 Made activity stream more useful for form changes 2016-05-23 01:04:54 +01:00
David Taylor
b3c6e4ce73 Added __str__ method & absolute url method to Forms class 2016-05-23 01:02:46 +01:00
David Taylor
de8a3088d2 Added forms to "Recent Changes"... That was way too easy, python is awesome 2016-05-23 01:02:46 +01:00
David Taylor
d611bf3905 Versioning UI now utilises decorators to register classes, in line with reversion, to allow expansion without adding dependencies 2016-05-23 01:02:46 +01:00
David Taylor
0aa2c62af8 Added template tags functionality to schema/layout (to allow specifying default values based on event) 2016-05-23 01:02:43 +01:00
David Taylor
bdb7941579 Added buttons to detail & edit view 2016-05-23 01:02:43 +01:00
David Taylor
45c020828f Set arrays to start empty in edit interface 2016-05-23 01:02:40 +01:00
David Taylor
33958d0670 Added array empty condition to print 2016-05-23 01:02:40 +01:00
David Taylor
48236680ca Tidied print templates 2016-05-23 01:02:40 +01:00
David Taylor
8e600c8749 Added read-only view of form 2016-05-23 01:02:40 +01:00
David Taylor
58e06c1b3e More tidying of code & templates 2016-05-23 01:02:40 +01:00
David Taylor
b3cf0b39ac Tidied code 2016-05-23 01:02:40 +01:00
David Taylor
a481b325a4 Added print button to form list 2016-05-23 01:02:40 +01:00
David Taylor
5862646314 Added title to print 2016-05-23 01:02:40 +01:00
David Taylor
09dc2a3eb4 Make validation error more verbose 2016-05-23 01:02:37 +01:00
David Taylor
c557d810ae Added jsonschema to requirements 2016-05-23 01:02:37 +01:00
David Taylor
ca8ba10efa Added printing 2016-05-23 01:02:37 +01:00
David Taylor
fe943e12c1 Checks schema & layout are valid
Checks received data follows the schema
2016-05-23 01:02:35 +01:00
David Taylor
212b055b59 Corrected default layout value 2016-05-23 01:02:35 +01:00
David Taylor
4ee0dab5dc Added migration & sample form type/schema 2016-05-23 01:02:32 +01:00
David Taylor
879060a171 Added link to event_detail page 2016-05-23 01:02:28 +01:00
David Taylor
0e42999d3e Added list view & more stuff 2016-05-23 01:02:28 +01:00
David Taylor
153dacbf22 Added update view & saving logic 2016-05-23 01:02:28 +01:00
David Taylor
7a0746d1a4 Added view, doesn't yet submit 2016-05-23 01:02:28 +01:00
David Taylor
f99e16f562 Renamed app to avoid confusion with django forms 2016-05-23 01:02:28 +01:00
David Taylor
eb41636ffc Corrected model copying fail 2016-05-23 01:02:28 +01:00
David Taylor
e56cff17fa Added initial model tests 2016-05-23 01:02:28 +01:00
David Taylor
56a2ea68fb Created forms app and added initial models 2016-05-23 01:02:28 +01:00
557 changed files with 34888 additions and 43215 deletions

View File

@@ -1,5 +0,0 @@
[run]
omit = */migrations/*
*/tests/*
*/site-packages/*
*/distutils/*

View File

@@ -1,151 +0,0 @@
name: 'Combine PRs'
# Controls when the action will run - in this case triggered manually
on:
workflow_dispatch:
inputs:
branchPrefix:
description: 'Branch prefix to find combinable PRs based on'
required: true
default: 'dependabot'
mustBeGreen:
description: 'Only combine PRs that are green (status is success)'
required: true
default: true
combineBranchName:
description: 'Name of the branch to combine PRs into'
required: true
default: 'combine-prs-branch'
ignoreLabel:
description: 'Exclude PRs with this label'
required: true
default: 'nocombine'
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "combine-prs"
combine-prs:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/github-script@v6
id: create-combined-pr
name: Create Combined PR
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', {
owner: context.repo.owner,
repo: context.repo.repo
});
let branchesAndPRStrings = [];
let baseBranch = null;
let baseBranchSHA = null;
for (const pull of pulls) {
const branch = pull['head']['ref'];
console.log('Pull for branch: ' + branch);
if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) {
console.log('Branch matched prefix: ' + branch);
let statusOK = true;
if(${{ github.event.inputs.mustBeGreen }}) {
console.log('Checking green status: ' + branch);
const stateQuery = `query($owner: String!, $repo: String!, $pull_number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number:$pull_number) {
commits(last: 1) {
nodes {
commit {
statusCheckRollup {
state
}
}
}
}
}
}
}`
const vars = {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pull['number']
};
const result = await github.graphql(stateQuery, vars);
const [{ commit }] = result.repository.pullRequest.commits.nodes;
const state = commit.statusCheckRollup.state
console.log('Validating status: ' + state);
if(state != 'SUCCESS') {
console.log('Discarding ' + branch + ' with status ' + state);
statusOK = false;
}
}
console.log('Checking labels: ' + branch);
const labels = pull['labels'];
for(const label of labels) {
const labelName = label['name'];
console.log('Checking label: ' + labelName);
if(labelName == '${{ github.event.inputs.ignoreLabel }}') {
console.log('Discarding ' + branch + ' with label ' + labelName);
statusOK = false;
}
}
if (statusOK) {
console.log('Adding branch to array: ' + branch);
const prString = '#' + pull['number'] + ' ' + pull['title'];
branchesAndPRStrings.push({ branch, prString });
baseBranch = pull['base']['ref'];
baseBranchSHA = pull['base']['sha'];
}
}
}
if (branchesAndPRStrings.length == 0) {
core.setFailed('No PRs/branches matched criteria');
return;
}
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}',
sha: baseBranchSHA
});
} catch (error) {
console.log(error);
core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?');
return;
}
let combinedPRs = [];
let mergeFailedPRs = [];
for(const { branch, prString } of branchesAndPRStrings) {
try {
await github.rest.repos.merge({
owner: context.repo.owner,
repo: context.repo.repo,
base: '${{ github.event.inputs.combineBranchName }}',
head: branch,
});
console.log('Merged branch ' + branch);
combinedPRs.push(prString);
} catch (error) {
console.log('Failed to merge branch ' + branch);
mergeFailedPRs.push(prString);
}
}
console.log('Creating combined PR');
const combinedPRsString = combinedPRs.join('\n');
let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString;
if(mergeFailedPRs.length > 0) {
const mergeFailedPRsString = mergeFailedPRs.join('\n');
body += '\n\n⚠ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString
}
await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Combined PR',
head: '${{ github.event.inputs.combineBranchName }}',
base: baseBranch,
body: body
});

View File

@@ -1,14 +0,0 @@
name: Manual Deploy
on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: akhileshns/heroku-deploy@v3.12.12 # This is the action
with:
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
heroku_app_name: "pyrigs" #Must be unique in Heroku
heroku_email: "aj@aronajones.com"

View File

@@ -1,53 +0,0 @@
name: Django CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PYTHONDONTWRITEBYTECODE: 1
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.9
cache: 'pipenv'
- name: Install Dependencies
run: |
python3 -m pip install --upgrade pip pipenv
pipenv install -d
# if: steps.pcache.outputs.cache-hit != 'true'
- name: Cache Static Files
id: static-cache
uses: actions/cache@v3
with:
path: 'pipeline/built_assets'
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
- uses: bahmutov/npm-install@v1
if: steps.static-cache.outputs.cache-hit != 'true'
- run: node node_modules/gulp/bin/gulp build
if: steps.static-cache.outputs.cache-hit != 'true'
- name: Basic Checks
run: |
pipenv run pycodestyle . --exclude=migrations,node_modules
pipenv run python3 manage.py check
pipenv run python3 manage.py makemigrations --check --dry-run
pipenv run python3 manage.py collectstatic --noinput
- name: Run Tests
run: pipenv run pytest -n auto --cov
- uses: actions/upload-artifact@v3
if: failure()
with:
name: failure-screenshots ${{ matrix.test-group }}
path: screenshots/
retention-days: 5
- name: Coveralls
run: pipenv run coveralls --service=github

25
.gitignore vendored
View File

@@ -1,6 +1,3 @@
tmp/
db.sqlite3
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -25,11 +22,12 @@ var/
*.egg-info/
.installed.cfg
*.egg
node_modules/
data/
# Continer extras
.vagrant
_builds
_steps
_projects
# PyInstaller
# Usually these files are written by a python script from a template
@@ -46,7 +44,6 @@ htmlcov/
.tox/
.coverage
.cache
.pytest_cache
nosetests.xml
coverage.xml
@@ -69,9 +66,19 @@ target/
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
#Built dependencies
pipeline/built_assets
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle:
# .idea/gradle.xml
@@ -99,5 +106,3 @@ atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
.vscode/
screenshots/

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
PyRIGS

5
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pyrigs.iml" filepath="$PROJECT_DIR$/.idea/pyrigs.iml" />
</modules>
</component>
</project>

5
.idea/scopes/scope_settings.xml generated Normal file
View File

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

7
.idea/vcs.xml generated Normal file
View File

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

View File

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

104
Pipfile
View File

@@ -1,104 +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"
chardet = "~=4.0.0"
configparser = "~=5.0.1"
contextlib2 = "~=0.6.0.post1"
cssselect = "~=1.1.0"
cssutils = "~=1.0.2"
dj-database-url = "~=0.5.0"
dj-static = "~=0.0.6"
Django = "~=3.2"
django-debug-toolbar = "~=4.0.0"
django-filter = "~=2.4.0"
django-ical = "~=1.7.1"
django-recurrence = "~=1.10.3"
django-registration-redux = "~=2.9"
django-reversion = "~=3.0.9"
django-widget-tweaks = "~=1.4.8"
django-htmlmin = "~=0.11.0"
envparse = "*"
gunicorn = "~=20.0.4"
icalendar = "~=4.0.7"
idna = "~=2.10"
Markdown = "~=3.3.3"
msgpack = "~=1.0.2"
pep517 = "~=0.9.1"
Pillow = "~=9.3.0"
premailer = "~=3.7.0"
progress = "~=1.5"
psutil = "~=5.8.0"
psycopg2 = "~=2.8.6"
Pygments = "~=2.15.0"
pyparsing = "~=2.4.7"
PyPDF2 = "~=1.27.5"
PyPOM = "~=2.2.4"
python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21"
pytz = "~=2020.5"
reportlab = "*"
requests = "~=2.31.0"
retrying = "~=1.3.3"
simplejson = "~=3.17.2"
six = "~=1.15.0"
soupsieve = "~=2.1"
sqlparse = "~=0.4.2"
static3 = "~=0.7.0"
svg2rlg = "~=0.3"
tini = "~=3.0.1"
tornado = "~=6.3"
urllib3 = "~=1.26.5"
whitenoise = "~=5.2.0"
yolk = "~=0.4.3"
zipp = "~=3.4.0"
"zope.component" = "~=4.6.2"
"zope.deferredimport" = "~=4.3.1"
"zope.deprecation" = "~=4.4.0"
"zope.event" = "~=4.5.0"
"zope.hookable" = "~=5.0.1"
"zope.interface" = "~=5.2.0"
"zope.proxy" = "~=4.3.5"
"zope.schema" = "~=6.0.1"
sentry-sdk = "*"
diff-match-patch = "*"
python-barcode = "*"
django-hCaptcha = "*"
importlib-metadata = "*"
django-hcaptcha = "*"
"z3c.rml" = "*"
pikepdf = "*"
django-queryable-properties = "*"
django-mass-edit = "*"
selenium = "~=4.9.1"
[dev-packages]
pycodestyle = "~=2.9.1"
coveralls = "*"
django-coverage-plugin = "*"
pytest-cov = "*"
pytest-django = "*"
pluggy = "*"
pytest-splinter = "*"
pytest = "*"
pytest-reverse = "*"
[requires]
python_version = "3.10"
[dev-packages.pytest-xdist]
extras = [ "psutil",]
version = "*"
[dev-packages.PyPOM]
extras = [ "splinter",]
version = "*"

2078
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1 @@
release: python manage.py migrate
web: gunicorn PyRIGS.wsgi --log-file -

View File

@@ -1,79 +1,41 @@
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from RIGS import models
def get_oembed(login_url, request, oembed_view, kwargs):
context = {}
context['oembed_url'] = f"{request.scheme}://{request.META['HTTP_HOST']}{reverse(oembed_view, kwargs=kwargs)}"
context['login_url'] = f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}"
resp = render(request, 'login_redirect.html', context=context)
return resp
def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
def _dec(view_func):
def _checklogin(request, *args, **kwargs):
if request.user.is_authenticated:
return view_func(request, *args, **kwargs)
else:
if oembed_view is not None:
return get_oembed(login_url, request, oembed_view, kwargs)
else:
return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
_checklogin.__doc__ = view_func.__doc__
_checklogin.__dict__ = view_func.__dict__
return _checklogin
return _dec
def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
def user_passes_test_with_403(test_func, login_url=None):
"""
Decorator for views that checks that the user passes the given test.
Anonymous users will be redirected to login_url, while users that fail
the test will be given a 403 error.
If embed_view is set, then a JS redirect will be used, and a application/json+oembed
meta tag set with the url of oembed_view
(oembed_view will be passed the kwargs from the main function)
"""
if not login_url:
from django.conf import settings
login_url = settings.LOGIN_URL
def _dec(view_func):
def _checklogin(request, *args, **kwargs):
if test_func(request.user):
return view_func(request, *args, **kwargs)
elif not request.user.is_authenticated:
if oembed_view is not None:
return get_oembed(login_url, request, oembed_view, kwargs)
else:
return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
elif not request.user.is_authenticated():
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
else:
resp = render(request, '403.html')
resp = render_to_response('403.html', context_instance=RequestContext(request))
resp.status_code = 403
return resp
_checklogin.__doc__ = view_func.__doc__
_checklogin.__dict__ = view_func.__dict__
return _checklogin
return _dec
def permission_required_with_403(perm, login_url=None, oembed_view=None):
def permission_required_with_403(perm, login_url=None):
"""
Decorator for views that checks whether a user has a particular permission
enabled, redirecting to the log-in page or rendering a 403 as necessary.
"""
return user_passes_test_with_403(lambda u: u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)
return user_passes_test_with_403(lambda u: u.has_perm(perm), login_url=login_url)
from RIGS import models
def api_key_required(function):
"""
@@ -81,13 +43,12 @@ def api_key_required(function):
Failed users will be given a 403 error.
Should only be used for urls which include <api_pk> and <api_key> kwargs
"""
def wrap(request, *args, **kwargs):
userid = kwargs.get('api_pk')
key = kwargs.get('api_key')
error_resp = render(request, '403.html')
error_resp = render_to_response('403.html', context_instance=RequestContext(request))
error_resp.status_code = 403
if key is None:
@@ -97,31 +58,10 @@ def api_key_required(function):
try:
user_object = models.Profile.objects.get(pk=userid)
except models.Profile.DoesNotExist:
except Profile.DoesNotExist:
return error_resp
if user_object.api_key != key:
return error_resp
return function(request, *args, **kwargs)
return wrap
def nottinghamtec_address_required(function):
"""
Checks that the current user has an email address ending @nottinghamtec.co.uk
"""
def wrap(request, *args, **kwargs):
# Fail if current user's email address isn't @nottinghamtec.co.uk
if not request.user.email.endswith('@nottinghamtec.co.uk'):
error_resp = render(request, 'eventauthorisation_request_error.html')
return error_resp
return function(request, *args, **kwargs)
return wrap
def not_estates():
return user_passes_test_with_403(lambda u: not u.email.endswith('@nottingham.ac.uk'))
return wrap

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
DATETIME_FORMAT = ('d/m/Y H:i')
DATE_FORMAT = ('d/m/Y')
TIME_FORMAT = ('H:i')
TIME_FORMAT = ('H:i')

View File

@@ -8,106 +8,83 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/
"""
import datetime
from pathlib import Path
import secrets
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from envparse import env
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Build paths inside the project like this: BASE_DIR / 'subdir'.
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!
SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e')
SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get('SECRET_KEY') else 'gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG', cast=bool, default=True)
STAGING = env('STAGING', cast=bool, default=False)
CI = env('CI', cast=bool, default=False)
DEBUG = bool(int(os.environ.get('DEBUG'))) if os.environ.get('DEBUG') else True
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
if STAGING:
ALLOWED_HOSTS.append('.herokuapp.com')
if DEBUG:
ALLOWED_HOSTS.append('localhost')
ALLOWED_HOSTS.append('example.com')
ALLOWED_HOSTS.append('127.0.0.1')
ALLOWED_HOSTS.append('.github.dev')
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
if not DEBUG:
SECURE_SSL_REDIRECT = True # Redirect all http requests to https
INTERNAL_IPS = ['127.0.0.1']
DOMAIN = env('DOMAIN', default='example.com')
ADMINS = (
('Tom Price', 'tomtom5152@gmail.com')
)
ADMINS = [('IT Manager', f'it@{DOMAIN}'), ('Arona Jones', f'arona.jones@{DOMAIN}')]
if DEBUG:
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
# Application definition
INSTALLED_APPS = (
'whitenoise.runserver_nostatic',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'versioning',
'users',
'RIGS',
'assets',
'training',
'rigForms',
# 'debug_toolbar',
'debug_toolbar',
'registration',
'reversion',
'captcha',
'widget_tweaks',
'hcaptcha',
'massadmin',
'raven.contrib.django.raven_compat',
)
MIDDLEWARE = (
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
# 'debug_toolbar.middleware.DebugToolbarMiddleware',
MIDDLEWARE_CLASSES = (
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'htmlmin.middleware.HtmlMinifyMiddleware',
'htmlmin.middleware.MarkRequestMiddleware',
)
ROOT_URLCONF = 'PyRIGS.urls'
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': str(BASE_DIR / 'db.sqlite3'),
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
if not DEBUG:
import dj_database_url
DATABASES['default'] = dj_database_url.config()
# Logging
# Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
@@ -135,12 +112,12 @@ LOGGING = {
'mail_admins': {
'class': 'django.utils.log.AdminEmailHandler',
'level': 'ERROR',
# But the emails are plain text by default - HTML is nicer
# But the emails are plain text by default - HTML is nicer
'include_html': True,
},
},
'loggers': {
# Again, default Django configuration to email unhandled exceptions
# Again, default Django configuration to email unhandled exceptions
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
@@ -155,63 +132,43 @@ LOGGING = {
}
}
# Tests lock up SQLite otherwise
if STAGING or CI:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
}
}
elif DEBUG:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
}
}
else:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'cache_table',
}
}
import raven
# Error/performance monitoring
sentry_sdk.init(
dsn=env('SENTRY_DSN', default=""),
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
RAVEN_CONFIG = {
'dsn': os.environ.get('RAVEN_DSN'),
# If you are using git, you can also automatically configure the
# release based on the git info.
# 'release': raven.fetch_git_sha(os.path.dirname(os.path.dirname(__file__))),
}
# User system
AUTH_USER_MODEL = 'RIGS.Profile'
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/user/login/'
LOGOUT_URL = '/user/logout/'
LOGIN_URL = '/user/login'
LOGOUT_URL = '/user/logout'
ACCOUNT_ACTIVATION_DAYS = 7
# CAPTCHA settings
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001')
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000')
# reCAPTCHA settings
RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', None)
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', None)
NOCAPTCHA = True
# Email
EMAILER_TEST = False
if not DEBUG or EMAILER_TEST:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = env('EMAIL_HOST')
EMAIL_PORT = env('EMAIL_PORT', cast=int, default=25)
EMAIL_HOST_USER = env('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
EMAIL_USE_TLS = env('EMAIL_USE_TLS', cast=bool, default=False)
EMAIL_USE_SSL = env('EMAIL_USE_SSL', cast=bool, default=False)
DEFAULT_FROM_EMAIL = env('EMAIL_FROM')
EMAIL_HOST = os.environ.get('EMAIL_HOST')
EMAIL_PORT = int(os.environ.get('EMAIL_PORT'))
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
EMAIL_USE_TLS = bool(int(os.environ.get('EMAIL_USE_TLS', 0)))
EMAIL_USE_SSL = bool(int(os.environ.get('EMAIL_USE_SSL', 0)))
DEFAULT_FROM_EMAIL = os.environ.get('EMAIL_FROM')
else:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_COOLDOWN = datetime.timedelta(minutes=15)
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
@@ -221,54 +178,39 @@ TIME_ZONE = 'Europe/London'
FORMAT_MODULE_PATH = 'PyRIGS.formats'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Need to allow seconds as datetime-local input type spits out a time that has seconds
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M','%Y-%m-%dT%H:%M:%S')
TEMPLATE_CONTEXT_PROCESSORS = (
"django.contrib.auth.context_processors.auth",
"django.core.context_processors.debug",
"django.core.context_processors.i18n",
"django.core.context_processors.media",
"django.core.context_processors.static",
"django.core.context_processors.tz",
"django.core.context_processors.request",
"django.contrib.messages.context_processors.messages",
)
# Static files (CSS, JavaScript, Images)
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# https://docs.djangoproject.com/en/1.7/howto/static-files/
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
STATIC_URL = '/static/'
STATIC_ROOT = str(BASE_DIR / 'static/')
STATICFILES_DIRS = [
str(BASE_DIR / 'pipeline/built_assets'),
]
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
STATIC_DIRS = (
os.path.join(BASE_DIR, 'static/')
)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
BASE_DIR / 'templates'
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.debug",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.template.context_processors.request",
"django.contrib.messages.context_processors.messages",
],
'debug': DEBUG
},
},
]
TEMPLATE_DIRS = (
os.path.join(BASE_DIR, 'templates'),
)
USE_GRAVATAR = True
USE_GRAVATAR=True
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
SECURE_HSTS_SECONDS = 3600
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = env('SESSION_COOKIE_SECURE_ENABLED', True)
CSRF_COOKIE_SECURE = env('CSRF_COOKIE_SECURE_ENABLED', True)
SECURE_HSTS_PRELOAD = True

View File

@@ -1,105 +0,0 @@
import os
import pathlib
import sys
from datetime import datetime
import pytz
from django.conf import settings
from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from RIGS import models as rigsmodels
from . import pages
from pytest_django.asserts import assertContains
def create_datetime(year, month, day, hour, minute):
tz = pytz.timezone(settings.TIME_ZONE)
return tz.localize(datetime(year, month, day, hour, minute)).astimezone(tz)
def create_browser():
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
options.add_argument("--headless")
if settings.CI:
options.add_argument("--no-sandbox")
driver = webdriver.Chrome(options=options)
return driver
class BaseTest(LiveServerTestCase):
def setUp(self):
super().setUpClass()
self.driver = create_browser()
self.wait = WebDriverWait(self.driver, 15)
def tearDown(self):
super().tearDown()
self.driver.quit()
class AutoLoginTest(BaseTest):
def setUp(self):
super().setUp()
self.profile = rigsmodels.Profile(
username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True)
self.profile.set_password("EventTestPassword")
self.profile.save()
login_page = pages.LoginPage(self.driver, self.live_server_url).open()
login_page.login("EventTest", "EventTestPassword")
# FIXME Refactor as a pytest fixture
def screenshot_failure(func):
def wrapper_func(self, *args, **kwargs):
try:
func(self, *args, **kwargs)
except Exception as e:
screenshot_name = func.__module__ + "." + func.__qualname__
screenshot_file = "screenshots/" + func.__qualname__ + ".png"
if not pathlib.Path("screenshots").is_dir():
os.mkdir("screenshots")
self.driver.save_screenshot(screenshot_file)
print(f"Error in test {screenshot_name} is at path {screenshot_file}", file=sys.stderr)
raise e
return wrapper_func
def screenshot_failure_cls(cls):
for attr in cls.__dict__:
if callable(getattr(cls, attr)) and attr.startswith("test"):
setattr(cls, attr, screenshot_failure(getattr(cls, attr)))
return cls
def assert_times_almost_equal(first_time, second_time):
assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0)
def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url):
# Test the meta tag is in place
response = client.get(event_url, follow=True, HTTP_HOST='example.com')
assertContains(response, 'application/json+oembed')
assertContains(response, oembed_url)
# Test that the JSON exists
response = client.get(oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assertContains(response, event_embed_url)
# Should also work for non-existant events
response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assertContains(response, alt_event_embed_url)
def login(client, django_user_model):
pwd = 'testuser'
usr = 'TestUser'
user = django_user_model.objects.create_user(username=usr, email="TestUser@test.com", password=pwd,
is_superuser=True,
is_active=True, is_staff=True)
assert client.login(username=usr, password=pwd)
return user

View File

@@ -1,86 +0,0 @@
from pypom import Page
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from PyRIGS.tests import regions
class BasePage(Page):
form_items = {}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __getattr__(self, name):
if name in self.form_items:
element = self.form_items[name]
form_element = element[0](self, self.find_element(*element[1]))
return form_element.value
else:
return super().__getattribute__(name)
def __setattr__(self, name, value):
if name in self.form_items:
element = self.form_items[name]
form_element = element[0](self, self.find_element(*element[1]))
form_element.set_value(value)
else:
self.__dict__[name] = value
class FormPage(BasePage):
_errors_selector = (By.CLASS_NAME, "alert-danger")
_submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
def remove_all_required(self):
self.driver.execute_script(
"Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
self.driver.execute_script(
"Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
def submit(self):
previous_errors = self.errors
submit = self.find_element(*self._submit_locator)
ActionChains(self.driver).move_to_element(submit).perform()
submit.click()
self.wait.until(animation_is_finished())
self.wait.until(lambda x: self.errors != previous_errors or self.success)
@property
def errors(self):
try:
error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector))
return error_page.errors
except NoSuchElementException:
return None
class LoginPage(BasePage):
URL_TEMPLATE = '/user/login'
_username_locator = (By.ID, 'id_username')
_password_locator = (By.ID, 'id_password')
_submit_locator = (By.ID, 'id_submit')
_error_locator = (By.CSS_SELECTOR, '.errorlist>li')
def login(self, username, password):
username_element = self.find_element(*self._username_locator)
username_element.clear()
username_element.send_keys(username)
password_element = self.find_element(*self._password_locator)
password_element.clear()
password_element.send_keys(password)
self.find_element(*self._submit_locator).click()
class animation_is_finished():
def __call__(self, driver):
number_animating = driver.execute_script('return $(":animated").length')
finished = number_animating == 0
if finished:
import time
time.sleep(0.1)
return finished

View File

@@ -1,262 +0,0 @@
import datetime
from django.conf import settings
from pypom import Region
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.select import Select
def parse_bool_from_string(string):
# Used to convert from attribute strings to boolean values, written after I found this:
# >>> bool("false")
# True
if string == "true":
return True
else:
return False
def get_time_format():
# Default
time_format = "%H%M"
if settings.CI: # The CI is American
time_format = "%I%M%p"
return time_format
def get_date_format():
date_format = "%d%m%Y"
if settings.CI: # And try as I might I can't stop it being so
date_format = "%m%d%Y"
return date_format
class BootstrapSelectElement(Region):
_main_button_locator = (By.CSS_SELECTOR, 'button.dropdown-toggle')
_option_box_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu')
_option_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu.inner>li>a.dropdown-item')
_select_all_locator = (By.CLASS_NAME, 'bs-select-all')
_deselect_all_locator = (By.CLASS_NAME, 'bs-deselect-all')
_search_locator = (By.CSS_SELECTOR, '.bs-searchbox>input')
_status_locator = (By.CLASS_NAME, 'status')
@property
def is_open(self):
return parse_bool_from_string(self.find_element(*self._main_button_locator).get_attribute("aria-expanded"))
def toggle(self):
original_state = self.is_open
option_box = self.find_element(*self._option_box_locator)
if not original_state:
self.wait.until(expected_conditions.invisibility_of_element(option_box))
else:
self.wait.until(expected_conditions.visibility_of(option_box))
return self.find_element(*self._main_button_locator).click()
def open(self):
if not self.is_open:
self.toggle()
def close(self):
if self.is_open:
self.toggle()
def select_all(self):
self.find_element(*self._select_all_locator).click()
def deselect_all(self):
self.find_element(*self._deselect_all_locator).click()
def search(self, query):
# self.wait.until(expected_conditions.visibility_of_element_located(self._status_locator))
search_box = self.find_element(*self._search_locator)
self.open()
search_box.clear()
search_box.send_keys(query)
self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator))
@property
def options(self):
options = list(self.find_elements(*self._option_locator))
return [self.BootstrapSelectOption(self, i) for i in options]
def set_option(self, name, selected):
options = [x for x in self.options if x.name == name]
assert len(options) == 1
options[0].set_selected(selected)
class BootstrapSelectOption(Region):
_text_locator = (By.CLASS_NAME, 'text')
@property
def selected(self):
return parse_bool_from_string(self.root.get_attribute("aria-selected"))
def toggle(self):
self.root.click()
def set_selected(self, selected):
if self.selected != selected:
self.toggle()
@property
def name(self):
return self.find_element(*self._text_locator).text
class TextBox(Region):
@property
def value(self):
return self.root.get_attribute("value")
def set_value(self, value):
self.root.clear()
self.root.send_keys(value)
class SimpleMDETextArea(Region):
@property
def value(self):
return self.driver.execute_script("return document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().getValue();", self.root.get_attribute("id"))
def set_value(self, value):
self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
class CheckBox(Region):
def toggle(self):
self.root.click()
@property
def value(self):
return parse_bool_from_string(self.root.get_attribute("checked"))
def set_value(self, value):
if value != self.value:
self.toggle()
class RadioSelect(Region): # Currently only works for yes/no radio selects
def set_value(self, value):
if value:
value = "0"
else:
value = "1"
self.find_element(By.XPATH, f"//label[@for='{self.root.get_attribute('id')}_{value}']").click()
@property
def value(self):
try:
return parse_bool_from_string(self.find_element(By.CSS_SELECTOR, '.custom-control-input:checked').get_attribute("value").lower())
except NoSuchElementException:
return None
class DatePicker(Region):
@property
def value(self):
return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d")
def set_value(self, value):
self.root.clear()
self.root.send_keys(value.strftime(get_date_format()))
class TimePicker(Region):
@property
def value(self):
return datetime.datetime.strptime(self.root.get_attribute("value"), "%H:%M")
def set_value(self, value):
self.root.clear()
self.root.send_keys(value.strftime(get_time_format()))
class DateTimePicker(Region):
@property
def value(self):
return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d %H:%M")
def set_value(self, value):
self.root.clear()
date = value.date().strftime(get_date_format())
time = value.time().strftime(get_time_format())
self.root.send_keys(date)
self.root.send_keys(Keys.TAB)
self.root.send_keys(time)
class SingleSelectPicker(Region):
@property
def value(self):
picker = Select(self.root)
return picker.first_selected_option.text
def set_value(self, value):
picker = Select(self.root)
picker.select_by_visible_text(value)
class ErrorPage(Region):
_error_item_selector = (By.CSS_SELECTOR, "dl>span")
class ErrorItem(Region):
_field_selector = (By.CSS_SELECTOR, "dt")
_error_selector = (By.CSS_SELECTOR, "dd>ul>li")
@property
def field_name(self):
return self.find_element(*self._field_selector).text
@property
def errors(self):
return [x.text for x in self.find_elements(*self._error_selector)]
@property
def errors(self):
error_items = [self.ErrorItem(self, x) for x in self.find_elements(*self._error_item_selector)]
errors = {}
for error in error_items:
errors[error.field_name] = error.errors
return errors
class Modal(Region):
_submit_locator = (By.CSS_SELECTOR, '.btn-primary')
_header_selector = (By.TAG_NAME, 'h4')
form_items = {
'name': (TextBox, (By.ID, 'id_name'))
}
@property
def header(self):
return self.find_element(*self._header_selector).text
@property
def is_open(self):
return self.root.is_displayed()
def submit(self):
self.root.find_element(*self._submit_locator).click()
def __getattr__(self, name):
if name in self.form_items:
element = self.form_items[name]
form_element = element[0](self, self.find_element(*element[1]))
return form_element.value
else:
return super().__getattribute__(name)
def __setattr__(self, name, value):
if name in self.form_items:
element = self.form_items[name]
form_element = element[0](self, self.find_element(*element[1]))
form_element.set_value(value)
else:
self.__dict__[name] = value

View File

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

View File

@@ -1,45 +1,26 @@
from django.conf import settings
from django.conf.urls import include
from django.conf.urls import patterns, include, url
from django.contrib import admin
from django.contrib.auth.decorators import login_required
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path
from django.views.generic import TemplateView
from django.conf import settings
from registration.backends.default.views import RegistrationView
import RIGS
from RIGS import regbackend
from PyRIGS.decorators import not_estates
urlpatterns = patterns('',
# Examples:
# url(r'^$', 'PyRIGS.views.home', name='home'),
# url(r'^blog/', include('blog.urls')),
from PyRIGS import views
url(r'^', include('RIGS.urls')),
url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail),
name="registration_register"),
url('^user/', include('django.contrib.auth.urls')),
url('^user/', include('registration.backends.default.urls')),
urlpatterns = [
path('', include('versioning.urls')),
path('', include('RIGS.urls')),
path('assets/', include('assets.urls')),
path('training/', include('training.urls')),
url(r'^admin/', include(admin.site.urls)),
path('', not_estates()(views.Index.as_view()), name='index'),
# API
path('api/<str:model>/', not_estates()(views.SecureAPIRequest.as_view()),
name="api_secure"),
path('api/<str:model>/<int:pk>/', not_estates()(views.SecureAPIRequest.as_view()),
name="api_secure"),
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
path('search/', not_estates()(views.Search.as_view()), name='search'),
path('search_help/', not_estates()(views.SearchHelp.as_view()), name='search_help'),
path('', include('users.urls')),
path('admin/', include('massadmin.urls')),
path('admin/', admin.site.urls),
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
]
url(r'^forms/', include('rigForms.urls')),
)
if settings.DEBUG:
urlpatterns += staticfiles_urlpatterns()
import debug_toolbar
urlpatterns += [
path('__debug__/', include(debug_toolbar.urls)),
path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")),
]
urlpatterns += staticfiles_urlpatterns()

View File

@@ -1,385 +0,0 @@
import datetime
import operator
import re
import urllib.error
import urllib.parse
import urllib.request
from functools import reduce
from itertools import chain
from io import BytesIO
from PyPDF2 import PdfFileMerger, PdfFileReader
from z3c.rml import rml2pdf
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.core import serializers
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy, reverse, NoReverseMatch
from django.views import generic
from django.views.decorators.clickjacking import xframe_options_exempt
from django.template.loader import get_template
from django.utils import timezone
from RIGS import models
from assets import models as asset_models
from training import models as training_models
def is_ajax(request):
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
def get_related(form, context): # Get some other objects to include in the form. Used when there are errors but also nice and quick.
for field, model in form.related_models.items():
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
class Index(generic.TemplateView): # Displays the current rig count along with a few other bits and pieces
template_name = 'index.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['rig_count'] = models.Event.objects.rig_count()
context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now()).exclude(status=models.Event.CANCELLED).filter(is_rig=True, dry_hire=False)
return context
class SecureAPIRequest(generic.View):
models = {
'venue': models.Venue,
'person': models.Person,
'organisation': models.Organisation,
'profile': models.Profile,
'event': models.Event,
'asset': asset_models.Asset,
'supplier': asset_models.Supplier,
'training_item': training_models.TrainingItem,
}
perms = {
'venue': 'RIGS.view_venue',
'person': 'RIGS.view_person',
'organisation': 'RIGS.view_organisation',
'profile': 'RIGS.view_profile',
'event': None,
'asset': None,
'supplier': None,
'training_item': None,
}
'''
Validate the request is allowed based on user permissions.
Raises 403 if denied.
Potential to add API key validation at a later date.
'''
def __validate__(self, request, key, perm):
if request.user.is_active:
if request.user.is_superuser or perm is None:
return True
elif request.user.has_perm(perm):
return True
raise PermissionDenied()
def get(self, request, model, pk=None, param=None):
# Request permission validation things
key = request.GET.get('apikey', None)
perm = self.perms[model]
self.__validate__(request, key, perm)
# Response format where applicable
format = request.GET.get('format', 'json')
fields = request.GET.get('fields', None)
if fields:
fields = fields.split(",")
filters = request.GET.get('filters', [])
if filters:
filters = filters.split(",")
# Supply data for one record
if pk:
object = get_object_or_404(self.models[model], pk=pk)
data = serializers.serialize(format, [object], fields=fields)
return HttpResponse(data, content_type="application/" + format)
# Supply data for autocomplete ajax request in json form
term = request.GET.get('q', None)
if term:
if fields is None: # Default to just name
fields = ['name']
# Build a list of Q objects for use later
queries = []
for part in term.split(" "):
qs = []
for field in fields:
q = Q(**{field + "__icontains": part})
qs.append(q)
queries.append(reduce(operator.or_, qs))
for f in filters:
q = Q(**{f: True})
queries.append(q)
# Build the data response list
results = []
query = reduce(operator.and_, queries)
objects = self.models[model].objects.filter(query)
# Returning unactivated or unapproved users when they are elsewhere filtered out of the default queryset leads to some *very* unexpected results
if model == "profile":
objects = objects.filter(is_active=True, is_approved=True)
for o in objects:
name = o.display_name if hasattr(o, 'display_name') else o.name
data = {
'pk': o.pk,
'value': o.pk,
'text': name,
}
try: # See if there is a valid update URL
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
except NoReverseMatch:
pass
results.append(data)
# return a data response
return JsonResponse(results, safe=False)
start = request.GET.get('start', None)
end = request.GET.get('end', None)
if model == "event" and start and end:
# Probably a calendar request
start_datetime = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S")
end_datetime = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S")
objects = self.models[model].objects.events_in_bounds(start_datetime, end_datetime)
results = []
for item in objects:
data = {
'pk': item.pk,
'title': item.name,
'is_rig': item.is_rig,
'status': str(item.get_status_display()),
'earliest': item.earliest_time.isoformat(),
'latest': item.latest_time.isoformat(),
'url': str(item.get_absolute_url())
}
results.append(data)
return JsonResponse(results, safe=False)
return HttpResponse(model)
class ModalURLMixin:
def get_close_url(self, update, detail):
if is_ajax(self.request):
url = reverse_lazy('closemodal')
update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk}))
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
messages.info(self.request, f"modalobject[0]['update_url']='{update_url}'")
else:
url = reverse_lazy(detail, kwargs={
'pk': self.object.pk,
})
return url
class GenericListView(generic.ListView):
template_name = 'generic_list.html'
paginate_by = 20
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = self.model.__name__ + "s"
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
def get_queryset(self):
object_list = self.model.objects.search(query=self.request.GET.get('q', ""))
orderBy = self.request.GET.get('orderBy', "name")
if orderBy != "":
object_list = object_list.order_by(orderBy)
return object_list
class GenericDetailView(generic.DetailView):
template_name = "generic_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"{self.model.__name__} | {self.object.name}"
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
class GenericUpdateView(generic.UpdateView):
template_name = "generic_form.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Edit {self.model.__name__}"
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
class GenericCreateView(generic.CreateView):
template_name = "generic_form.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = f"Create {self.model.__name__}"
if is_ajax(self.request):
context['override'] = "base_ajax.html"
return context
class Search(generic.ListView):
template_name = 'search_results.html'
paginate_by = 20
count = 0
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['count'] = self.count or 0
context['query'] = self.request.GET.get('q')
context['page_title'] = f"{context['count']} search results for <b>{context['query']}</b>"
return context
def get_queryset(self):
request = self.request
query = request.GET.get('q', None)
if query is not None:
event_results = models.Event.objects.search(query)
person_results = models.Person.objects.search(query)
organisation_results = models.Organisation.objects.search(query)
venue_results = models.Venue.objects.search(query)
invoice_results = models.Invoice.objects.search(query)
asset_results = asset_models.Asset.objects.search(query)
supplier_results = asset_models.Supplier.objects.search(query)
trainee_results = training_models.Trainee.objects.search(query)
training_item_results = training_models.TrainingItem.objects.search(query)
# combine querysets
queryset_chain = chain(
event_results,
person_results,
organisation_results,
venue_results,
invoice_results,
asset_results,
supplier_results,
trainee_results,
training_item_results,
)
qs = sorted(queryset_chain,
key=lambda instance: instance.pk,
reverse=True)
self.count = len(qs) # since qs is actually a list
return qs
return models.Event.objects.none() # just an empty queryset as default
class SearchHelp(generic.TemplateView):
template_name = 'search_help.html'
class CloseModal(generic.TemplateView):
"""
Called from a modal window (e.g. when an item is submitted to an event/invoice).
May optionally also include some javascript in a success message to cause a load of
the new information onto the page.
"""
template_name = 'closemodal.html'
def get_context_data(self, **kwargs):
return {'messages': messages.get_messages(self.request)}
class OEmbedView(generic.View):
def get(self, request, pk=None):
embed_url = reverse(self.url_name, args=[pk])
full_url = f"{request.scheme}://{request.META['HTTP_HOST']}{embed_url}"
data = {
'html': f'<iframe src="{full_url}" frameborder="0" width="100%" height="250"></iframe>',
'version': '1.0',
'type': 'rich',
'height': '250'
}
return JsonResponse(data)
def get_info_string(user):
user_str = f"by {user.name} " if user else ""
time = timezone.now().strftime('%d/%m/%Y %H:%I')
return f"[Paperwork generated {user_str}on {time}"
def render_pdf_response(template, context, append_terms):
merger = PdfFileMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
if append_terms:
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(BytesIO(terms.read()))
merged = BytesIO()
merger.write(merged)
response = HttpResponse(content_type='application/pdf')
f = context['filename']
response['Content-Disposition'] = f'filename="{f}"'
response.write(merged.getvalue())
return response
class PrintView(generic.View):
append_terms = False
def get_context_data(self, **kwargs):
obj = get_object_or_404(self.model, pk=self.kwargs['pk'])
object_name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', obj.name)
context = {
'object': obj,
'current_user': self.request.user,
'object_name': object_name,
'info_string': get_info_string(self.request.user) + f"- {obj.current_version_id}]",
}
return context
def get(self, request, pk):
return render_pdf_response(get_template(self.template_name), self.get_context_data(), self.append_terms)
class PrintListView(generic.ListView):
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['current_user'] = self.request.user
context['info_string'] = get_info_string(self.request.user) + "]"
return context
def get(self, request):
self.object_list = self.get_queryset()
return render_pdf_response(get_template(self.template_name), self.get_context_data(), False)

View File

@@ -7,10 +7,9 @@ For more information on this file, see
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "PyRIGS.settings")
from django.core.wsgi import get_wsgi_application # noqa
from dj_static import Cling # noqa
from django.core.wsgi import get_wsgi_application
from dj_static import Cling
application = Cling(get_wsgi_application())

View File

@@ -1,19 +1,78 @@
# TEC PA & Lighting - PyRIGS #
![Build Status](https://github.com/nottinghamtec/PyRIGS/workflows/Django%20CI/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS)
[![Maintainability](https://api.codeclimate.com/v1/badges/79ca3b8106911a1d143f/maintainability)](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability)
[![wercker status](https://app.wercker.com/status/2dbe0517c3d83859c985ffc5a55a2802/m/master "wercker status")](https://app.wercker.com/project/bykey/2dbe0517c3d83859c985ffc5a55a2802)
Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets.
Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails.
For setup information and other such helpful stuff check the [Wiki](https://github.com/nottinghamtec/PyRIGS/wiki)
The purpose of this project is to make the system more compatible and easier to understand such that should future changes be needed they can be made without having to understand the intricacies of Rails.
# Apps
- PyRIGS: Base app, stores 'global' information
- RIGS: Rigboard stuff - event calendar etc
- assets: Database of our kit, testing data etc
- training: Logs in-house training within various "departments" (sound, lighting etc).
- versioning: Our custom logic built on top of django-reversion. Semi-modular.
- users: Our custom logic for registration and profiles. Semi-modular.
At this stage the project is very early on, and the main focus has been on getting a working system that can be tested and put into use ASAP due to the imminent failure of the existing system. Because of this, the documentation is still quite weak, but this should be fixed as time goes on.
This document is intended to get you up and running, but if don't care about what I have to say, just clone the sodding repository and have a poke around with what's in it, but for GODS SAKE DO NOT PUSH WITHOUT TESTING.
[![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com)
### What is this repository for? ###
For the rapid development of the application for medium term deployment, the main branch is being used.
Once the application is deployed in a production environment, other branches should be used to properly stage edits and pushes of new features. When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production.
Most of the documents here assume a basic knowledge of how Python and Django work (hint, if I don't say something, Google it, you will find 10000's of answers). The documentation is purely to be specific to TEC's application of the framework.
### Editing ###
It is recommended that you use the PyCharm IDE by JetBrains. Whilst other editors are available, this is the best for integration with Django as it can automatically manage all the pesky admin commands that frequently need running, as well as nice integration with git.
For the more experienced developer/somebody who doesn't want a full IDE and wants it to open in less than the age of the universe, I can strongly recommend [Sublime Text](http://www.sublimetext.com/). It has a bit of a steeper learning curve, and won't manage anything Django/git related out of the box, but once you get the hang of it is by far the fastest and most powerful editor I have used (for any type of project).
Please contact TJP for details on how to acquire these.
### Python Environment ###
Whilst the Python version used is not critical to the running of the application, using the same version usually helps avoid a lot of issues. Mainly the C implementation of Python 2 (CPython 2) has been used (specifically the Python 2.7 standard). Most of the application has been written with Python 3 in mind however, and should run without issue. Some level of testing on Python 3 has been done, but there is no guarantee it will work (for more information on this please see [[Python Version]] on the wiki)
Once you have your Python distribution installed, go ahead an follow the steps to set up a virtualenv, which will isolate the project from the system environment.
#### PyCharm ####
If you are using the prefered PyCharm IDE, then this should be quite easy.
1. Select "File/Settings" -> "Project Interpreter"
2. Click the small cog in the top right
3. Select "Create VirtualEnv"
4. Enter a name and a location. This doesn't matter where, just make sure it makes sense and you remember it incase you need it later (I recommend calling it "pyrigs" in "~/.virtualenvs/pyrigs")
5. Select the base interpreter to your Python 3 base interpreter (Python 2 will work, just be careful)
6. Click OK, you *don't* want to inherit global packages or make it available to all projects.
7. Open a file such as manage.py. PyCharm should winge that dependances aren't installed. This might take a while to register, but give it change. When it does, click the button to install them and let it do it's thing. If for some reason PyCharm should decide that it doesn't want to help you here, see below for the console instructions on how to do this manually.
To run the Django application follow these steps
1. Select "Run/Edit Configurations"
2. Create a new "Django server", give it a sensible name for when you need it later.
3. You might need to set the interpreter to be your virtualenv.
4. Click "OK"
5. Run the application
#### Console Based ####
If you aren't using PyCharm, or want to use a console for some reason, this is really easy, there is even [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) to help things along. Simply run
```
virtualenv <dir>
```
Where dir is the directory you wish to create the virtualenv in.
Next activate the virtualenv.
```
Windows
<virtualenv_dir>/Scripts/activate.bat
Unix
source <virtualenv_dir>/bin/activate
```
Finally install the requirements using pip
```
cd <pyrigs project directory>
pip install -r requirements.txt
```
This might take a while, but be patient and you should then be ready to go.
To run the server under normal conditions when you are already in the virtualenv (see above)
```
python manage.py runserver
```
Please refer to Django documentation for a full list of options available here.
### Committing, pushing and testing ###
Feel free to commit as you wish, on your own branch. On my branch (master for development) do not commit code that you either know doesn't work or don't know works. If you must commit this code, please make sure you say in the commit message that it isn't working, and if you can why it isn't working. If and only if you absolutely must push, then please don't leave it as the HEAD for too long, it's not much to ask but when you are done just make sure you haven't broken the HEAD for the next person.

View File

@@ -1,120 +1,56 @@
from django.contrib import admin
from django.contrib import messages
from django.contrib.admin import helpers
from RIGS import models, forms
from django.contrib.auth.admin import UserAdmin
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_lazy as _
import reversion
from django.contrib.admin import helpers
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 django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.db import IntegrityError
from reversion import revisions as reversion
from reversion.admin import VersionAdmin
from RIGS import models
from users import forms as user_forms
# Register your models here.
admin.site.register(models.VatRate, reversion.VersionAdmin)
admin.site.register(models.Event, reversion.VersionAdmin)
admin.site.register(models.EventItem, reversion.VersionAdmin)
admin.site.register(models.Invoice)
admin.site.register(models.Payment)
admin.site.register(models.VatRate, VersionAdmin)
admin.site.register(models.Event, VersionAdmin)
admin.site.register(models.EventItem, VersionAdmin)
admin.site.register(models.Invoice, VersionAdmin)
admin.site.register(models.EventCheckIn)
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin):
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {
'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'password1', 'password2'),
}),
)
form = forms.ProfileChangeForm
add_form = forms.ProfileCreationForm
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
def merge_model_instances(primary_object, alias_objects):
"""
Merge several model instances into one, the `primary_object`.
Use this function to merge model objects and migrate all of the related
fields from the alias objects the primary object.
"""
# get related fields
related_fields = list(filter(
lambda x: x.is_relation is True,
primary_object._meta.get_fields()))
many_to_many_fields = list(filter(
lambda x: x.many_to_many is True, related_fields))
related_fields = list(filter(
lambda x: x.many_to_many is False, related_fields))
# Loop through all alias objects and migrate their references to the
# primary object
deleted_objects = []
deleted_objects_count = 0
for alias_object in alias_objects:
# Migrate all foreign key references from alias object to primary
# object.
for many_to_many_field in many_to_many_fields:
alias_varname = many_to_many_field.name
related_objects = getattr(alias_object, alias_varname)
for obj in related_objects.all():
try:
# Handle regular M2M relationships.
getattr(alias_object, alias_varname).remove(obj)
getattr(primary_object, alias_varname).add(obj)
except AttributeError:
# Handle M2M relationships with a 'through' model.
# This does not delete the 'through model.
# TODO: Allow the user to delete a duplicate 'through' model.
through_model = getattr(alias_object, alias_varname).through
kwargs = {
many_to_many_field.m2m_reverse_field_name(): obj,
many_to_many_field.m2m_field_name(): alias_object,
}
through_model_instances = through_model.objects.filter(**kwargs)
for instance in through_model_instances:
# Re-attach the through model to the primary_object
setattr(
instance,
many_to_many_field.m2m_field_name(),
primary_object)
instance.save()
# TODO: Here, try to delete duplicate instances that are
# disallowed by a unique_together constraint
for related_field in related_fields:
if related_field.one_to_many:
with transaction.atomic():
try:
alias_varname = related_field.get_accessor_name()
related_objects = getattr(alias_object, alias_varname)
for obj in related_objects.all():
field_name = related_field.field.name
setattr(obj, field_name, primary_object)
obj.save()
except IntegrityError:
pass # Skip to avoid integrity error from unique_together
elif related_field.one_to_one or related_field.many_to_one:
alias_varname = related_field.name
if hasattr(alias_object, alias_varname):
related_object = getattr(alias_object, alias_varname)
primary_related_object = getattr(primary_object, alias_varname)
if primary_related_object is None:
setattr(primary_object, alias_varname, related_object)
primary_object.save()
elif related_field.one_to_one:
related_object.delete()
if alias_object.id:
deleted_objects += [alias_object]
alias_object.delete()
deleted_objects_count += 1
return primary_object, deleted_objects, deleted_objects_count
class AssociateAdmin(VersionAdmin):
class AssociateAdmin(reversion.VersionAdmin):
list_display = ('id', 'name', 'number_of_events')
search_fields = ['id', 'name']
list_display_links = ['id', 'name']
actions = ['merge']
merge_fields = ['name']
def get_queryset(self, request):
return super().get_queryset(request).annotate(event_count=Count('event'))
return super(AssociateAdmin, self).get_queryset(request).annotate(event_count=Count('event'))
def number_of_events(self, obj):
return obj.latest_events.count()
@@ -124,16 +60,24 @@ class AssociateAdmin(VersionAdmin):
def merge(self, request, queryset):
if request.POST.get('post'): # Has the user confirmed which is the master record?
try:
master_object_pk = request.POST.get('master')
master_object = queryset.get(pk=master_object_pk)
masterObjectPk = request.POST.get('master')
masterObject = queryset.get(pk=masterObjectPk)
except ObjectDoesNotExist:
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
return
primary_object, deleted_objects, deleted_objects_count = merge_model_instances(master_object, queryset.exclude(pk=master_object_pk).all())
reversion.set_comment('Merging Objects')
self.message_user(request, f"Objects successfully merged. {deleted_objects_count} old objects deleted.")
with transaction.atomic(), reversion.create_revision():
for obj in queryset.exclude(pk=masterObjectPk):
events = obj.event_set.all()
for event in events:
masterObject.event_set.add(event)
obj.delete()
reversion.set_comment('Merging Objects')
self.message_user(request, "Objects successfully merged.")
return
else: # Present the confirmation screen
class TempForm(ModelForm):
class Meta:
model = queryset.model
@@ -149,37 +93,8 @@ class AssociateAdmin(VersionAdmin):
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
'forms': forms
}
return TemplateResponse(request, 'admin_associate_merge.html', context)
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin, AssociateAdmin):
list_display = ('username', 'name', 'is_approved', 'is_superuser', 'is_supervisor', 'number_of_events', 'last_login')
list_display_links = ['username']
list_filter = UserAdmin.list_filter + ('is_approved',)
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
(_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {
'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'password1', 'password2'),
}),
)
form = user_forms.ProfileChangeForm
add_form = user_forms.ProfileCreationForm
actions = ['approve_user', 'merge']
merge_fields = ['username', 'first_name', 'last_name', 'initials', 'email', 'phone', 'is_supervisor']
def approve_user(modeladmin, request, queryset):
queryset.update(is_approved=True)
return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context,
current_app=self.admin_site.name)
@admin.register(models.Person)
@@ -198,18 +113,3 @@ class VenueAdmin(AssociateAdmin):
class OrganisationAdmin(AssociateAdmin):
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account']
@admin.register(models.RiskAssessment)
class RiskAssessmentAdmin(VersionAdmin):
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
@admin.register(models.EventChecklist)
class EventChecklistAdmin(VersionAdmin):
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')
@admin.register(models.PowerTestRecord)
class EventChecklistAdmin(VersionAdmin):
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')

View File

@@ -1,8 +0,0 @@
from django.apps import AppConfig
class RIGSAppConfig(AppConfig):
name = 'RIGS'
def ready(self):
import RIGS.signals

142
RIGS/finance.py Normal file
View File

@@ -0,0 +1,142 @@
import cStringIO as StringIO
from django.core.urlresolvers import reverse_lazy
from django.db import connection
from django.http import Http404, HttpResponseRedirect
from django.views import generic
from django.template import RequestContext
from django.template.loader import get_template
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.contrib import messages
import datetime
from z3c.rml import rml2pdf
from RIGS import models
import re
class InvoiceIndex(generic.ListView):
model = models.Invoice
template_name = 'RIGS/invoice_list.html'
def get_queryset(self):
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
sql = "SELECT * FROM " \
"(SELECT " \
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" as p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" as p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
"AS sub " \
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
"ORDER BY invoice_date"
query = self.model.objects.raw(sql)
return query
class InvoiceDetail(generic.DetailView):
model = models.Invoice
class InvoicePrint(generic.View):
def get(self, request, pk):
invoice = get_object_or_404(models.Invoice, pk=pk)
object = invoice.event
template = get_template('RIGS/event_print.xml')
copies = ('TEC', 'Client')
context = RequestContext(request, {
'object': object,
'fonts': {
'opensans': {
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'invoice':invoice,
'current_user':request.user,
})
rml = template.render(context)
buffer = StringIO.StringIO()
buffer = rml2pdf.parseString(rml)
pdfData = buffer.read()
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = "filename=Invoice %05d | %s.pdf" % (invoice.pk, escapedEventName)
response.write(pdfData)
return response
class InvoiceVoid(generic.View):
def get(self, *args, **kwargs):
pk = kwargs.get('pk')
object = get_object_or_404(models.Invoice, pk=pk)
object.void = not object.void
object.save()
if object.void:
return HttpResponseRedirect(reverse_lazy('invoice_list'))
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
class InvoiceArchive(generic.ListView):
model = models.Invoice
paginate_by = 25
class InvoiceWaiting(generic.ListView):
model = models.Event
paginate_by = 25
template_name = 'RIGS/event_invoice.html'
def get_queryset(self):
# @todo find a way to select items
events = self.model.objects.filter(is_rig=True, end_date__lt=datetime.date.today(),
invoice__isnull=True) \
.order_by('start_date') \
.select_related('person',
'organisation',
'venue', 'mic')
return events
class InvoiceEvent(generic.View):
def get(self, *args, **kwargs):
epk = kwargs.get('pk')
event = models.Event.objects.get(pk=epk)
invoice, created = models.Invoice.objects.get_or_create(event=event)
if created:
invoice.invoice_date = datetime.date.today()
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))
class PaymentCreate(generic.CreateView):
model = models.Payment
fields = ['invoice','date','amount','method']
def get_initial(self):
initial = super(generic.CreateView, self).get_initial()
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
if invoicepk == None:
raise Http404()
invoice = get_object_or_404(models.Invoice, pk=invoicepk)
initial.update({'invoice': invoice})
return initial
def get_success_url(self):
messages.info(self.request, "location.reload()")
return reverse_lazy('closemodal')
class PaymentDelete(generic.DeleteView):
model = models.Payment
def get_success_url(self):
return self.request.POST.get('next')

View File

@@ -1,24 +1,51 @@
from datetime import datetime
import simplejson
__author__ = 'Ghost'
from django import forms
from django.utils import formats
from django.conf import settings
from django.core import serializers
from django.utils import timezone
from reversion import revisions as reversion
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
from registration.forms import RegistrationFormUniqueEmail
from captcha.fields import ReCaptchaField
import simplejson
from RIGS import models
from training.models import TrainingLevel
# Override the django form defaults to use the HTML date/time/datetime UI elements
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
forms.TimeField.widget = forms.TimeInput(attrs={'type': 'time'}, format='%H:%M')
forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M')
# Registration
class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
captcha = ReCaptchaField()
class Meta:
model = models.Profile
fields = ('username', 'email', 'first_name', 'last_name', 'initials', 'phone')
def clean_initials(self):
"""
Validate that the supplied initials are unique.
"""
if models.Profile.objects.filter(initials__iexact=self.cleaned_data['initials']):
raise forms.ValidationError("These initials are already in use. Please supply different initials.")
return self.cleaned_data['initials']
# Login form
class PasswordReset(PasswordResetForm):
captcha = ReCaptchaField(label='Captcha')
class ProfileCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = models.Profile
class ProfileChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta):
model = models.Profile
# Events Shit
class EventForm(forms.ModelForm):
datetime_input_formats = list(settings.DATETIME_INPUT_FORMATS)
datetime_input_formats = formats.get_format_lazy("DATETIME_INPUT_FORMATS") + settings.DATETIME_INPUT_FORMATS
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
access_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
@@ -44,7 +71,7 @@ class EventForm(forms.ModelForm):
return simplejson.dumps(items)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
super(EventForm, self).__init__(*args, **kwargs)
self.fields['items_json'].initial = self._get_items_json
self.fields['start_date'].widget.format = '%Y-%m-%d'
@@ -91,16 +118,8 @@ class EventForm(forms.ModelForm):
return item
def clean(self):
if self.cleaned_data.get("is_rig") and not (
self.cleaned_data.get('person') or self.cleaned_data.get('organisation')):
raise forms.ValidationError(
'You haven\'t provided any client contact details. Please add a person or organisation.',
code='contact')
return super().clean()
def save(self, commit=True):
m = super().save(commit=False)
m = super(EventForm, self).save(commit=False)
if (commit):
m.save()
@@ -121,123 +140,4 @@ class EventForm(forms.ModelForm):
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
'purchase_order', 'collector', 'forum_url']
class BaseClientEventAuthorisationForm(forms.ModelForm):
tos = forms.BooleanField(required=True, label="Terms of hire")
name = forms.CharField(label="Your Name")
def clean(self):
if self.cleaned_data.get('amount') != self.instance.event.total:
self.add_error('amount', 'The amount authorised must equal the total for the event (inc VAT).')
return super().clean()
class Meta:
abstract = True
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.fields['uni_id'].required = True
self.fields['account_code'].required = True
class Meta:
model = models.EventAuthorisation
fields = ('tos', 'name', 'amount', 'uni_id', 'account_code')
class EventAuthorisationRequestForm(forms.Form):
email = forms.EmailField(required=True, label='Authoriser Email')
class EventRiskAssessmentForm(forms.ModelForm):
related_models = {
'power_mic': models.Profile,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
if str(name) == 'supervisor_consulted':
field.widget = forms.CheckboxInput()
elif field.__class__ == forms.BooleanField:
field.widget = forms.RadioSelect(choices=[
(True, 'Yes'),
(False, 'No')
], attrs={'class': 'custom-control-input', 'required': 'true'})
def clean(self):
if self.cleaned_data.get('big_power'):
if not self.cleaned_data.get('power_mic').level_qualifications.filter(level__department=TrainingLevel.POWER).exists():
self.add_error('power_mic', forms.ValidationError("Your Power MIC must be a Power Technician.", code="power_tech_required"))
# Check expected values
unexpected_values = []
for field, value in models.RiskAssessment.expected_values.items():
if self.cleaned_data.get(field) != value:
unexpected_values.append(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>")
if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'):
raise forms.ValidationError(f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.", code='unusual_answers')
return super().clean()
class Meta:
model = models.RiskAssessment
fields = '__all__'
exclude = ['reviewed_at', 'reviewed_by']
class EventChecklistForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['date'].widget.format = '%Y-%m-%d'
for name, field in self.fields.items():
if field.__class__ == forms.NullBooleanField:
# Only display yes/no to user, the 'none' is only ever set in the background
field.widget = forms.CheckboxInput()
related_models = {
'venue': models.Venue,
}
class Meta:
model = models.EventChecklist
fields = '__all__'
exclude = ['reviewed_at', 'reviewed_by']
class PowerTestRecordForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
if field.__class__ == forms.NullBooleanField:
# Only display yes/no to user, the 'none' is only ever set in the background
field.widget = forms.CheckboxInput()
related_models = {
'venue': models.Venue,
'power_mic': models.Profile,
}
class Meta:
model = models.PowerTestRecord
fields = '__all__'
exclude = ['reviewed_at', 'reviewed_by']
class EventCheckInForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['time'].initial = timezone.now()
self.fields['role'].initial = "Crew"
class Meta:
model = models.EventCheckIn
fields = '__all__'
exclude = ['end_time']
class EditCheckInForm(forms.ModelForm):
class Meta:
model = models.EventCheckIn
fields = '__all__'
'collector', 'purchase_order']

View File

@@ -1,18 +1,17 @@
import datetime
import pytz
from django.conf import settings
from django.db.models import Q
from RIGS import models, forms
from django_ical.views import ICalFeed
from django.db.models import Q
from django.core.urlresolvers import reverse_lazy, reverse, NoReverseMatch
from django.utils import timezone
from django.conf import settings
from RIGS import models
import datetime, pytz
class CalendarICS(ICalFeed):
"""
A simple event calender
"""
# Metadata which is passed on to clients
#Metadata which is passed on to clients
product_id = 'RIGS'
title = 'RIGS Calendar'
timezone = settings.TIME_ZONE
@@ -28,41 +27,39 @@ class CalendarICS(ICalFeed):
def get_object(self, request, *args, **kwargs):
params = {}
params['dry-hire'] = request.GET.get('dry-hire', 'true') == 'true'
params['non-rig'] = request.GET.get('non-rig', 'true') == 'true'
params['rig'] = request.GET.get('rig', 'true') == 'true'
params['dry-hire'] = request.GET.get('dry-hire','true') == 'true'
params['non-rig'] = request.GET.get('non-rig','true') == 'true'
params['rig'] = request.GET.get('rig','true') == 'true'
params['cancelled'] = request.GET.get('cancelled', 'false') == 'true'
params['provisional'] = request.GET.get('provisional', 'true') == 'true'
params['confirmed'] = request.GET.get('confirmed', 'true') == 'true'
params['cancelled'] = request.GET.get('cancelled','false') == 'true'
params['provisional'] = request.GET.get('provisional','true') == 'true'
params['confirmed'] = request.GET.get('confirmed','true') == 'true'
return params
def description(self, params):
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + (
'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + (
'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
def description(self,params):
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + ('Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + ('Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
return desc
def items(self, params):
# include events from up to 1 year ago
#include events from up to 1 year ago
start = datetime.datetime.now() - datetime.timedelta(days=365)
filter = Q(start_date__gte=start)
typeFilters = Q(pk=None) # Need something that is false for every entry
typeFilters = Q(pk=None) #Need something that is false for every entry
if params['dry-hire']:
typeFilters = typeFilters | Q(dry_hire=True, is_rig=True)
if params['non-rig']:
typeFilters = typeFilters | Q(is_rig=False)
if params['rig']:
typeFilters = typeFilters | Q(is_rig=True, dry_hire=False)
statusFilters = Q(pk=None) # Need something that is false for every entry
statusFilters = Q(pk=None) #Need something that is false for every entry
if params['cancelled']:
statusFilters = statusFilters | Q(status=models.Event.CANCELLED)
@@ -73,8 +70,7 @@ class CalendarICS(ICalFeed):
filter = filter & typeFilters & statusFilters
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation',
'venue', 'mic')
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
def item_title(self, item):
title = ''
@@ -91,9 +87,9 @@ class CalendarICS(ICalFeed):
# Add the rig name
title += item.name
# Add the status
title += f' ({item.get_status_display()})'
title += ' ('+str(item.get_status_display())+')'
return title
@@ -101,11 +97,12 @@ class CalendarICS(ICalFeed):
return item.earliest_time
def item_end_datetime(self, item):
# if isinstance(item.latest_time, datetime.date): # Ical end_datetime is non-inclusive, so add a day
# return item.latest_time + datetime.timedelta(days=1)
if type(item.latest_time) is datetime.date: # Ical end_datetime is non-inclusive, so add a day
return item.latest_time + datetime.timedelta(days=1)
return item.latest_time
def item_location(self, item):
def item_location(self,item):
return item.venue
def item_description(self, item):
@@ -114,45 +111,46 @@ class CalendarICS(ICalFeed):
tz = pytz.timezone(self.timezone)
desc = f'Rig ID = {item.display_id}\n'
desc += f'Event = {item.name}\n'
desc = 'Rig ID = '+str(item.pk)+'\n'
desc += 'Event = ' + item.name + '\n'
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
if item.is_rig and item.person:
desc += 'Client = ' + item.person.name + (
(' for ' + item.organisation.name) if item.organisation else '') + '\n'
desc += f'Status = {item.get_status_display()}\n'
desc += 'Client = ' + item.person.name + ( (' for '+item.organisation.name) if item.organisation else '') + '\n'
desc += 'Status = ' + str(item.get_status_display()) + '\n'
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
desc += '\n'
if item.meet_at:
desc += 'Crew Meet = ' + (
item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
desc += 'Crew Meet = ' + (item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
if item.access_at:
desc += 'Access At = ' + (
item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
desc += 'Access At = ' + (item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
if item.start_date:
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + (
(' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + ((' '+item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
if item.end_date:
desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + (
(' ' + item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n'
desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + ((' '+item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n'
desc += '\n'
if item.description:
desc += f'Event Description:\n{item.description}\n\n'
desc += 'Event Description:\n'+item.description+'\n\n'
# if item.notes: // Need to add proper keyholder checks before this gets put back
# desc += 'Notes:\n'+item.notes+'\n\n'
desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}'
# desc += 'Notes:\n'+item.notes+'\n\n'
base_url = "http://rigs.nottinghamtec.co.uk"
desc += 'URL = '+base_url+str(item.get_absolute_url())
return desc
def item_link(self, item):
# Make a link to the event in the web interface
# base_url = "https://pyrigs.nottinghamtec.co.uk"
return item.get_absolute_url()
def item_updated(self, item): # some ical clients will display this
# def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) - not really necessary though
# return ''
def item_updated(self, item): # some ical clients will display this
return item.last_edited_at
def item_guid(self, item): # use the rig-id as the ical unique event identifier
return item.pk
def item_guid(self, item): # use the rig-id as the ical unique event identifier
return item.pk

28
RIGS/importer_tests.py Normal file
View File

@@ -0,0 +1,28 @@
__author__ = 'ghost'
import unittest
from importer import fix_email
class EmailFixerTest(unittest.TestCase):
def test_correct(self):
e = fix_email("tom@ghost.uk.net")
self.assertEqual(e, "tom@ghost.uk.net")
def test_partial(self):
e = fix_email("psytp")
self.assertEqual(e, "psytp@nottingham.ac.uk")
def test_none(self):
old = None
new = fix_email(old)
self.assertEqual(old, new)
def test_empty(self):
old = ""
new = fix_email(old)
self.assertEqual(old, new)
if __name__ == '__main__':
unittest.main()

View File

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

View File

@@ -1,15 +0,0 @@
from django.core.management import call_command
from django.core.management.base import BaseCommand
from RIGS import models
class Command(BaseCommand):
help = 'Adds sample data to use for testing'
can_import_settings = True
def handle(self, *args, **options):
call_command('generateSampleUserData')
call_command('generateSampleRIGSData')
call_command('generateSampleAssetsData')
call_command('generateSampleTrainingData')

View File

@@ -1,290 +0,0 @@
import datetime
import random
from django.contrib.auth.models import Group, Permission
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
from reversion import revisions as reversion
from RIGS import models
class Command(BaseCommand):
help = 'Adds sample data to use for testing'
can_import_settings = True
people = []
organisations = []
venues = []
events = []
profiles = models.Profile.objects.all()
def handle(self, *args, **options):
print("Generating rigboard data")
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
raise CommandError('You cannot run this command in production')
random.seed(
'Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests
with transaction.atomic():
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
self.setup_people()
self.setup_organisations()
self.setup_venues()
self.setup_events()
print("Done generating rigboard data")
def setup_people(self):
names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe",
"Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore",
"Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan",
"Nicolas Flamel", "Cornelius Fudge", "Goyle", "Gregory Goyle", "Hermione Granger", "Rubeus Hagrid",
"Igor Karkaroff", "Viktor Krum", "Bellatrix Lestrange", "Alice Longbottom", "Frank Longbottom",
"Neville Longbottom", "Luna Lovegood", "Xenophilius Lovegood", # noqa
"Remus Lupin", "Draco Malfoy", "Lucius Malfoy", "Narcissa Malfoy", "Olympe Maxime",
"Minerva McGonagall", "Mad-Eye Moody", "Peter Pettigrew", "Harry Potter", "James Potter",
"Lily Potter", "Quirinus Quirrell", "Tom Riddle", "Mary Riddle", "Lord Voldemort", "Rita Skeeter",
"Severus Snape", "Nymphadora Tonks", "Dolores Janes Umbridge", "Arthur Weasley", "Bill Weasley",
"Charlie Weasley", "Fred Weasley", "George Weasley", "Ginny Weasley", "Molly Weasley", "Percy Weasley",
"Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(models.Profile.objects.all()))
person = models.Person.objects.create(name=name)
if i % 3 == 0:
person.email = "address@person.com"
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"
if i % 7 == 0:
person.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
person.phone = "01234 567894"
person.save()
self.people.append(person)
def setup_organisations(self):
names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars",
"ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc",
"Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp",
"Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp",
"North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation",
"Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp",
"Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power",
"Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank",
"Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium",
"Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors",
"Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation",
"U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation",
"Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa
"Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron",
"Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry",
"Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.",
"Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer",
"Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron",
"Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company",
"Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer",
"Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp",
"The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute",
"Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King",
"Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman",
"Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande",
"Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(models.Profile.objects.all()))
new_organisation = models.Organisation.objects.create(name=name)
if i % 2 == 0:
new_organisation.has_su_account = True
if i % 3 == 0:
new_organisation.email = "address@organisation.com"
if i % 5 == 0:
new_organisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
new_organisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
new_organisation.phone = "01234 567894"
new_organisation.save()
self.organisations.append(new_organisation)
def setup_venues(self):
names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch",
"The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands",
"The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins",
"The Vale of Arryn", "The Whispering Wood", "White Harbor", "Winterfell", "The Arbor", "Ashemark",
"Brightwater Keep", "Casterly Rock", "Clegane's Keep", "Dragonstone", "Dorne", "God's Eye",
"The Golden Tooth", # noqa
"Harrenhal", "Highgarden", "Horn Hill", "Fingers", "King's Landing", "Lannisport", "Oldtown",
"Rainswood", "Storm's End", "Summerhall", "Sunspear", "Tarth", "Castle Black", "Craster's Keep",
"Fist of the First Men", "The Frostfangs", "The Gift", "The Skirling Pass", "The Wall", "Asshai",
"Astapor", "Braavos", "The Dothraki Sea", "Lys", "Meereen", "Myr", "Norvos", "Pentos", "Qarth",
"Qohor", "The Red Waste", "Tyrosh", "Vaes Dothrak", "Valyria", "Village of the Lhazareen", "Volantis",
"Yunkai"] # noqa
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
new_venue = models.Venue.objects.create(name=name)
if i % 2 == 0:
new_venue.three_phase_available = True
if i % 3 == 0:
new_venue.email = "address@venue.com"
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"
if i % 7 == 0:
new_venue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
new_venue.phone = "01234 567894"
new_venue.save()
self.venues.append(new_venue)
def setup_events(self):
names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball",
"Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event",
"End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show",
"Awards Ceremony", "Debating Event", "Club Night", "DJ Evening", "Building Projection",
"Choir Concert"]
descriptions = ["A brief description of the event", "This event is boring", "Probably wont happen",
"Warning: this has lots of kit"]
notes = ["The client came into the office at some point", "Who knows if this will happen",
"Probably should check this event", "Maybe not happening", "Run away!"]
item_options = [
{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2,
'cost': 200.00},
{'name': 'Projector',
'description': 'Some kind of video thinamejig, probably with unnecessary processing for free',
'quantity': 1, 'cost': 500.00},
{'name': 'Lighting Desk', 'description': 'Cannot provide guarentee that it will work', 'quantity': 1,
'cost': 200.52},
{'name': 'Moving lights', 'description': 'Flashy lights, with the copper', 'quantity': 8, 'cost': 50.00},
{'name': 'Microphones', 'description': 'Make loud noise \n you will want speakers with this', 'quantity': 5,
'cost': 0.50},
{'name': 'Sound Mixer Thing', 'description': 'Might be analogue, might be digital', 'quantity': 1,
'cost': 100.00},
{'name': 'Electricity', 'description': 'You need this', 'quantity': 1, 'cost': 200.00},
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
day_delta = -120 # start adding events from 4 months ago
for i in range(150): # Let's add 100 events
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
name = names[i % len(names)]
start_date = datetime.date.today() + datetime.timedelta(days=day_delta)
day_delta = day_delta + random.randint(0, 3)
new_event = models.Event.objects.create(name=name, start_date=start_date)
if random.randint(0, 2) > 1: # 1 in 3 have a start time
new_event.start_time = datetime.time(random.randint(15, 20))
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
new_event.end_time = datetime.time(random.randint(21, 23))
elif random.randint(0, 1) > 0: # half of the others finish early the next day
new_event.end_date = new_event.start_date + datetime.timedelta(days=1)
new_event.end_time = datetime.time(random.randint(0, 5))
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
new_event.end_date = new_event.start_date + datetime.timedelta(days=random.randint(1, 4))
if random.randint(0, 6) > 0: # 5 in 6 have MIC
new_event.mic = random.choice(self.profiles)
if random.randint(0, 6) > 0: # 5 in 6 have organisation
new_event.organisation = random.choice(self.organisations)
if random.randint(0, 6) > 0: # 5 in 6 have person
new_event.person = random.choice(self.people)
if random.randint(0, 6) > 0: # 5 in 6 have venue
new_event.venue = random.choice(self.venues)
# Could have any status, equally weighted
new_event.status = random.choice(
[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
if random.randint(0, 1) > 0: # 1 in 2 have description
new_event.description = random.choice(descriptions)
if random.randint(0, 1) > 0: # 1 in 2 have notes
new_event.notes = random.choice(notes)
new_event.save()
# Now add some items
for j in range(random.randint(1, 5)):
item_data = item_options[random.randint(0, len(item_options) - 1)]
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
new_item.save()
while new_event.sum_total < 0:
item_data = item_options[random.randint(0, len(item_options) - 1)]
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
new_item.save()
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
if new_event.start_date < datetime.date.today(): # think about adding an invoice
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
new_invoice = models.Invoice.objects.create(event=new_event)
if new_event.status is models.Event.CANCELLED: # void cancelled events
new_invoice.void = True
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
models.Payment.objects.create(invoice=new_invoice, amount=new_invoice.balance,
date=datetime.date.today())
if i == 1 or random.randint(0, 5) > 0: # Event 1 and 1 in 5 have a RA
models.RiskAssessment.objects.create(event=new_event, supervisor_consulted=bool(random.getrandbits(1)),
nonstandard_equipment=bool(random.getrandbits(1)),
nonstandard_use=bool(random.getrandbits(1)),
contractors=bool(random.getrandbits(1)),
other_companies=bool(random.getrandbits(1)),
crew_fatigue=bool(random.getrandbits(1)),
big_power=bool(random.getrandbits(1)),
generators=bool(random.getrandbits(1)),
other_companies_power=bool(random.getrandbits(1)),
nonstandard_equipment_power=bool(random.getrandbits(1)),
multiple_electrical_environments=bool(random.getrandbits(1)),
noise_monitoring=bool(random.getrandbits(1)),
known_venue=bool(random.getrandbits(1)),
safe_loading=bool(random.getrandbits(1)),
safe_storage=bool(random.getrandbits(1)),
area_outside_of_control=bool(random.getrandbits(1)),
barrier_required=bool(random.getrandbits(1)),
nonstandard_emergency_procedure=bool(random.getrandbits(1)),
special_structures=bool(random.getrandbits(1)),
suspended_structures=bool(random.getrandbits(1)),
outside=bool(random.getrandbits(1)))
if i == 0 or random.randint(0, 1) > 0: # Event 1 and 1 in 10 have a Checklist
models.EventChecklist.objects.create(event=new_event,
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",
date=timezone.now(), venue=random.choice(self.venues))

View File

@@ -1,38 +0,0 @@
import premailer
import datetime
from django.template.loader import get_template
from django.contrib.staticfiles import finders
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.core.mail import EmailMultiAlternatives
from django.utils import timezone
from django.urls import reverse
from RIGS import models
class Command(BaseCommand):
help = 'Sends email reminders as required. Triggered daily through heroku-scheduler in production.'
def handle(self, *args, **options):
events = models.Event.objects.current_events().select_related('riskassessment')
for event in events:
earliest_time = event.earliest_time if isinstance(event.earliest_time, datetime.datetime) else timezone.make_aware(datetime.datetime.combine(event.earliest_time, datetime.time(00, 00)))
# 48 hours = 172800 seconds
if event.is_rig and not event.cancelled and not event.dry_hire and (earliest_time - timezone.now()).total_seconds() <= 172800 and not hasattr(event, 'riskassessment'):
context = {
"event": event,
"url": "https://" + settings.DOMAIN + reverse('event_ra', kwargs={'pk': event.pk})
}
target = event.mic.email if event.mic else f"productions@{settings.DOMAIN}"
msg = EmailMultiAlternatives(
f"{event} - Risk Assessment Incomplete",
get_template("email/ra_reminder.txt").render(context),
to=[target],
reply_to=[f"h.s.manager@{settings.DOMAIN}"],
)
css = finders.find('css/email.css')
html = premailer.Premailer(get_template("email/ra_reminder.html").render(context), external_styles=css).transform()
msg.attach_alternative(html, 'text/html')
msg.send()

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.core.validators

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('postedAt', models.DateTimeField(auto_now=True)),
('message', models.TextField()),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
options={
},

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import RIGS.models
import versioning
class Migration(migrations.Migration):
@@ -26,6 +25,6 @@ class Migration(migrations.Migration):
],
options={
},
bases=(models.Model, versioning.versioning.RevisionMixin),
bases=(models.Model, RIGS.models.RevisionMixin),
),
]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import RIGS.models
import versioning
class Migration(migrations.Migration):
@@ -22,6 +21,6 @@ class Migration(migrations.Migration):
],
options={
},
bases=(models.Model, versioning.versioning.RevisionMixin),
bases=(models.Model, RIGS.models.RevisionMixin),
),
]

View File

@@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
import RIGS.models
import versioning
class Migration(migrations.Migration):
@@ -34,15 +33,15 @@ class Migration(migrations.Migration):
('payment_method', models.CharField(blank=True, null=True, max_length=255)),
('payment_received', models.CharField(blank=True, null=True, max_length=255)),
('purchase_order', models.CharField(blank=True, null=True, max_length=255)),
('based_on', models.ForeignKey(to='RIGS.Event', related_name='future_events', on_delete=models.CASCADE)),
('checked_in_by', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in', on_delete=models.CASCADE)),
('mic', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', on_delete=models.CASCADE)),
('organisation', models.ForeignKey(to='RIGS.Organisation', on_delete=models.CASCADE)),
('person', models.ForeignKey(to='RIGS.Person', on_delete=models.CASCADE)),
('based_on', models.ForeignKey(to='RIGS.Event', related_name='future_events')),
('checked_in_by', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in')),
('mic', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic')),
('organisation', models.ForeignKey(to='RIGS.Organisation')),
('person', models.ForeignKey(to='RIGS.Person')),
],
options={
},
bases=(models.Model, versioning.versioning.RevisionMixin),
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.CreateModel(
name='EventItem',
@@ -53,7 +52,7 @@ class Migration(migrations.Migration):
('quantity', models.IntegerField()),
('cost', models.DecimalField(max_digits=10, decimal_places=2)),
('order', models.IntegerField()),
('event', models.ForeignKey(to='RIGS.Event', related_name='item', on_delete=models.CASCADE)),
('event', models.ForeignKey(to='RIGS.Event', related_name='item')),
],
options={
},
@@ -71,12 +70,12 @@ class Migration(migrations.Migration):
],
options={
},
bases=(models.Model, versioning.versioning.RevisionMixin),
bases=(models.Model, RIGS.models.RevisionMixin),
),
migrations.AddField(
model_name='event',
name='venue',
field=models.ForeignKey(to='RIGS.Venue', on_delete=models.CASCADE),
field=models.ForeignKey(to='RIGS.Venue'),
preserve_default=True,
),
]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
@@ -14,26 +14,26 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='based_on',
field=models.ForeignKey(to='RIGS.Event', related_name='future_events', blank=True, null=True, on_delete=models.CASCADE),
field=models.ForeignKey(to='RIGS.Event', related_name='future_events', blank=True, null=True),
preserve_default=True,
),
migrations.AlterField(
model_name='event',
name='checked_in_by',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True,
null=True, on_delete=models.CASCADE),
null=True),
preserve_default=True,
),
migrations.AlterField(
model_name='event',
name='mic',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True, on_delete=models.CASCADE),
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True),
preserve_default=True,
),
migrations.AlterField(
model_name='event',
name='organisation',
field=models.ForeignKey(to='RIGS.Organisation', blank=True, null=True, on_delete=models.CASCADE),
field=models.ForeignKey(to='RIGS.Organisation', blank=True, null=True),
preserve_default=True,
),
migrations.AlterField(

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
@@ -19,8 +19,8 @@ class Migration(migrations.Migration):
('run', models.BooleanField(default=False)),
('derig', models.BooleanField(default=False)),
('notes', models.TextField(blank=True, null=True)),
('event', models.ForeignKey(related_name='crew', to='RIGS.Event', on_delete=models.CASCADE)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
('event', models.ForeignKey(related_name='crew', to='RIGS.Event')),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
options={
},
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='eventitem',
name='event',
field=models.ForeignKey(related_name='items', to='RIGS.Event', on_delete=models.CASCADE),
field=models.ForeignKey(related_name='items', to='RIGS.Event'),
preserve_default=True,
),
]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
@@ -14,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='person',
field=models.ForeignKey(blank=True, null=True, to='RIGS.Person', on_delete=models.CASCADE),
field=models.ForeignKey(blank=True, null=True, to='RIGS.Person'),
preserve_default=True,
),
]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
@@ -14,13 +14,13 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='venue',
field=models.ForeignKey(blank=True, to='RIGS.Venue', null=True, on_delete=models.CASCADE),
field=models.ForeignKey(blank=True, to='RIGS.Venue', null=True),
preserve_default=True,
),
migrations.AlterField(
model_name='eventitem',
name='event',
field=models.ForeignKey(related_name='items', blank=True, to='RIGS.Event', on_delete=models.CASCADE),
field=models.ForeignKey(related_name='items', blank=True, to='RIGS.Event'),
preserve_default=True,
),
]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('invoice_date', models.DateField(auto_now_add=True)),
('void', models.BooleanField()),
('event', models.OneToOneField(to='RIGS.Event', on_delete=models.CASCADE)),
('event', models.OneToOneField(to='RIGS.Event')),
],
options={
},
@@ -31,7 +31,7 @@ class Migration(migrations.Migration):
('date', models.DateField()),
('amount', models.DecimalField(help_text=b'Please use ex. VAT', max_digits=10, decimal_places=2)),
('method', models.CharField(max_length=2, choices=[(b'C', b'Cash'), (b'I', b'Internal'), (b'E', b'External'), (b'SU', b'SU Core'), (b'M', b'Members')])),
('invoice', models.ForeignKey(to='RIGS.Invoice', on_delete=models.CASCADE)),
('invoice', models.ForeignKey(to='RIGS.Invoice')),
],
options={
},
@@ -40,7 +40,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='mic',
field=models.ForeignKey(related_name='event_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE),
field=models.ForeignKey(related_name='event_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True),
preserve_default=True,
),
]

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.core.validators

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.db.models.deletion

View File

@@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2016-03-31 12:02
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0024_auto_20160229_2042'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='username',
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username'),
),
migrations.AlterField(
model_name='vatrate',
name='start_at',
field=models.DateField(),
),
]

View File

@@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0024_auto_20160229_2042'),
]
operations = [
migrations.CreateModel(
name='EventAuthorisation',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('email', models.EmailField(max_length=254)),
('name', models.CharField(max_length=255)),
('uni_id', models.CharField(max_length=10, null=True, verbose_name=b'University ID', blank=True)),
('account_code', models.CharField(max_length=50, null=True, blank=True)),
('amount', models.DecimalField(verbose_name=b'authorisation amount', max_digits=10, decimal_places=2)),
('created_at', models.DateTimeField(auto_now_add=True)),
('event', models.ForeignKey(related_name='authroisations', to='RIGS.Event', on_delete=models.CASCADE)),
],
),
]

View File

@@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-10 17:46
import django.contrib.auth.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0025_auto_20160331_1302'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='username',
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator()], verbose_name='username'),
),
]

View File

@@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0025_eventauthorisation'),
]
operations = [
migrations.RemoveField(
model_name='eventauthorisation',
name='created_at',
),
]

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0026_remove_eventauthorisation_created_at'),
]
operations = [
migrations.AlterField(
model_name='eventauthorisation',
name='event',
field=models.OneToOneField(related_name='authorisation', to='RIGS.Event', on_delete=models.CASCADE),
),
]

View File

@@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0027_eventauthorisation_event_singular'),
]
operations = [
migrations.AddField(
model_name='eventauthorisation',
name='sent_by',
field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
preserve_default=False,
),
]

View File

@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0029_eventauthorisation_sent_by'),
]
operations = [
migrations.AddField(
model_name='event',
name='auth_request_at',
field=models.DateTimeField(null=True, blank=True),
),
migrations.AddField(
model_name='event',
name='auth_request_by',
field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE),
),
migrations.AddField(
model_name='event',
name='auth_request_to',
field=models.EmailField(max_length=254, null=True, blank=True),
),
]

View File

@@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-12 20:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0030_auth_request_sending'),
('RIGS', '0026_auto_20170510_1846'),
]
operations = [
]

View File

@@ -1,69 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-04 22:55
from __future__ import unicode_literals
from django.conf import settings
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('reversion', '0001_squashed_0004_auto_20160611_1202'),
('RIGS', '0031_merge_20170512_2102'),
]
operations = [
migrations.CreateModel(
name='RIGSVersion',
fields=[
],
options={
'indexes': [],
'proxy': True,
},
bases=('reversion.version',),
),
migrations.AlterField(
model_name='event',
name='collector',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='collected by'),
),
migrations.AlterField(
model_name='event',
name='mic',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='event_mic', to=settings.AUTH_USER_MODEL, verbose_name='MIC'),
),
migrations.AlterField(
model_name='event',
name='purchase_order',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='PO'),
),
migrations.AlterField(
model_name='eventauthorisation',
name='amount',
field=models.DecimalField(decimal_places=2, max_digits=10, verbose_name='authorisation amount'),
),
migrations.AlterField(
model_name='eventauthorisation',
name='uni_id',
field=models.CharField(blank=True, max_length=10, null=True, verbose_name='University ID'),
),
migrations.AlterField(
model_name='payment',
name='amount',
field=models.DecimalField(decimal_places=2, help_text='Please use ex. VAT', max_digits=10),
),
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')], max_length=2, null=True),
),
migrations.AlterField(
model_name='profile',
name='username',
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.0.3 on 2018-03-25 00:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0032_auto_20170904_2355'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='last_name',
field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.0.5 on 2019-07-28 21:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0033_auto_20180325_0016'),
]
operations = [
migrations.AddField(
model_name='event',
name='risk_assessment_edit_url',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.0.13 on 2019-11-24 13:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0034_event_risk_assessment_edit_url'),
]
operations = [
migrations.AlterField(
model_name='event',
name='risk_assessment_edit_url',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='risk assessment'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 2.0.13 on 2020-01-10 14:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0035_auto_20191124_1319'),
]
operations = [
migrations.AddField(
model_name='profile',
name='is_approved',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='profile',
name='last_emailed',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 2.0.13 on 2020-01-11 18:29
# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
from django.db import migrations
def approve_legacy(apps, schema_editor):
Profile = apps.get_model('RIGS', 'Profile')
for person in Profile.objects.all():
person.is_approved = True
person.save()
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0036_profile_is_approved'),
]
operations = [
migrations.RunPython(approve_legacy, migrations.RunPython.noop)
]

View File

@@ -1,37 +0,0 @@
# Generated by Django 2.0.13 on 2020-03-06 20:00
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0037_approve_legacy'),
]
operations = [
migrations.AlterModelOptions(
name='event',
options={},
),
migrations.AlterModelOptions(
name='invoice',
options={'ordering': ['-invoice_date']},
),
migrations.AlterModelOptions(
name='organisation',
options={},
),
migrations.AlterModelOptions(
name='person',
options={},
),
migrations.AlterModelOptions(
name='profile',
options={'verbose_name': 'user', 'verbose_name_plural': 'users'},
),
migrations.AlterModelOptions(
name='venue',
options={},
),
]

View File

@@ -1,191 +0,0 @@
# Generated by Django 3.1.2 on 2021-01-23 19:10
import RIGS.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import versioning
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0038_auto_20200306_2000'),
]
operations = [
migrations.CreateModel(
name='EventChecklist',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField()),
('safe_parking', models.BooleanField(blank=True, help_text='Vehicles parked safely?<br><small>(does not obstruct venue access)</small>', null=True)),
('safe_packing', models.BooleanField(blank=True, help_text='Equipment packed away safely?<br><small>(including flightcases)</small>', null=True)),
('exits', models.BooleanField(blank=True, help_text='Emergency exits clear?', null=True)),
('trip_hazard', models.BooleanField(blank=True, help_text='Appropriate barriers around kit and cabling secured?', null=True)),
('warning_signs', models.BooleanField(blank=True, help_text='Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>')),
('ear_plugs', models.BooleanField(blank=True, help_text='Ear plugs issued to crew where needed?', null=True)),
('hs_location', models.CharField(blank=True, help_text='Location of Safety Bag/Box', max_length=255, null=True)),
('extinguishers_location', models.CharField(blank=True, help_text='Location of fire extinguishers', max_length=255, null=True)),
('rcds', models.BooleanField(blank=True, help_text='RCDs installed where needed and tested?', null=True)),
('supply_test', models.BooleanField(blank=True, help_text='Electrical supplies tested?<br><small>(using socket tester)</small>', null=True)),
('earthing', models.BooleanField(blank=True, help_text='Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>', null=True)),
('pat', models.BooleanField(blank=True, help_text='All equipment in PAT period?', null=True)),
('source_rcd', models.BooleanField(blank=True, help_text='Source RCD protected?<br><small>(if cable is more than 3m long) </small>', null=True)),
('labelling', models.BooleanField(blank=True, help_text='Appropriate and clear labelling on distribution and cabling?', null=True)),
('fd_voltage_l1', models.IntegerField(blank=True, help_text='L1 - N', null=True, verbose_name='First Distro Voltage L1-N')),
('fd_voltage_l2', models.IntegerField(blank=True, help_text='L2 - N', null=True, verbose_name='First Distro Voltage L2-N')),
('fd_voltage_l3', models.IntegerField(blank=True, help_text='L3 - N', null=True, verbose_name='First Distro Voltage L3-N')),
('fd_phase_rotation', models.BooleanField(blank=True, help_text='Phase Rotation<br><small>(if required)</small>', null=True, verbose_name='Phase Rotation')),
('fd_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True, verbose_name='Earth Fault Loop Impedance')),
('fd_pssc', models.IntegerField(blank=True, help_text='Prospective Short Circuit Current', null=True, verbose_name='PSCC')),
('w1_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
('w1_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
('w1_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
('w1_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
('w2_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
('w2_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
('w2_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
('w2_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
('w3_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
('w3_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
('w3_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
('w3_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
('all_rcds_tested', models.BooleanField(blank=True, help_text='All circuit RCDs tested?<br><small>(using test button)</small>', null=True)),
('public_sockets_tested', models.BooleanField(blank=True, help_text='Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>', null=True)),
('reviewed_at', models.DateTimeField(null=True)),
],
options={
'ordering': ['event'],
'permissions': [('review_eventchecklist', 'Can review Event Checklists')],
},
bases=(models.Model, versioning.versioning.RevisionMixin),
),
migrations.CreateModel(
name='EventChecklistCrew',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(max_length=255)),
('start', models.DateTimeField()),
('end', models.DateTimeField()),
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.eventchecklist')),
],
bases=(models.Model, versioning.versioning.RevisionMixin),
),
migrations.CreateModel(
name='EventChecklistVehicle',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('vehicle', models.CharField(max_length=255)),
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to='RIGS.eventchecklist')),
],
bases=(models.Model, versioning.versioning.RevisionMixin),
),
migrations.CreateModel(
name='RiskAssessment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nonstandard_equipment', models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by <a href='https://nottinghamtec.sharepoint.com/:f:/g/HealthAndSafety/Eo4xED_DrqFFsfYIjKzMZIIB6Gm_ZfR-a8l84RnzxtBjrA?e=Bf0Haw'>TEC's standard risk assessments and method statements?</a>")),
('nonstandard_use', models.BooleanField(help_text='Are TEC using their equipment in a way that is abnormal?<br><small>i.e. Not covered by TECs standard health and safety documentation</small>')),
('contractors', models.BooleanField(help_text='Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>')),
('other_companies', models.BooleanField(help_text='Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>')),
('crew_fatigue', models.BooleanField(help_text='Is crew fatigue likely to be a risk at any point during this event?')),
('general_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
('big_power', models.BooleanField(help_text='Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?')),
('outside', models.BooleanField(help_text='Is the event outdoors?')),
('generators', models.BooleanField(help_text='Will generators be used?')),
('other_companies_power', models.BooleanField(help_text='Will TEC be supplying power to any other companies?')),
('nonstandard_equipment_power', models.BooleanField(help_text='Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?')),
('multiple_electrical_environments', models.BooleanField(help_text='Will the electrical installation occupy more than one electrical environment?')),
('power_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
('power_plan', models.URLField(blank=True, help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", null=True, validators=[RIGS.models.validate_url])),
('noise_monitoring', models.BooleanField(help_text='Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?')),
('sound_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
('known_venue', models.BooleanField(help_text='Is this venue new to you (the MIC) or new to TEC?')),
('safe_loading', models.BooleanField(help_text='Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)')),
('safe_storage', models.BooleanField(help_text='Are there any problems with safe and secure equipment storage?')),
('area_outside_of_control', models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")),
('barrier_required', models.BooleanField(help_text='Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?')),
('nonstandard_emergency_procedure', models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")),
('special_structures', models.BooleanField(help_text='Does the event require use of winch stands, motors, MPT Towers, or staging?')),
('suspended_structures', models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")),
('persons_responsible_structures', models.TextField(blank=True, help_text='Who are the persons on site responsible for their use?', null=True)),
('rigging_plan', models.URLField(blank=True, help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", null=True, validators=[RIGS.models.validate_url])),
('reviewed_at', models.DateTimeField(null=True)),
('supervisor_consulted', models.BooleanField(null=True)),
],
options={
'ordering': ['event'],
'permissions': [('review_riskassessment', 'Can review Risk Assessments')],
},
bases=(models.Model, versioning.versioning.RevisionMixin),
),
migrations.RemoveField(
model_name='eventcrew',
name='event',
),
migrations.RemoveField(
model_name='eventcrew',
name='user',
),
migrations.DeleteModel(
name='RIGSVersion',
),
migrations.RemoveField(
model_name='event',
name='risk_assessment_edit_url',
),
migrations.AlterField(
model_name='profile',
name='first_name',
field=models.CharField(blank=True, max_length=150, verbose_name='first name'),
),
migrations.DeleteModel(
name='EventCrew',
),
migrations.AddField(
model_name='riskassessment',
name='event',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='RIGS.event'),
),
migrations.AddField(
model_name='riskassessment',
name='power_mic',
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='power_mic', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
),
migrations.AddField(
model_name='riskassessment',
name='reviewed_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer'),
),
migrations.AddField(
model_name='eventchecklistvehicle',
name='driver',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='eventchecklistcrew',
name='crewmember',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crewed', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='eventchecklist',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to='RIGS.event'),
),
migrations.AddField(
model_name='eventchecklist',
name='power_mic',
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC?', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
),
migrations.AddField(
model_name='eventchecklist',
name='reviewed_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer'),
),
migrations.AddField(
model_name='eventchecklist',
name='venue',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.venue'),
),
]

View File

@@ -1,67 +0,0 @@
# Generated by Django 3.1.7 on 2021-03-02 11:48
from django.db import migrations
def postgres_migration_prep(apps, schema_editor):
model = apps.get_model("RIGS", "Event")
for field in ["auth_request_to", "collector", "description", "notes", "purchase_order"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "EventAuthorisation")
for field in ["account_code", "uni_id"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "EventChecklist")
for field in ["extinguishers_location", "hs_location", "w1_description", "w2_description", "w3_description"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "EventItem")
for field in ["description"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Organisation")
for field in ["address", "email", "notes", "phone"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Payment")
for field in ["method"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Person")
for field in ["address", "email", "notes", "phone"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Profile")
for field in ["phone"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "RiskAssessment")
for field in ["general_notes", "persons_responsible_structures", "power_notes", "rigging_plan", "sound_notes"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
model = apps.get_model("RIGS", "Venue")
for field in ["address", "email", "notes", "phone"]:
filter_param = {"{}__isnull".format(field): True}
update_param = {field: ""}
model.objects.filter(**filter_param).update(**update_param)
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0039_auto_20210123_1910'),
]
operations = [
migrations.RunPython(postgres_migration_prep, migrations.RunPython.noop)
]

View File

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

View File

@@ -1,34 +0,0 @@
# Generated by Django 3.1.13 on 2021-10-07 22:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0041_auto_20210302_1204'),
]
operations = [
migrations.AlterField(
model_name='eventchecklist',
name='fd_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='eventchecklist',
name='w1_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='eventchecklist',
name='w2_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='eventchecklist',
name='w3_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.1.13 on 2021-10-27 14:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0042_auto_20211007_2338'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='initials',
field=models.CharField(max_length=5, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.11 on 2022-01-09 14:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0043_auto_20211027_1519'),
]
operations = [
migrations.AddField(
model_name='profile',
name='is_supervisor',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2022-10-20 23:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0044_profile_is_supervisor'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='is_approved',
field=models.BooleanField(default=False, help_text='Designates whether a staff member has approved this user.', verbose_name='Approval Status'),
),
]

View File

@@ -1,71 +0,0 @@
# Generated by Django 3.2.16 on 2023-05-08 15:58
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import versioning.versioning
def migrate_old_data(apps, schema_editor):
EventChecklist = apps.get_model('RIGS', 'EventChecklist')
PowerTestRecord = apps.get_model('RIGS', 'PowerTestRecord')
for ec in EventChecklist.objects.all():
# New highscore for the most pythonic BS I've ever written.
PowerTestRecord.objects.create(event=ec.event, venue=ec.venue, reviewed_by=ec.reviewed_by, **{i.name:getattr(ec, i.attname) for i in PowerTestRecord._meta.get_fields() if not (i.is_relation or i.auto_created or i.name == "notes")})
def revert(apps, schema_editor):
apps.get_model('RIGS', 'PowerTestRecord').objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0045_alter_profile_is_approved'),
]
operations = [
migrations.CreateModel(
name='PowerTestRecord',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_tests', to='RIGS.event')),
('notes', models.TextField(blank=True, default='')),
('venue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.venue')),
('reviewed_at', models.DateTimeField(null=True)),
('rcds', models.BooleanField(blank=True, help_text='RCDs installed where needed and tested?', null=True)),
('supply_test', models.BooleanField(blank=True, help_text='Electrical supplies tested?<br><small>(using socket tester)</small>', null=True)),
('earthing', models.BooleanField(blank=True, help_text='Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>', null=True)),
('pat', models.BooleanField(blank=True, help_text='All equipment in PAT period?', null=True)),
('source_rcd', models.BooleanField(blank=True, help_text='Source RCD protected?<br><small>(if cable is more than 3m long) </small>', null=True)),
('labelling', models.BooleanField(blank=True, help_text='Appropriate and clear labelling on distribution and cabling?', null=True)),
('fd_voltage_l1', models.IntegerField(blank=True, help_text='L1 - N', null=True, verbose_name='First Distro Voltage L1-N')),
('fd_voltage_l2', models.IntegerField(blank=True, help_text='L2 - N', null=True, verbose_name='First Distro Voltage L2-N')),
('fd_voltage_l3', models.IntegerField(blank=True, help_text='L3 - N', null=True, verbose_name='First Distro Voltage L3-N')),
('fd_phase_rotation', models.BooleanField(blank=True, help_text='Phase Rotation<br><small>(if required)</small>', null=True, verbose_name='Phase Rotation')),
('fd_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
('fd_pssc', models.IntegerField(blank=True, help_text='Prospective Short Circuit Current', null=True, verbose_name='PSCC')),
('w1_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
('w1_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
('w1_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
('w1_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
('w2_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
('w2_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
('w2_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
('w2_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
('w3_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
('w3_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
('w3_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
('w3_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
('all_rcds_tested', models.BooleanField(blank=True, help_text='All circuit RCDs tested?<br><small>(using test button)</small>', null=True)),
('public_sockets_tested', models.BooleanField(blank=True, help_text='Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>', null=True)),
('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer')),
],
options={
'abstract': False,
'ordering': ['event'],
'permissions': [('review_power', 'Can review Power Test Records')],
},
bases=(models.Model, versioning.versioning.RevisionMixin),
),
migrations.RunPython(migrate_old_data, reverse_code=revert),
]

View File

@@ -1,44 +0,0 @@
# Generated by Django 3.2.19 on 2023-05-17 08:44
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from django.core.exceptions import ObjectDoesNotExist
def migrate_old_data(apps, schema_editor):
EventChecklist = apps.get_model('RIGS', 'EventChecklist')
EventCheckIn = apps.get_model('RIGS', 'EventCheckIn')
for ec in EventChecklist.objects.all():
for crew in ec.crew.all():
try:
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end, vehicle=ec.vehicles.get(driver=crew.crewmember).vehicle)
except ObjectDoesNotExist:
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end)
def revert(apps, schema_editor):
apps.get_model('RIGS', 'EventCheckIn').objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0046_create_powertests'),
]
operations = [
migrations.CreateModel(
name='EventCheckIn',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField()),
('role', models.CharField(blank=True, max_length=50)),
('vehicle', models.CharField(blank=True, max_length=100)),
('end_time', models.DateTimeField(blank=True, null=True)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.event')),
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkins', to=settings.AUTH_USER_MODEL)),
],
),
migrations.RunPython(migrate_old_data, reverse_code=revert),
]

View File

@@ -1,156 +0,0 @@
# Generated by Django 3.2.19 on 2023-05-18 11:56
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0047_auto_20230517_0944'),
]
operations = [
migrations.RemoveField(
model_name='eventchecklistvehicle',
name='checklist',
),
migrations.RemoveField(
model_name='eventchecklistvehicle',
name='driver',
),
migrations.RemoveField(
model_name='eventchecklist',
name='all_rcds_tested',
),
migrations.RemoveField(
model_name='eventchecklist',
name='earthing',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_phase_rotation',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_pssc',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_voltage_l1',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_voltage_l2',
),
migrations.RemoveField(
model_name='eventchecklist',
name='fd_voltage_l3',
),
migrations.RemoveField(
model_name='eventchecklist',
name='labelling',
),
migrations.RemoveField(
model_name='eventchecklist',
name='pat',
),
migrations.RemoveField(
model_name='eventchecklist',
name='power_mic',
),
migrations.RemoveField(
model_name='eventchecklist',
name='public_sockets_tested',
),
migrations.RemoveField(
model_name='eventchecklist',
name='rcds',
),
migrations.RemoveField(
model_name='eventchecklist',
name='source_rcd',
),
migrations.RemoveField(
model_name='eventchecklist',
name='supply_test',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_description',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_polarity',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w1_voltage',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_description',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_polarity',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w2_voltage',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_description',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_earth_fault',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_polarity',
),
migrations.RemoveField(
model_name='eventchecklist',
name='w3_voltage',
),
migrations.AddField(
model_name='powertestrecord',
name='power_mic',
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC?', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
),
migrations.AlterField(
model_name='eventchecklist',
name='reviewed_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='powertestrecord',
name='reviewed_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='riskassessment',
name='reviewed_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.DeleteModel(
name='EventChecklistCrew',
),
migrations.DeleteModel(
name='EventChecklistVehicle',
),
]

View File

@@ -1,53 +0,0 @@
# Generated by Django 3.2.19 on 2023-05-29 10:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0048_auto_20230518_1256'),
]
operations = [
migrations.AlterField(
model_name='powertestrecord',
name='fd_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
name='fd_pssc',
field=models.IntegerField(blank=True, help_text='Prospective Short Circuit Current / A', null=True, verbose_name='PSCC'),
),
migrations.AlterField(
model_name='powertestrecord',
name='w1_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
name='w1_voltage',
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
),
migrations.AlterField(
model_name='powertestrecord',
name='w2_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
name='w2_voltage',
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
),
migrations.AlterField(
model_name='powertestrecord',
name='w3_earth_fault',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
),
migrations.AlterField(
model_name='powertestrecord',
name='w3_voltage',
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 3.2.19 on 2023-06-27 11:28
import RIGS.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0049_auto_20230529_1123'),
]
operations = [
migrations.AddField(
model_name='event',
name='forum_url',
field=models.URLField(blank=True, default='', validators=[RIGS.models.validate_forum_url]),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.19 on 2023-07-09 21:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0050_event_forum_url'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='method',
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('T', 'TEC Adjustment')], default='', max_length=2),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.21 on 2023-09-05 22:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0051_alter_payment_method'),
]
operations = [
migrations.AddField(
model_name='venue',
name='on_campus',
field=models.BooleanField(default=False, verbose_name='Is this venue on a UoN campus?'),
),
]

File diff suppressed because it is too large Load Diff

13
RIGS/regbackend.py Normal file
View File

@@ -0,0 +1,13 @@
from RIGS.models import Profile
from RIGS.forms import ProfileRegistrationFormUniqueEmail
def user_created(sender, user, request, **kwargs):
form = ProfileRegistrationFormUniqueEmail(request.POST)
user.first_name = form.data['first_name']
user.last_name = form.data['last_name']
user.initials = form.data['initials']
user.phone = form.data['phone']
user.save()
from registration.signals import user_registered
user_registered.connect(user_created)

195
RIGS/rigboard.py Normal file
View File

@@ -0,0 +1,195 @@
import os
import cStringIO as StringIO
from io import BytesIO
import urllib2
from django.views import generic
from django.core.urlresolvers import reverse_lazy
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.template.loader import get_template
from django.conf import settings
from django.http import HttpResponse
from django.db.models import Q
from django.contrib import messages
from z3c.rml import rml2pdf
from PyPDF2 import PdfFileMerger, PdfFileReader
from RIGS import models, forms
import datetime
import re
import copy
__author__ = 'ghost'
class RigboardIndex(generic.TemplateView):
template_name = 'RIGS/rigboard.html'
def get_context_data(self, **kwargs):
# get super context
context = super(RigboardIndex, self).get_context_data(**kwargs)
# call out method to get current events
context['events'] = models.Event.objects.current_events()
return context
class WebCalendar(generic.TemplateView):
template_name = 'RIGS/calendar.html'
def get_context_data(self, **kwargs):
context = super(WebCalendar, self).get_context_data(**kwargs)
context['view'] = kwargs.get('view','')
context['date'] = kwargs.get('date','')
return context
class EventDetail(generic.DetailView):
model = models.Event
class EventCreate(generic.CreateView):
model = models.Event
form_class = forms.EventForm
def get_context_data(self, **kwargs):
context = super(EventCreate, self).get_context_data(**kwargs)
context['edit'] = True
context['currentVAT'] = models.VatRate.objects.current_rate()
form = context['form']
if re.search('"-\d+"', form['items_json'].value()):
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
for field, model in form.related_models.iteritems():
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
return context
def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
class EventUpdate(generic.UpdateView):
model = models.Event
form_class = forms.EventForm
def get_context_data(self, **kwargs):
context = super(EventUpdate, self).get_context_data(**kwargs)
context['edit'] = True
form = context['form']
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
for field, model in form.related_models.iteritems():
value = form[field].value()
if value is not None and value != '':
context[field] = model.objects.get(pk=value)
return context
def get_success_url(self):
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
class EventDuplicate(EventUpdate):
def get_object(self, queryset=None):
old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
new = copy.copy(old) # Make a copy of the object in memory
new.based_on = old # Make the new event based on the old event
if self.request.method in ('POST', 'PUT'): # This only happens on save (otherwise items won't display in editor)
new.pk = None # This means a new event will be created on save, and all items will be re-created
messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.')
return new
def get_context_data(self, **kwargs):
context = super(EventDuplicate, self).get_context_data(**kwargs)
context["duplicate"] = True
return context
class EventPrint(generic.View):
def get(self, request, pk):
object = get_object_or_404(models.Event, pk=pk)
template = get_template('RIGS/event_print.xml')
copies = ('TEC', 'Client')
merger = PdfFileMerger()
for copy in copies:
context = RequestContext(request, { # this should be outside the loop, but bug in 1.8.2 prevents this
'object': object,
'fonts': {
'opensans': {
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'copy':copy,
'current_user':request.user,
})
# context['copy'] = copy # this is the way to do it once we upgrade to Django 1.8.3
rml = template.render(context)
buffer = StringIO.StringIO()
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
terms = urllib2.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(StringIO.StringIO(terms.read()))
merged = BytesIO()
merger.write(merged)
response = HttpResponse(content_type='application/pdf')
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
response['Content-Disposition'] = "filename=N%05d | %s.pdf" % (object.pk, escapedEventName)
response.write(merged.getvalue())
return response
class EventArchive(generic.ArchiveIndexView):
model = models.Event
date_field = "start_date"
paginate_by = 25
def get_queryset(self):
start = self.request.GET.get('start', None)
end = self.request.GET.get('end', datetime.date.today())
# Assume idiots, always check
if start and start > end:
messages.add_message(self.request, messages.INFO,
"Muppet! Check the dates, it has been fixed for you.")
start, end = end, start # Stop the impending fail
filter = False
if end != "":
filter = Q(start_date__lte=end)
if start:
if filter:
filter = filter & Q(start_date__gte=start)
else:
filter = Q(start_date__gte=start)
if filter:
qs = self.model.objects.filter(filter).order_by('-start_date')
else:
qs = self.model.objects.all().order_by('-start_date')
# Preselect related for efficiency
qs.select_related('person', 'organisation', 'venue', 'mic')
if len(qs) == 0:
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
return qs

View File

@@ -1,143 +0,0 @@
import re
import urllib.error
import urllib.parse
import urllib.request
from io import BytesIO
from PyPDF2 import PdfFileReader, PdfFileMerger
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.cache import cache
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.db.models.signals import post_save
from django.template.loader import get_template
from django.urls import reverse
from django.utils import timezone
from premailer import Premailer
from registration.signals import user_activated
from reversion import revisions as reversion
from z3c.rml import rml2pdf
from RIGS import models
def send_eventauthorisation_success_email(instance):
# Generate PDF first to prevent context conflicts
context = {
'object': instance.event,
'receipt': True,
'current_user': False,
}
template = get_template('event_print.xml')
merger = PdfFileMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(BytesIO(terms.read()))
merged = BytesIO()
merger.write(merged)
# Produce email content
context = {
'object': instance,
}
if instance.event.person is not None and instance.email == instance.event.person.email:
context['to_name'] = instance.event.person.name
elif instance.event.organisation is not None and instance.email == instance.event.organisation.email:
context['to_name'] = instance.event.organisation.name
subject = f"{instance.event.display_id} | {instance.event.name} - Event Authorised"
client_email = EmailMultiAlternatives(
subject,
get_template("email/eventauthorisation_client_success.txt").render(context),
to=[instance.email],
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
)
css = finders.find('css/email.css')
html = Premailer(get_template("email/eventauthorisation_client_success.html").render(context),
external_styles=css).transform()
client_email.attach_alternative(html, 'text/html')
escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name)
client_email.attach(f'{instance.event.display_id} - {escapedEventName} - CONFIRMATION.pdf',
merged.getvalue(),
'application/pdf'
)
if instance.event.mic:
mic_email_address = instance.event.mic.email
else:
mic_email_address = settings.AUTHORISATION_NOTIFICATION_ADDRESS
mic_email = EmailMessage(
subject,
get_template("email/eventauthorisation_mic_success.txt").render(context),
to=[mic_email_address]
)
# Now we have both emails successfully generated, send them out
client_email.send(fail_silently=True)
mic_email.send(fail_silently=True)
# Set event to booked now that it's authorised
instance.event.status = models.Event.BOOKED
instance.event.save()
def on_revision_commit(sender, instance, created, **kwargs):
if created:
send_eventauthorisation_success_email(instance)
post_save.connect(on_revision_commit, sender=models.EventAuthorisation)
def send_admin_awaiting_approval_email(user, request, **kwargs):
# Bit more controlled than just emailing all superusers
for admin in models.Profile.admins():
# Check we've ever emailed them before and if so, if cooldown has passed.
if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now():
context = {
'request': request,
'link_suffix': reverse("admin:RIGS_profile_changelist") + '?is_approved__exact=0',
'number_of_users': models.Profile.users_awaiting_approval_count(),
'to_name': admin.first_name
}
email = EmailMultiAlternatives(
f"{context['number_of_users']} new users awaiting approval on RIGS",
get_template("email/admin_awaiting_approval.txt").render(context),
to=[admin.email],
reply_to=[user.email],
)
css = finders.find('css/email.css')
html = Premailer(get_template("email/admin_awaiting_approval.html").render(context),
external_styles=css).transform()
email.attach_alternative(html, 'text/html')
email.send()
# Update last sent
admin.last_emailed = timezone.now()
admin.save()
user_activated.connect(send_admin_awaiting_approval_email)
def update_cache(sender, instance, created, **kwargs):
cache.clear()
for model in reversion.get_registered_models():
post_save.connect(update_cache, sender=model)

27
RIGS/static/config.rb Normal file
View File

@@ -0,0 +1,27 @@
# Require any additional compass plugins here.
require 'bootstrap-sass'
# Set this to the root of your project when deployed:
http_path = "/static/"
css_dir = "css"
sass_dir = "scss"
images_dir = "img"
javascripts_dir = "js"
fonts_dir = "fonts"
# You can select your preferred output style here (can be overridden via the command line):
# output_style = :expanded or :nested or :compact or :compressed
output_style = :compressed
# To enable relative paths to assets via compass helper functions. Uncomment:
# relative_assets = true
# To disable debugging comments that display the original location of your selectors. Uncomment:
# line_comments = false
# If you prefer the indented syntax, you might want to regenerate this
# project again passing --syntax sass, or you can uncomment this:
# preferred_syntax = :sass
# and then run:
# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass

View File

@@ -0,0 +1,27 @@
/*!
* Ajax Bootstrap Select
*
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
*
* @version 1.3.1
* @author Adam Heim - https://github.com/truckingsim
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
* @copyright 2015 Adam Heim
* @license Released under the MIT license.
*
* Contributors:
* Mark Carver - https://github.com/markcarver
*
* Last build: 2015-01-06 8:43:11 PM EST
*/
.bootstrap-select .status {
background: #f0f0f0;
clear: both;
color: #999;
font-size: 11px;
font-style: italic;
font-weight: 500;
line-height: 1;
margin-bottom: -5px;
padding: 10px 20px;
}

View File

@@ -0,0 +1,366 @@
/*!
* Datetimepicker for Bootstrap 3
* ! version : 4.7.14
* https://github.com/Eonasdan/bootstrap-datetimepicker/
*/
.bootstrap-datetimepicker-widget {
list-style: none;
}
.bootstrap-datetimepicker-widget.dropdown-menu {
margin: 2px 0;
padding: 4px;
width: 19em;
}
@media (min-width: 768px) {
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
width: 38em;
}
}
@media (min-width: 992px) {
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
width: 38em;
}
}
@media (min-width: 1200px) {
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
width: 38em;
}
}
.bootstrap-datetimepicker-widget.dropdown-menu:before,
.bootstrap-datetimepicker-widget.dropdown-menu:after {
content: '';
display: inline-block;
position: absolute;
}
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before {
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #cccccc;
border-bottom-color: rgba(0, 0, 0, 0.2);
top: -7px;
left: 7px;
}
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid white;
top: -6px;
left: 8px;
}
.bootstrap-datetimepicker-widget.dropdown-menu.top:before {
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 7px solid #cccccc;
border-top-color: rgba(0, 0, 0, 0.2);
bottom: -7px;
left: 6px;
}
.bootstrap-datetimepicker-widget.dropdown-menu.top:after {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid white;
bottom: -6px;
left: 7px;
}
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before {
left: auto;
right: 6px;
}
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after {
left: auto;
right: 7px;
}
.bootstrap-datetimepicker-widget .list-unstyled {
margin: 0;
}
.bootstrap-datetimepicker-widget a[data-action] {
padding: 6px 0;
}
.bootstrap-datetimepicker-widget a[data-action]:active {
box-shadow: none;
}
.bootstrap-datetimepicker-widget .timepicker-hour,
.bootstrap-datetimepicker-widget .timepicker-minute,
.bootstrap-datetimepicker-widget .timepicker-second {
width: 54px;
font-weight: bold;
font-size: 1.2em;
margin: 0;
}
.bootstrap-datetimepicker-widget button[data-action] {
padding: 6px;
}
.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Increment Hours";
}
.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Increment Minutes";
}
.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Decrement Hours";
}
.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Decrement Minutes";
}
.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Show Hours";
}
.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Show Minutes";
}
.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Toggle AM/PM";
}
.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Clear the picker";
}
.bootstrap-datetimepicker-widget .btn[data-action="today"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Set the date to today";
}
.bootstrap-datetimepicker-widget .picker-switch {
text-align: center;
}
.bootstrap-datetimepicker-widget .picker-switch::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Toggle Date and Time Screens";
}
.bootstrap-datetimepicker-widget .picker-switch td {
padding: 0;
margin: 0;
height: auto;
width: auto;
line-height: inherit;
}
.bootstrap-datetimepicker-widget .picker-switch td span {
line-height: 2.5;
height: 2.5em;
width: 100%;
}
.bootstrap-datetimepicker-widget table {
width: 100%;
margin: 0;
}
.bootstrap-datetimepicker-widget table td,
.bootstrap-datetimepicker-widget table th {
text-align: center;
border-radius: 4px;
}
.bootstrap-datetimepicker-widget table th {
height: 20px;
line-height: 20px;
width: 20px;
}
.bootstrap-datetimepicker-widget table th.picker-switch {
width: 145px;
}
.bootstrap-datetimepicker-widget table th.disabled,
.bootstrap-datetimepicker-widget table th.disabled:hover {
background: none;
color: #777777;
cursor: not-allowed;
}
.bootstrap-datetimepicker-widget table th.prev::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Previous Month";
}
.bootstrap-datetimepicker-widget table th.next::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Next Month";
}
.bootstrap-datetimepicker-widget table thead tr:first-child th {
cursor: pointer;
}
.bootstrap-datetimepicker-widget table thead tr:first-child th:hover {
background: #eeeeee;
}
.bootstrap-datetimepicker-widget table td {
height: 54px;
line-height: 54px;
width: 54px;
}
.bootstrap-datetimepicker-widget table td.cw {
font-size: .8em;
height: 20px;
line-height: 20px;
color: #777777;
}
.bootstrap-datetimepicker-widget table td.day {
height: 20px;
line-height: 20px;
width: 20px;
}
.bootstrap-datetimepicker-widget table td.day:hover,
.bootstrap-datetimepicker-widget table td.hour:hover,
.bootstrap-datetimepicker-widget table td.minute:hover,
.bootstrap-datetimepicker-widget table td.second:hover {
background: #eeeeee;
cursor: pointer;
}
.bootstrap-datetimepicker-widget table td.old,
.bootstrap-datetimepicker-widget table td.new {
color: #777777;
}
.bootstrap-datetimepicker-widget table td.today {
position: relative;
}
.bootstrap-datetimepicker-widget table td.today:before {
content: '';
display: inline-block;
border: 0 0 7px 7px solid transparent;
border-bottom-color: #337ab7;
border-top-color: rgba(0, 0, 0, 0.2);
position: absolute;
bottom: 4px;
right: 4px;
}
.bootstrap-datetimepicker-widget table td.active,
.bootstrap-datetimepicker-widget table td.active:hover {
background-color: #337ab7;
color: #ffffff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.bootstrap-datetimepicker-widget table td.active.today:before {
border-bottom-color: #fff;
}
.bootstrap-datetimepicker-widget table td.disabled,
.bootstrap-datetimepicker-widget table td.disabled:hover {
background: none;
color: #777777;
cursor: not-allowed;
}
.bootstrap-datetimepicker-widget table td span {
display: inline-block;
width: 54px;
height: 54px;
line-height: 54px;
margin: 2px 1.5px;
cursor: pointer;
border-radius: 4px;
}
.bootstrap-datetimepicker-widget table td span:hover {
background: #eeeeee;
}
.bootstrap-datetimepicker-widget table td span.active {
background-color: #337ab7;
color: #ffffff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.bootstrap-datetimepicker-widget table td span.old {
color: #777777;
}
.bootstrap-datetimepicker-widget table td span.disabled,
.bootstrap-datetimepicker-widget table td span.disabled:hover {
background: none;
color: #777777;
cursor: not-allowed;
}
.bootstrap-datetimepicker-widget.usetwentyfour td.hour {
height: 27px;
line-height: 27px;
}
.input-group.date .input-group-addon {
cursor: pointer;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}

File diff suppressed because one or more lines are too long

1061
RIGS/static/css/fullcalendar.css Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,202 @@
/*!
* FullCalendar v2.3.1 Print Stylesheet
* Docs & License: http://fullcalendar.io/
* (c) 2015 Adam Shaw
*/
/*
* Include this stylesheet on your page to get a more printer-friendly calendar.
* When including this stylesheet, use the media='print' attribute of the <link> tag.
* Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css.
*/
.fc {
max-width: 100% !important;
}
/* Global Event Restyling
--------------------------------------------------------------------------------------------------*/
.fc-event {
background: #fff !important;
color: #000 !important;
page-break-inside: avoid;
}
.fc-event .fc-resizer {
display: none;
}
/* Table & Day-Row Restyling
--------------------------------------------------------------------------------------------------*/
th,
td,
hr,
thead,
tbody,
.fc-row {
border-color: #ccc !important;
background: #fff !important;
}
/* kill the overlaid, absolutely-positioned common components */
.fc-bg,
.fc-bgevent-skeleton,
.fc-highlight-skeleton,
.fc-helper-skeleton {
display: none;
}
/* don't force a min-height on rows (for DayGrid) */
.fc tbody .fc-row {
height: auto !important; /* undo height that JS set in distributeHeight */
min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */
}
.fc tbody .fc-row .fc-content-skeleton {
position: static; /* undo .fc-rigid */
padding-bottom: 0 !important; /* use a more border-friendly method for this... */
}
.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */
padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */
}
.fc tbody .fc-row .fc-content-skeleton table {
/* provides a min-height for the row, but only effective for IE, which exaggerates this value,
making it look more like 3em. for other browers, it will already be this tall */
height: 1em;
}
/* Undo month-view event limiting. Display all events and hide the "more" links
--------------------------------------------------------------------------------------------------*/
.fc-more-cell,
.fc-more {
display: none !important;
}
.fc tr.fc-limited {
display: table-row !important;
}
.fc td.fc-limited {
display: table-cell !important;
}
.fc-popover {
display: none; /* never display the "more.." popover in print mode */
}
/* TimeGrid Restyling
--------------------------------------------------------------------------------------------------*/
/* undo the min-height 100% trick used to fill the container's height */
.fc-time-grid {
min-height: 0 !important;
}
/* don't display the side axis at all ("all-day" and time cells) */
.fc-agenda-view .fc-axis {
display: none;
}
/* don't display the horizontal lines */
.fc-slats,
.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */
display: none !important; /* important overrides inline declaration */
}
/* let the container that holds the events be naturally positioned and create real height */
.fc-time-grid .fc-content-skeleton {
position: static;
}
/* in case there are no events, we still want some height */
.fc-time-grid .fc-content-skeleton table {
height: 4em;
}
/* kill the horizontal spacing made by the event container. event margins will be done below */
.fc-time-grid .fc-event-container {
margin: 0 !important;
}
/* TimeGrid *Event* Restyling
--------------------------------------------------------------------------------------------------*/
/* naturally position events, vertically stacking them */
.fc-time-grid .fc-event {
position: static !important;
margin: 3px 2px !important;
}
/* for events that continue to a future day, give the bottom border back */
.fc-time-grid .fc-event.fc-not-end {
border-bottom-width: 1px !important;
}
/* indicate the event continues via "..." text */
.fc-time-grid .fc-event.fc-not-end:after {
content: "...";
}
/* for events that are continuations from previous days, give the top border back */
.fc-time-grid .fc-event.fc-not-start {
border-top-width: 1px !important;
}
/* indicate the event is a continuation via "..." text */
.fc-time-grid .fc-event.fc-not-start:before {
content: "...";
}
/* time */
/* undo a previous declaration and let the time text span to a second line */
.fc-time-grid .fc-event .fc-time {
white-space: normal !important;
}
/* hide the the time that is normally displayed... */
.fc-time-grid .fc-event .fc-time span {
display: none;
}
/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */
.fc-time-grid .fc-event .fc-time:after {
content: attr(data-full);
}
/* Vertical Scroller & Containers
--------------------------------------------------------------------------------------------------*/
/* kill the scrollbars and allow natural height */
.fc-scroller,
.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */
.fc-time-grid-container { /* */
overflow: visible !important;
height: auto !important;
}
/* kill the horizontal border/padding used to compensate for scrollbars */
.fc-row {
border: 0 !important;
margin: 0 !important;
}
/* Button Controls
--------------------------------------------------------------------------------------------------*/
.fc-button-group,
.fc button {
display: none; /* don't display any button-related controls */
}

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