Compare commits

..

1 Commits

Author SHA1 Message Date
16d845ad3a Initial noodling 2022-05-20 17:43:46 +01:00
273 changed files with 23189 additions and 15888 deletions

View File

@@ -1,30 +0,0 @@
# Exclude everything by default, then explicitly include what the image needs.
# This also keeps .env files, keys and local virtualenvs out of the image.
*
# Build tooling
!package.json
!package-lock.json
!gulpfile.js
!pyproject.toml
!uv.lock
!.python-version
!manage.py
# Frontend sources
!pipeline/source_assets/**
# Django project and apps
!PyRIGS/**
!RIGS/**
!assets/**
!training/**
!users/**
!versioning/**
!templates/**
# Never ship these, even from within included directories
**/tests
**/__pycache__
**/*.pyc
**/*.sqlite3

View File

@@ -1,27 +0,0 @@
SECRET_KEY=mysecretkey
DEBUG=true
STAGING=false
DJANGO_ALLOWED_HOSTS=localhost
DOMAIN=
SESSION_COOKIE_SECURE_ENABLED=true
CSRF_COOKIE_SECURE_ENABLED=true
FORUM_WEBHOOK_SECRET=
DATABASE_ENGINE=postgresql
DATABASE_NAME=rigs
DATABASE_USERNAME=rigs
DATABASE_PASSWORD=rigs
DATABASE_HOST=db
DATABASE_PORT=5432
EMAIL_HOST=
EMAIL_PORT=587
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
EMAIL_USE_TLS=
EMAIL_USE_SSL=
EMAIL_FROM=
HCAPTCHA_SITEKEY=
HCAPTCHA_SECRET=
SENTRY_DSN=

View File

@@ -1,32 +0,0 @@
version: 2
updates:
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"
groups:
python-minor-patch:
update-types: ["minor", "patch"]
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
groups:
npm-minor-patch:
update-types: ["minor", "patch"]
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker-compose"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -1,211 +0,0 @@
name: CI
on:
pull_request:
push:
branches: [master]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
PYTHONDONTWRITEBYTECODE: 1
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libcairo2-dev
- uses: astral-sh/setup-uv@v6
with:
python-version-file: ".python-version"
enable-cache: true
- run: uv sync --locked
- name: Pre-commit hooks (ruff, formatting, file hygiene)
run: uv run prek run --all-files --show-diff-on-failure
- name: Django system checks
run: uv run python manage.py check
- name: Check for missing migrations
run: uv run python manage.py makemigrations --check --dry-run
test:
name: Test
runs-on: ubuntu-latest
env:
DATABASE_ENGINE: sqlite3
DATABASE_NAME: db.sqlite3
steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libcairo2-dev
- uses: astral-sh/setup-uv@v6
with:
python-version-file: ".python-version"
enable-cache: true
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: uv sync --locked
- name: Build frontend assets
run: |
npm ci
npm run build
- run: uv run python manage.py collectstatic --noinput
- name: Run tests
run: uv run pytest -n auto --cov
- name: Coveralls
run: uv run coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Each architecture builds on a native runner: emulating arm64 under QEMU (compiling pycairo, npm, uv) was the slow part
docker-build:
name: Docker build (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
needs: [lint, test]
permissions:
contents: read
packages: write
strategy:
fail-fast: true
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v4
# Only authenticate when we are actually going to push (never for PRs, which may come from forks)
- name: Log in to GHCR
if: github.event_name == 'push'
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Registry references must be lowercase, but the repository name is not
- name: Set image name
run: echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
- id: build
name: Build (and push by digest on master)
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
build-args: GIT_SHA=${{ github.sha }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=${{ github.event_name == 'push' }}
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
- name: Export digest
if: github.event_name == 'push'
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
mkdir -p "$RUNNER_TEMP/digests"
touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}"
- name: Upload digest
if: github.event_name == 'push'
uses: actions/upload-artifact@v6
with:
name: digests-${{ strategy.job-index }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
docker:
name: Docker image
runs-on: ubuntu-latest
needs: [docker-build]
if: github.event_name == 'push'
outputs:
tag: ${{ steps.sha-tag.outputs.tag }}
permissions:
contents: read
packages: write
steps:
- uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest
type=sha
- id: sha-tag
name: Pick the commit tag
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
ref="$(grep -m1 ':sha-' <<< "$TAGS")"
test -n "$ref"
echo "tag=${ref##*:}" >> "$GITHUB_OUTPUT"
- name: Create multi-arch manifest
working-directory: ${{ runner.temp }}/digests
run: |
IMAGE="ghcr.io/${GITHUB_REPOSITORY,,}"
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf "$IMAGE@sha256:%s " *)
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: [docker]
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
environment: production
concurrency: deploy-production
steps:
- name: Deploy the image built for this commit
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
TAG: ${{ needs.docker.outputs.tag }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
ssh deploy@rigs.nottinghamtec.co.uk "$TAG"

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

14
.github/workflows/deploy.yml vendored Normal file
View File

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

58
.github/workflows/django.yml vendored Normal file
View File

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

14
.gitignore vendored
View File

@@ -101,17 +101,3 @@ crashlytics.properties
crashlytics-build.properties
.vscode/
screenshots/
# Virutal Environments
.venv/
# Environment variable files (keep .env.example)
.env
.env.*
!.env.example
nginx/certs/
nginx/certbot/
nginx/*.conf
*.pem
*.key

View File

@@ -1,38 +0,0 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-merge-conflict
- id: check-toml
- id: check-yaml
args: [--allow-multiple-documents]
- id: end-of-file-fixer
- id: mixed-line-ending
args: [--fix=lf]
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- repo: local
hooks:
- id: uv-check
name: uv lock check
description: Checks the validity of the uv.lock file.
entry: uv lock --check
language: system
files: pyproject.toml
pass_filenames: false
- id: ruff-lint
name: ruff linting
description: Run ruff linting
entry: uv run ruff check --force-exclude
language: system
'types_or': [python, pyi]
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
name: ruff formatting
description: Run ruff formatting
entry: uv run ruff format --force-exclude
language: system
'types_or': [python, pyi]

View File

@@ -1 +0,0 @@
3.14

6
.slugignore Normal file
View File

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

View File

@@ -1,106 +0,0 @@
# syntax=docker/dockerfile:1
#
# Targets:
# prod (default, last stage) gunicorn image with assets and static files baked in
# dev dependencies only; the source is bind-mounted by compose.override.yml
#
# Stages are ordered so that editing application code only invalidates the cheap final layers:
# apt packages, Python dependencies and node modules are all cached separately.
# ---- Frontend assets (multi-arch: official node image) ----
FROM node:26-slim AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY gulpfile.js ./
COPY pipeline/source_assets ./pipeline/source_assets
RUN npm run build
# ---- Shared Python base: runtime system libraries only ----
FROM python:3.14-slim-trixie AS python-base
# Keep downloaded packages between builds (the default Docker config for Debian images deletes them)
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get install -y --no-install-recommends libcairo2
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_PROJECT_ENVIRONMENT=/opt/venv \
UV_LINK_MODE=copy \
PATH="/opt/venv/bin:$PATH"
# ---- Python dependencies (only rebuilt when pyproject.toml / uv.lock change) ----
FROM python-base AS deps-base
COPY --from=ghcr.io/astral-sh/uv:0.12.21 /uv /uvx /bin/
# pycairo (via z3c.rml) has no wheels and is compiled against cairo
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get install -y --no-install-recommends build-essential pkg-config libcairo2-dev
WORKDIR /app
FROM deps-base AS deps-prod
ENV UV_COMPILE_BYTECODE=1
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev
FROM deps-base AS deps-dev
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project
# ---- Development: dependencies only, source and static assets are mounted at runtime ----
FROM python-base AS dev
COPY --from=deps-dev /opt/venv /opt/venv
WORKDIR /app
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
# ---- Static files: depends on the source, so this is the first stage that reruns on a code change ----
FROM python-base AS static
COPY --from=deps-prod /opt/venv /opt/venv
WORKDIR /app
COPY . /app
COPY --from=assets /app/pipeline/built_assets /app/pipeline/built_assets
# Placeholder values only satisfy settings that are mandatory when DEBUG is off; they are not kept in the image
RUN DEBUG=false EMAIL_HOST=build EMAIL_HOST_USER=build EMAIL_HOST_PASSWORD=build EMAIL_FROM=build@example.com \
python manage.py collectstatic --noinput
# ---- Production image ----
FROM python-base AS prod
RUN addgroup --system app \
&& adduser --system --group --home /home/app app
COPY --from=deps-prod /opt/venv /opt/venv
WORKDIR /app
COPY --chown=app:app . /app
COPY --from=static --chown=app:app /app/static /app/static
COPY --from=assets --chown=app:app /app/pipeline/built_assets /app/pipeline/built_assets
# DEBUG must never be on in a built image; enable it explicitly via the environment if needed
ENV DEBUG=false
# Declared last so a new commit SHA does not invalidate any of the layers above
ARG GIT_SHA=unknown
ENV GIT_SHA=$GIT_SHA
USER app
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "PyRIGS.wsgi"]

View File

@@ -10,4 +10,4 @@ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

107
Pipfile Normal file
View File

@@ -0,0 +1,107 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
ansicolors = "~=1.1.8"
asgiref = "~=3.3.1"
"backports.tempfile" = "~=1.0"
"backports.weakref" = "~=1.0.post1"
beautifulsoup4 = "~=4.9.3"
Brotli = "~=1.0.9"
cachetools = "~=4.2.1"
certifi = "~=2020.12.5"
chardet = "~=4.0.0"
configparser = "~=5.0.1"
contextlib2 = "~=0.6.0.post1"
cssselect = "~=1.1.0"
cssutils = "~=1.0.2"
dj-database-url = "~=0.5.0"
dj-static = "~=0.0.6"
Django = "~=3.2"
django-debug-toolbar = "~=3.2"
django-filter = "~=2.4.0"
django-ical = "~=1.7.1"
django-recurrence = "~=1.10.3"
django-registration-redux = "~=2.9"
django-reversion = "~=3.0.9"
django-toolbelt = "~=0.0.1"
django-widget-tweaks = "~=1.4.8"
django-htmlmin = "~=0.11.0"
envparse = "~=0.2.0"
gunicorn = "~=20.0.4"
icalendar = "~=4.0.7"
idna = "~=2.10"
lxml = "~=4.7.1"
Markdown = "~=3.3.3"
msgpack = "~=1.0.2"
pep517 = "~=0.9.1"
Pillow = "~=9.0.0"
premailer = "~=3.7.0"
progress = "~=1.5"
psutil = "~=5.8.0"
psycopg2 = "~=2.8.6"
Pygments = "~=2.7.4"
pyparsing = "~=2.4.7"
PyPDF2 = "~=1.27.5"
PyPOM = "~=2.2.0"
python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21"
pytz = "~=2020.5"
reportlab = "*"
requests = "~=2.25.1"
retrying = "~=1.3.3"
simplejson = "~=3.17.2"
six = "~=1.15.0"
soupsieve = "~=2.1"
sqlparse = "~=0.4.2"
static3 = "~=0.7.0"
svg2rlg = "~=0.3"
tini = "~=3.0.1"
tornado = "~=6.1"
urllib3 = "~=1.26.5"
whitenoise = "~=5.2.0"
yolk = "~=0.4.3"
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 = "*"
[dev-packages]
selenium = "~=3.141.0"
pycodestyle = "*"
coveralls = "*"
django-coverage-plugin = "*"
pytest-cov = "*"
pytest-django = "*"
pluggy = "*"
pytest-splinter = "*"
pytest = "*"
pytest-reverse = "*"
[requires]
python_version = "3.9"
[dev-packages.pytest-xdist]
extras = [ "psutil",]
version = "*"
[dev-packages.PyPOM]
extras = [ "splinter",]
version = "*"

1958
Pipfile.lock generated Normal file

File diff suppressed because it is too large Load Diff

2
Procfile Normal file
View File

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

View File

@@ -9,9 +9,9 @@ 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)
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
@@ -24,7 +24,7 @@ def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
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()}")
return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
_checklogin.__doc__ = view_func.__doc__
_checklogin.__dict__ = view_func.__dict__
@@ -44,7 +44,6 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
"""
if not login_url:
from django.conf import settings
login_url = settings.LOGIN_URL
def _dec(view_func):
@@ -55,9 +54,9 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
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()}")
return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}')
else:
resp = render(request, "403.html")
resp = render(request, '403.html')
resp.status_code = 403
return resp
@@ -80,17 +79,15 @@ def api_key_required(function):
"""
Decorator for views that checks api_pk and api_key.
Failed users will be given a 403 error.
Should only be used for urls which include <api_pk> and <api_key> kwargs.
Will update the kwargs to include the user object if successful (under the key 'user').
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")
userid = kwargs.get('api_pk')
key = kwargs.get('api_key')
error_resp = render(request, "403.html")
error_resp = render(request, '403.html')
error_resp.status_code = 403
if key is None:
@@ -100,7 +97,6 @@ def api_key_required(function):
try:
user_object = models.Profile.objects.get(pk=userid)
kwargs = {**kwargs, "user": user_object}
except models.Profile.DoesNotExist:
return error_resp
@@ -118,8 +114,8 @@ def nottinghamtec_address_required(function):
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")
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)

View File

@@ -1,3 +1,3 @@
DATETIME_FORMAT = "d/m/Y H:i"
DATE_FORMAT = "d/m/Y"
TIME_FORMAT = "H:i"
DATETIME_FORMAT = ('d/m/Y H:i')
DATE_FORMAT = ('d/m/Y')
TIME_FORMAT = ('H:i')

View File

@@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
import datetime
from pathlib import Path
import secrets
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
@@ -19,233 +20,229 @@ from envparse import env
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env("SECRET_KEY", default="gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e")
SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env("DEBUG", cast=bool, default=True)
STAGING = env("STAGING", cast=bool, default=False)
CI = env("CI", cast=bool, default=False)
# Commit the running image was built from (baked in at build time)
GIT_SHA = env("GIT_SHA", default="unknown")
DEBUG = env('DEBUG', cast=bool, default=True)
STAGING = env('STAGING', cast=bool, default=False)
CI = env('CI', cast=bool, default=False)
ALLOWED_HOSTS = [
host.strip() for host in env("DJANGO_ALLOWED_HOSTS", default="rigs.nottinghamtec.co.uk").split(",") if host.strip()
]
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
if STAGING:
ALLOWED_HOSTS.append('.herokuapp.com')
if DEBUG:
CSRF_TRUSTED_ORIGINS = [f"http://{host}" for host in ALLOWED_HOSTS]
CSRF_TRUSTED_ORIGINS.append("http://localhost:8000")
CSRF_TRUSTED_ORIGINS.append("http://localhost:8001")
ALLOWED_HOSTS = ["*"]
else:
# Django 4+ checks the Origin header on unsafe requests against this list, which
# matters when TLS is terminated by a proxy in front of us.
CSRF_TRUSTED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS]
ALLOWED_HOSTS.append('localhost')
ALLOWED_HOSTS.append('example.com')
ALLOWED_HOSTS.append('127.0.0.1')
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
if not DEBUG:
SECURE_SSL_REDIRECT = True # Redirect all http requests to https
SECURE_HSTS_SECONDS = 3600
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = env("SESSION_COOKIE_SECURE_ENABLED", cast=bool, default=True)
CSRF_COOKIE_SECURE = env("CSRF_COOKIE_SECURE_ENABLED", cast=bool, default=True)
INTERNAL_IPS = ["127.0.0.1"]
INTERNAL_IPS = ['127.0.0.1']
DOMAIN = env("DOMAIN", default="example.com")
ADMINS = [("IT Manager", f"it@{DOMAIN}")]
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
if DEBUG:
ADMINS.append(("Testing Superuser", "superuser@example.com"))
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",
"registration",
"reversion",
"widget_tweaks",
"hcaptcha",
"massadmin",
'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',
'debug_toolbar',
'registration',
'reversion',
'widget_tweaks',
'hcaptcha',
'massadmin',
)
MIDDLEWARE = (
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"reversion.middleware.RevisionMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"htmlmin.middleware.HtmlMinifyMiddleware",
"htmlmin.middleware.MarkRequestMiddleware",
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'htmlmin.middleware.HtmlMinifyMiddleware',
'htmlmin.middleware.MarkRequestMiddleware',
)
ROOT_URLCONF = "PyRIGS.urls"
ROOT_URLCONF = 'PyRIGS.urls'
WSGI_APPLICATION = "PyRIGS.wsgi.application"
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
# Database
DATABASES = {
"default": {
"ENGINE": "django.db.backends.{}".format(env("DATABASE_ENGINE", default="sqlite3")),
"NAME": env("DATABASE_NAME", default="rigs"),
"USER": env("DATABASE_USERNAME", default="rigs"),
"PASSWORD": env("DATABASE_PASSWORD", default="rigs"),
"HOST": env("DATABASE_HOST", default="127.0.0.1"),
"PORT": env("DATABASE_PORT", 5432),
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': str(BASE_DIR / 'db.sqlite3'),
}
}
if not DEBUG:
import dj_database_url
DATABASES['default'] = dj_database_url.config()
# Logging
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": (
"%(asctime)s [%(process)d] [%(levelname)s] "
+ "pathname=%(pathname)s lineno=%(lineno)s "
+ "funcname=%(funcName)s %(message)s"
),
"datefmt": "%Y-%m-%d %H:%M:%S",
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': ('%(asctime)s [%(process)d] [%(levelname)s] ' +
'pathname=%(pathname)s lineno=%(lineno)s ' +
'funcname=%(funcName)s %(message)s'),
'datefmt': '%Y-%m-%d %H:%M:%S'
},
"simple": {"format": "%(levelname)s %(message)s"},
'simple': {
'format': '%(levelname)s %(message)s'
}
},
"handlers": {
"null": {
"level": "DEBUG",
"class": "logging.NullHandler",
'handlers': {
'null': {
'level': 'DEBUG',
'class': 'logging.NullHandler',
},
"console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose"},
"mail_admins": {
"class": "django.utils.log.AdminEmailHandler",
"level": "ERROR",
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'mail_admins': {
'class': 'django.utils.log.AdminEmailHandler',
'level': 'ERROR',
# But the emails are plain text by default - HTML is nicer
"include_html": True,
'include_html': True,
},
},
"loggers": {
'loggers': {
# Again, default Django configuration to email unhandled exceptions
"django.request": {
"handlers": ["mail_admins"],
"level": "ERROR",
"propagate": True,
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
# Might as well log any errors anywhere else in Django
"django": {
"handlers": ["console"],
"level": "ERROR",
"propagate": False,
'django': {
'handlers': ['console'],
'level': 'ERROR',
'propagate': False,
},
# Ignore dangling object references
"pypdf": {
"level": "ERROR",
"propagate": False,
},
},
}
}
# Tests lock up SQLite otherwise
if STAGING or CI:
CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
}
}
elif DEBUG:
CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
}
}
else:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "cache_table",
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'cache_table',
}
}
# Error/performance monitoring
sentry_sdk.init(
dsn=env("SENTRY_DSN", default=""),
dsn=env('SENTRY_DSN', default=""),
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
# User system
AUTH_USER_MODEL = "RIGS.Profile"
AUTH_USER_MODEL = 'RIGS.Profile'
LOGIN_REDIRECT_URL = "/"
LOGIN_URL = "/user/login/"
LOGOUT_URL = "/user/logout/"
LOGIN_REDIRECT_URL = '/'
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")
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001')
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000')
# 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_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')
else:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_COOLDOWN = datetime.timedelta(minutes=15)
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
LANGUAGE_CODE = "en-gb"
LANGUAGE_CODE = 'en-gb'
TIME_ZONE = "Europe/London"
TIME_ZONE = 'Europe/London'
FORMAT_MODULE_PATH = "PyRIGS.formats"
FORMAT_MODULE_PATH = 'PyRIGS.formats'
USE_I18N = True
USE_L10N = True
USE_TZ = True
USE_THOUSAND_SEPARATOR = False
# 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')
# Static files (CSS, JavaScript, Images)
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
STATIC_URL = "/static/"
STATIC_ROOT = str(BASE_DIR / "static/")
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
STATIC_URL = '/static/'
STATIC_ROOT = str(BASE_DIR / 'static/')
STATICFILES_DIRS = [
str(BASE_DIR / "pipeline/built_assets"),
str(BASE_DIR / 'pipeline/built_assets'),
]
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
'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",
@@ -254,10 +251,8 @@ TEMPLATES = [
"django.template.context_processors.tz",
"django.template.context_processors.request",
"django.contrib.messages.context_processors.messages",
"PyRIGS.views.ajax_context",
"PyRIGS.views.version_context",
],
"debug": DEBUG,
'debug': DEBUG
},
},
]
@@ -265,6 +260,6 @@ TEMPLATES = [
USE_GRAVATAR = True
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
AUTHORISATION_NOTIFICATION_ADDRESS = "productions@nottinghamtec.co.uk"
AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk'
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

View File

@@ -1,8 +1,16 @@
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
@@ -15,7 +23,6 @@ def create_datetime(year, month, day, hour, minute):
def create_browser():
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
options.add_argument("--lang=en-GB")
options.add_argument("--headless")
if settings.CI:
options.add_argument("--no-sandbox")
@@ -23,30 +30,76 @@ def create_browser():
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("Error in test {} is at path {}".format(screenshot_name, screenshot_file), file=sys.stderr)
raise e
return wrapper_func
def screenshot_failure_cls(cls):
for attr in cls.__dict__:
if callable(getattr(cls, attr)) and attr.startswith("test"):
setattr(cls, attr, screenshot_failure(getattr(cls, attr)))
return cls
def assert_times_almost_equal(first_time, second_time):
assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0)
def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url):
# Test the meta tag is in place
response = client.get(event_url, follow=True, HTTP_HOST="example.com")
assertContains(response, "application/json+oembed")
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")
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")
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
)
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

86
PyRIGS/tests/pages.py Normal file
View File

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

262
PyRIGS/tests/regions.py Normal file
View File

@@ -0,0 +1,262 @@
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,16 +1,21 @@
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
from pytest_django.asserts import assertTemplateUsed, assertInHTML
from PyRIGS import urls
from RIGS.models import Event
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
from django.test import TestCase, TransactionTestCase
from django.test.utils import override_settings
@@ -31,28 +36,18 @@ def get_request_url(url):
try:
kwargz = {}
if ":pk>" in pattern:
kwargz["pk"] = 1
kwargz['pk'] = 1
if ":model>" in pattern:
kwargz["model"] = "event"
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",
],
)
@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)
@@ -60,93 +55,92 @@ def test_production_exception(command):
class TestSampleDataGenerator(TestCase):
@override_settings(DEBUG=True)
def test_sample_data(self):
call_command("generateSampleData")
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()
call_command('deleteSampleData')
assert Asset.objects.all().count() == 0
assert Event.objects.all().count() == 0
@override_settings(DEBUG=True)
@pytest.mark.skip(reason="broken")
def test_unauthenticated(client): # Nothing should be available to the unauthenticated
call_command("generateSampleData")
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")
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}"
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
else:
expected_url = f"{reverse('login')}?next={request_url}"
expected_url = "{0}?next={1}".format(reverse('login'), request_url)
assertRedirects(response, expected_url)
call_command("deleteSampleData")
call_command('deleteSampleData')
@override_settings(DEBUG=True)
@pytest.mark.skip(reason="broken")
def test_basic_access(client):
call_command("generateSampleData")
call_command('generateSampleData')
assert client.login(username="basic", password="basic")
url = reverse("asset_list")
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
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})
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = client.get(url)
assertNotContains(response, "Purchase Details")
assertNotContains(response, "View Revision History")
assertNotContains(response, 'Purchase Details')
assertNotContains(response, 'View Revision History')
urlz = {"asset_history", "asset_update", "asset_duplicate"}
urlz = {'asset_history', 'asset_update', 'asset_duplicate'}
for url_name in urlz:
request_url = reverse(url_name, kwargs={"pk": Asset.objects.first().asset_id})
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")
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})
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")
call_command('deleteSampleData')
@override_settings(DEBUG=True)
@pytest.mark.skip(reason="broken")
def test_keyholder_access(client):
call_command("generateSampleData")
call_command('generateSampleData')
assert client.login(username="keyholder", password="keyholder")
url = reverse("asset_list")
url = reverse('asset_list')
response = client.get(url)
# Check edit and duplicate buttons shown in list
assertContains(response, "Edit")
assertContains(response, "Duplicate")
assertContains(response, 'Edit')
assertContains(response, 'Duplicate')
url = reverse("asset_detail", kwargs={"pk": Asset.objects.first().asset_id})
url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id})
response = client.get(url)
assertContains(response, "Purchase Details")
assertContains(response, "View Revision History")
assertContains(response, 'Purchase Details')
assertContains(response, 'View Revision History')
client.logout()
call_command("deleteSampleData")
call_command('deleteSampleData')
def test_search(admin_client, admin_user):
url = reverse("search")
response = admin_client.get(url, {"q": "Definetelynothingfoundifwesearchthis"})
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})
response = admin_client.get(url, {'q': admin_user.first_name})
assertContains(response, admin_user.first_name)

View File

@@ -1,29 +1,43 @@
from django.conf import settings
from django.conf.urls import include
from django.contrib import admin
from django.contrib.auth.decorators import login_required
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path
from django.urls import path
from django.views.generic import TemplateView
from PyRIGS import views
urlpatterns = [
path("", include("versioning.urls")),
path("", include("RIGS.urls")),
path("assets/", include("assets.urls")),
path("training/", include("training.urls")),
path("", login_required(views.Index.as_view()), name="index"),
path('', include('versioning.urls')),
path('', include('RIGS.urls')),
path('assets/', include('assets.urls')),
path('training/', include('training.urls')),
path('', login_required(views.Index.as_view()), name='index'),
# API
path("api/<str:model>/", login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
path("api/<str:model>/<int:pk>/", login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
path("closemodal/", views.CloseModal.as_view(), name="closemodal"),
path("search/", login_required(views.Search.as_view()), name="search"),
path("search_help/", login_required(views.SearchHelp.as_view()), name="search_help"),
path("", include("users.urls")),
path("admin/", include("massadmin.urls")),
path("admin/", admin.site.urls),
path('api/<str:model>/', login_required(views.SecureAPIRequest.as_view()),
name="api_secure"),
path('api/<str:model>/<int:pk>/', login_required(views.SecureAPIRequest.as_view()),
name="api_secure"),
path('closemodal/', views.CloseModal.as_view(), name='closemodal'),
path('search/', login_required(views.Search.as_view()), name='search'),
path('search_help/', login_required(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")),
]
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")),
]

View File

@@ -9,10 +9,11 @@ from functools import reduce
from itertools import chain
from io import BytesIO
from pypdf import PdfReader, PdfWriter
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
@@ -21,6 +22,7 @@ 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
@@ -30,71 +32,53 @@ from training import models as training_models
def is_ajax(request):
return request.headers.get("x-requested-with") == "XMLHttpRequest"
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
def ajax_context(request):
"""Template context processor exposing ``is_ajax`` to every template."""
return {"is_ajax": is_ajax(request)}
def version_context(request):
"""Template context processor exposing the running commit to every template."""
sha = settings.GIT_SHA
return {"git_sha": sha, "git_sha_short": sha[:7]}
def get_related(
form, context
): # Get some other objects to include in the form. Used when there are errors but also nice and quick.
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 != "":
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"
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)
)
context['rig_count'] = models.Event.objects.rig_count()
return context
class SecureAPIRequest(generic.View):
models = {
"venue": models.Venue,
"person": models.Person,
"organisation": models.Organisation,
"profile": models.Profile,
"event": models.Event,
"asset": asset_models.Asset,
"supplier": asset_models.Supplier,
"training_item": training_models.TrainingItem,
'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,
'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:
@@ -106,16 +90,16 @@ class SecureAPIRequest(generic.View):
def get(self, request, model, pk=None, param=None):
# Request permission validation things
key = request.GET.get("apikey", None)
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)
format = request.GET.get('format', 'json')
fields = request.GET.get('fields', None)
if fields:
fields = fields.split(",")
filters = request.GET.get("filters", [])
filters = request.GET.get('filters', [])
if filters:
filters = filters.split(",")
@@ -126,10 +110,10 @@ class SecureAPIRequest(generic.View):
return HttpResponse(data, content_type="application/" + format)
# Supply data for autocomplete ajax request in json form
term = request.GET.get("q", None)
term = request.GET.get('q', None)
if term:
if fields is None: # Default to just name
fields = ["name"]
fields = ['name']
# Build a list of Q objects for use later
queries = []
@@ -149,18 +133,14 @@ class SecureAPIRequest(generic.View):
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,
'pk': o.pk,
'value': o.pk,
'text': o.name,
}
try: # See if there is a valid update URL
data["update"] = reverse(f"{model}_update", kwargs={"pk": o.pk})
data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk})
except NoReverseMatch:
pass
results.append(data)
@@ -168,8 +148,8 @@ class SecureAPIRequest(generic.View):
# return a data response
return JsonResponse(results, safe=False)
start = request.GET.get("start", None)
end = request.GET.get("end", None)
start = request.GET.get('start', None)
end = request.GET.get('end', None)
if model == "event" and start and end:
# Probably a calendar request
@@ -181,13 +161,13 @@ class SecureAPIRequest(generic.View):
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()),
'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)
@@ -199,35 +179,32 @@ class SecureAPIRequest(generic.View):
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}))
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}'")
messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'")
else:
url = reverse_lazy(
detail,
kwargs={
"pk": self.object.pk,
},
)
url = reverse_lazy(detail, kwargs={
'pk': self.object.pk,
})
return url
class GenericListView(generic.ListView):
template_name = "generic_list.html"
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"
context['page_title'] = self.model.__name__ + "s"
if is_ajax(self.request):
context["override"] = "base_ajax.html"
context['override'] = "base_ajax.html"
return context
def get_queryset(self):
object_list = self.model.objects.search(query=self.request.GET.get("q", ""))
object_list = self.model.objects.search(query=self.request.GET.get('q', ""))
orderBy = self.request.GET.get("orderBy", "name")
orderBy = self.request.GET.get('orderBy', "name")
if orderBy != "":
object_list = object_list.order_by(orderBy)
return object_list
@@ -238,9 +215,9 @@ class GenericDetailView(generic.DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = f"{self.model.__name__} | {self.object.name}"
context['page_title'] = f"{self.model.__name__} | {self.object.name}"
if is_ajax(self.request):
context["override"] = "base_ajax.html"
context['override'] = "base_ajax.html"
return context
@@ -249,9 +226,9 @@ class GenericUpdateView(generic.UpdateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = f"Edit {self.model.__name__}"
context['page_title'] = f"Edit {self.model.__name__}"
if is_ajax(self.request):
context["override"] = "base_ajax.html"
context['override'] = "base_ajax.html"
return context
@@ -260,27 +237,27 @@ class GenericCreateView(generic.CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = f"Create {self.model.__name__}"
context['page_title'] = f"Create {self.model.__name__}"
if is_ajax(self.request):
context["override"] = "base_ajax.html"
context['override'] = "base_ajax.html"
return context
class Search(generic.ListView):
template_name = "search_results.html"
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>"
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)
query = request.GET.get('q', None)
if query is not None:
event_results = models.Event.objects.search(query)
@@ -305,14 +282,16 @@ class Search(generic.ListView):
trainee_results,
training_item_results,
)
qs = sorted(queryset_chain, key=lambda instance: instance.pk, reverse=True)
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"
template_name = 'search_help.html'
class CloseModal(generic.TemplateView):
@@ -321,11 +300,10 @@ class CloseModal(generic.TemplateView):
May optionally also include some javascript in a success message to cause a load of
the new information onto the page.
"""
template_name = "closemodal.html"
template_name = 'closemodal.html'
def get_context_data(self, **kwargs):
return {"messages": messages.get_messages(self.request)}
return {'messages': messages.get_messages(self.request)}
class OEmbedView(generic.View):
@@ -334,69 +312,54 @@ class OEmbedView(generic.View):
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",
'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 = PdfWriter()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfReader(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)
obj = get_object_or_404(self.model, pk=self.kwargs['pk'])
user_str = f"by {self.request.user.name} " if self.request.user is not None else ""
time = timezone.now().strftime('%d/%m/%Y %H:%I')
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}]",
'object': obj,
'current_user': self.request.user,
'object_name': object_name,
'info_string': f"[Paperwork generated {user_str}on {time} - {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)
template = get_template(self.template_name)
merger = PdfFileMerger()
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
context = self.get_context_data()
def get(self, request):
self.object_list = self.get_queryset()
return render_pdf_response(get_template(self.template_name), self.get_context_data(), False)
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
if self.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

View File

@@ -6,11 +6,11 @@ It exposes the WSGI callable as a module-level variable named ``application``.
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
application = get_wsgi_application()
application = Cling(get_wsgi_application())

View File

@@ -1,73 +1,18 @@
# TEC PA & Lighting - PyRIGS #
![Build Status](https://github.com/nottinghamtec/PyRIGS/workflows/Django%20CI/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS)
[![Maintainability](https://api.codeclimate.com/v1/badges/79ca3b8106911a1d143f/maintainability)](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability)
Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets.
For setup information and other such helpful stuff check the [Wiki](https://github.com/nottinghamtec/PyRIGS/wiki)
# 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.
# Running locally
> [!WARNING]
> `compose.yml` is for **development only** and must not be run in production. It mounts the source into the container, runs Django's development server with `DEBUG` on by default, and uses placeholder captcha keys. Production is deployed from the image built by the `prod` target of the `Dockerfile`.
The compose stack runs Postgres, the Django development server and a gulp watcher that rebuilds the CSS/JS. Changes to Python code and templates reload the app automatically, and changes to `pipeline/source_assets` are rebuilt into `pipeline/built_assets` (refresh the browser to pick them up).
1. Copy `.env.example` to `.env` and fill it in. For local use set at least:
```
DEBUG=true
DJANGO_ALLOWED_HOSTS=localhost
```
The sample data commands refuse to run unless `DEBUG` (or `STAGING`) is true.
2. Start the stack (migrations run automatically on start):
```
docker compose up -d --build
```
The first start installs the node modules, so give the assets a minute to appear. After changing `pyproject.toml`, `uv.lock` or the `Dockerfile`, rebuild with `docker compose up -d --build` (or run `docker compose watch`).
3. Open <http://localhost:8000/>.
## Creating a user
```
docker compose exec pyrigs python manage.py createsuperuser
```
## Sample data
Load sample users, events, assets and training data (run once on a fresh database):
```
docker compose exec pyrigs python manage.py generateSampleData
```
The individual commands (`generateSampleUserData`, `generateSampleRIGSData`, `generateSampleAssetsData`, `generateSampleTrainingData`) can be run separately, but run the user one first. To remove the sample data again (requires `DEBUG=true`):
```
docker compose exec pyrigs python manage.py deleteSampleData
```
The sample data creates these logins (the password is the same as the username):
| Username | Password | Access |
|---|---|---|
| `superuser` | `superuser` | Superuser |
| `finance` | `finance` | Finance and Keyholders groups |
| `hs` | `hs` | H&S and Keyholders groups |
| `keyholder` | `keyholder` | Keyholders group |
| `basic` | `basic` | Approved user, no groups |
It also creates a number of generic profiles (e.g. `AmyPond`, `RoryWilliams`) which have no password set, so they can't be logged into.
# Development
Linting and formatting use [ruff](https://docs.astral.sh/ruff/), run through [prek](https://github.com/j178/prek) (a pre-commit compatible hook runner) using `.pre-commit-config.yaml`:
```
uv sync
uv run prek install # run the hooks on every commit
uv run prek run --all-files
```
[![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)
# TEC PA & Lighting - PyRIGS #
![Build Status](https://github.com/nottinghamtec/PyRIGS/workflows/Django%20CI/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS)
[![Maintainability](https://api.codeclimate.com/v1/badges/79ca3b8106911a1d143f/maintainability)](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability)
Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets.
For setup information and other such helpful stuff check the [Wiki](https://github.com/nottinghamtec/PyRIGS/wiki)
# Apps
- PyRIGS: Base app, stores 'global' information
- RIGS: Rigboard stuff - event calendar etc
- assets: Database of our kit, testing data etc
- versioning: Our custom logic built on top of django-reversion. Semi-modular.
- users: Our custom logic for registration and profiles. Semi-modular.
- training: SoonTM
[![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)

View File

@@ -20,7 +20,6 @@ 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)
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
@@ -32,11 +31,15 @@ def merge_model_instances(primary_object, alias_objects):
"""
# get related fields
related_fields = list(filter(lambda x: x.is_relation is True, primary_object._meta.get_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))
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))
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
@@ -65,7 +68,10 @@ def merge_model_instances(primary_object, alias_objects):
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)
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
@@ -102,34 +108,31 @@ def merge_model_instances(primary_object, alias_objects):
class AssociateAdmin(VersionAdmin):
search_fields = ["id", "name"]
list_display_links = ["id", "name"]
actions = ["merge"]
search_fields = ['id', 'name']
list_display_links = ['id', 'name']
actions = ['merge']
def get_queryset(self, request):
return super().get_queryset(request).annotate(event_count=Count("event"))
return super().get_queryset(request).annotate(event_count=Count('event'))
def number_of_events(self, obj):
return obj.latest_events.count()
number_of_events.admin_order_field = "event_count"
number_of_events.admin_order_field = 'event_count'
def merge(self, request, queryset):
if request.POST.get("post"): # Has the user confirmed which is the master record?
if request.POST.get('post'): # Has the user confirmed which is the master record?
try:
master_object_pk = request.POST.get("master")
master_object_pk = request.POST.get('master')
master_object = queryset.get(pk=master_object_pk)
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")
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.")
else: # Present the confirmation screen
class TempForm(ModelForm):
class Meta:
model = queryset.model
@@ -140,51 +143,38 @@ class AssociateAdmin(VersionAdmin):
forms.append(TempForm(instance=obj))
context = {
"title": _("Are you sure?"),
"queryset": queryset,
"action_checkbox_name": helpers.ACTION_CHECKBOX_NAME,
"forms": forms,
'title': _("Are you sure?"),
'queryset': queryset,
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
'forms': forms
}
return TemplateResponse(request, "admin_associate_merge.html", context)
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",
"date_joined",
)
list_display_links = ["username"]
list_filter = UserAdmin.list_filter + ("is_approved", "date_joined")
list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events')
list_display_links = ['username']
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")}),
(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"),
},
),
(None, {
'classes': ('wide',),
'fields': ('username', 'password1', 'password2'),
}),
)
form = user_forms.ProfileChangeForm
add_form = user_forms.ProfileCreationForm
actions = ["approve_user", "merge"]
actions = ['approve_user', 'merge']
merge_fields = ["username", "first_name", "last_name", "initials", "email", "phone", "is_supervisor"]
merge_fields = ['username', 'first_name', 'last_name', 'initials', 'email', 'phone', 'is_supervisor']
def approve_user(modeladmin, request, queryset):
queryset.update(is_approved=True)
@@ -192,32 +182,27 @@ class ProfileAdmin(UserAdmin, AssociateAdmin):
@admin.register(models.Person)
class PersonAdmin(AssociateAdmin):
list_display = ("id", "name", "phone", "email", "number_of_events")
merge_fields = ["name", "phone", "email", "address", "notes"]
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
merge_fields = ['name', 'phone', 'email', 'address', 'notes']
@admin.register(models.Venue)
class VenueAdmin(AssociateAdmin):
list_display = ("id", "name", "phone", "email", "number_of_events")
merge_fields = ["name", "phone", "email", "address", "notes", "three_phase_available"]
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available']
@admin.register(models.Organisation)
class OrganisationAdmin(AssociateAdmin):
list_display = ("id", "name", "phone", "email", "number_of_events")
merge_fields = ["name", "phone", "email", "address", "notes", "union_account"]
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")
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 PowerTestRecordAdmin(VersionAdmin):
list_display = ("id", "event", "reviewed_at", "reviewed_by")
list_display = ('id', 'event', 'reviewed_at', 'reviewed_by')

View File

@@ -2,7 +2,7 @@ from django.apps import AppConfig
class RIGSAppConfig(AppConfig):
name = "RIGS"
name = 'RIGS'
def ready(self):
import RIGS.signals # noqa: F401 (registers signal handlers)
import RIGS.signals

View File

@@ -1,19 +1,19 @@
from datetime import timedelta
from datetime import datetime
import simplejson
from django import forms
from django.conf import settings
from django.core import serializers
from django.utils import timezone
from django.utils.html import format_html
from reversion import revisions as reversion
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")
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')
# Events Shit
@@ -21,53 +21,48 @@ class EventForm(forms.ModelForm):
datetime_input_formats = list(settings.DATETIME_INPUT_FORMATS)
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
access_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
parking_and_access = forms.BooleanField(
label="Additional parking or access requirements (i.e. campus parking permits, wristbands)?", required=False
)
items_json = forms.CharField()
items = {}
related_models = {
"person": models.Person,
"organisation": models.Organisation,
"venue": models.Venue,
"mic": models.Profile,
"checked_in_by": models.Profile,
'person': models.Person,
'organisation': models.Organisation,
'venue': models.Venue,
'mic': models.Profile,
'checked_in_by': models.Profile,
}
@property
def _get_items_json(self):
items = {}
if self.instance.pk is None:
return items
for item in self.instance.items.all():
data = serializers.serialize("json", [item])
data = serializers.serialize('json', [item])
struct = simplejson.loads(data)
items[item.pk] = simplejson.dumps(struct[0])
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"
self.fields["end_date"].widget.format = "%Y-%m-%d"
self.fields['items_json'].initial = self._get_items_json
self.fields['start_date'].widget.format = '%Y-%m-%d'
self.fields['end_date'].widget.format = '%Y-%m-%d'
self.fields["access_at"].widget.format = "%Y-%m-%dT%H:%M:%S"
self.fields["meet_at"].widget.format = "%Y-%m-%dT%H:%M:%S"
self.fields['access_at'].widget.format = '%Y-%m-%dT%H:%M:%S'
self.fields['meet_at'].widget.format = '%Y-%m-%dT%H:%M:%S'
def init_items(self):
self.items = self.process_items_json()
return self.items
def process_items_json(self, event=None):
data = simplejson.loads(self.cleaned_data["items_json"])
data = simplejson.loads(self.cleaned_data['items_json'])
items = {}
for key in data:
pk = int(key)
items[pk] = self._get_or_initialise_item(pk, data[key]["fields"], event)
items[pk] = self._get_or_initialise_item(pk, data[key]['fields'], event)
return items
@@ -82,45 +77,32 @@ class EventForm(forms.ModelForm):
item = models.EventItem()
# Take the data from the form and update the item object
item.name = data["name"]
item.description = data["description"]
item.quantity = data["quantity"]
item.cost = data["cost"]
item.order = data["order"]
item.name = data['name']
item.description = data['description']
item.quantity = data['quantity']
item.cost = data['cost']
item.order = data['order']
if event:
if (event):
item.event = event
item.full_clean()
else:
item.full_clean("event")
item.full_clean('event')
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")
):
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"
)
access = self.cleaned_data.get("access_at")
if (
"warn-access" not in self.data
and access is not None
and access.date() < (self.cleaned_data.get("start_date") - timedelta(days=7))
):
raise forms.ValidationError(
format_html(
"Are you sure about that? Your access time seems a bit optimistic. If you're sure, save again. <input type='hidden' id='warn-access' name='warn-access' value='0'/>"
),
code="access_sanity",
)
'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)
if commit:
if (commit):
m.save()
cur_items = m.items.all()
items = self.process_items_json(m)
@@ -136,29 +118,10 @@ class EventForm(forms.ModelForm):
class Meta:
model = models.Event
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",
"parking_and_access",
]
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']
class BaseClientEventAuthorisationForm(forms.ModelForm):
@@ -166,9 +129,9 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
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()
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(BaseClientEventAuthorisationForm, self).clean()
class Meta:
abstract = True
@@ -177,115 +140,146 @@ class BaseClientEventAuthorisationForm(forms.ModelForm):
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.fields["uni_id"].required = True
self.fields["account_code"].required = True
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")
fields = ('tos', 'name', 'amount', 'uni_id', 'account_code')
class EventAuthorisationRequestForm(forms.Form):
email = forms.EmailField(required=True, label="Authoriser Email")
email = forms.EmailField(required=True, label='Authoriser Email')
class EventRiskAssessmentForm(forms.ModelForm):
related_models = {
"power_mic": models.Profile,
'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":
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": "form-check-input", "required": "true"}
)
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"):
power_mic = self.cleaned_data.get("power_mic")
if power_mic is None:
self.add_error(
"power_mic",
forms.ValidationError("A Power MIC is required for big power.", code="power_mic_required"),
)
elif not 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"),
)
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()
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(EventRiskAssessmentForm, self).clean()
class Meta:
model = models.RiskAssessment
fields = "__all__"
exclude = ["reviewed_at", "reviewed_by"]
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"
self.fields['date'].widget.format = '%Y-%m-%d'
for name, field in self.fields.items():
if field.__class__ == forms.NullBooleanField:
# Only display yes/no to user, the 'none' is only ever set in the background
field.widget = forms.CheckboxInput()
# Parsed from incoming form data by clean, then saved into models when the form is saved
items = {}
related_models = {
"venue": models.Venue,
'venue': models.Venue,
'power_mic': models.Profile,
}
# Two possible formats
def parsedatetime(self, date_string):
try:
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S'))
except ValueError:
return timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%dT%H:%M'))
# There's probably a thousand better ways to do this, but this one is mine
def clean(self):
vehicles = {key: val for key, val in self.data.items()
if key.startswith('vehicle')}
for key in vehicles:
pk = int(key.split('_')[1])
driver_key = 'driver_' + str(pk)
if(self.data[driver_key] == ''):
raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch')
else:
try:
item = models.EventChecklistVehicle.objects.get(pk=pk)
except models.EventChecklistVehicle.DoesNotExist:
item = models.EventChecklistVehicle()
item.vehicle = vehicles['vehicle_' + str(pk)]
item.driver = models.Profile.objects.get(pk=self.data[driver_key])
item.full_clean('checklist')
# item does not have a database pk yet as it isn't saved
self.items['v' + str(pk)] = item
crewmembers = {key: val for key, val in self.data.items()
if key.startswith('crewmember')}
other_fields = ['start', 'role', 'end']
for key in crewmembers:
pk = int(key.split('_')[1])
for field in other_fields:
value = self.data[f'{field}_{pk}']
if value == '':
raise forms.ValidationError(f'Add a {field} to crewmember {pk}', code=f'{field}_mismatch')
try:
item = models.EventChecklistCrew.objects.get(pk=pk)
except models.EventChecklistCrew.DoesNotExist:
item = models.EventChecklistCrew()
item.crewmember = models.Profile.objects.get(pk=self.data['crewmember_' + str(pk)])
item.start = self.parsedatetime(self.data['start_' + str(pk)])
item.role = self.data['role_' + str(pk)]
item.end = self.parsedatetime(self.data['end_' + str(pk)])
item.full_clean('checklist')
# item does not have a database pk yet as it isn't saved
self.items['c' + str(pk)] = item
return super(EventChecklistForm, self).clean()
def save(self, commit=True):
checklist = super(EventChecklistForm, self).save(commit=False)
if (commit):
# Remove all existing, to be recreated from the form
checklist.vehicles.all().delete()
checklist.crew.all().delete()
checklist.save()
for key in self.items:
item = self.items[key]
reversion.add_to_revision(item)
# finish and save new database items
item.checklist = checklist
item.full_clean()
item.save()
self.items.clear()
return checklist
class Meta:
model = models.EventChecklist
fields = "__all__"
exclude = ["reviewed_at", "reviewed_by"]
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__"
fields = '__all__'
exclude = ['reviewed_at', 'reviewed_by']

View File

@@ -7,13 +7,13 @@ from training import models as tmodels
class Command(BaseCommand):
help = "Deletes testing sample data"
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")
raise CommandError('You cannot run this command in production')
self.delete_objects(models.AssetCategory)
self.delete_objects(models.AssetStatus)

View File

@@ -1,13 +1,15 @@
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"
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")
call_command('generateSampleUserData')
call_command('generateSampleRIGSData')
call_command('generateSampleAssetsData')
call_command('generateSampleTrainingData')

View File

@@ -1,6 +1,7 @@
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
@@ -10,7 +11,7 @@ from RIGS import models
class Command(BaseCommand):
help = "Adds sample data to use for testing"
help = 'Adds sample data to use for testing'
can_import_settings = True
people = []
@@ -24,14 +25,13 @@ class Command(BaseCommand):
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
raise CommandError("You cannot run this command in production")
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
'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")
models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
self.setup_people()
self.setup_organisations()
self.setup_venues()
@@ -39,72 +39,18 @@ class Command(BaseCommand):
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
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()))
@@ -126,131 +72,32 @@ class Command(BaseCommand):
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
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()))
@@ -266,9 +113,7 @@ class Command(BaseCommand):
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"
)
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"
@@ -277,76 +122,18 @@ class Command(BaseCommand):
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
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))
@@ -371,81 +158,32 @@ class Command(BaseCommand):
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!",
]
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},
]
{'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
@@ -484,10 +222,9 @@ class Command(BaseCommand):
# Could have any status, equally weighted
new_event.status = random.choice(
[models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED]
)
[models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED])
new_event.dry_hire = random.randint(0, 7) == 0 # 1 in 7 are dry hire
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)
@@ -516,49 +253,40 @@ class Command(BaseCommand):
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(),
method=random.choice(models.Payment.METHODS)[0],
)
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)),
parking_and_access=bool(random.getrandbits(1)),
outside=bool(random.getrandbits(1)),
)
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),
)
models.EventChecklist.objects.create(event=new_event, power_mic=random.choice(self.profiles),
safe_parking=bool(random.getrandbits(1)),
safe_packing=bool(random.getrandbits(1)),
exits=bool(random.getrandbits(1)),
trip_hazard=bool(random.getrandbits(1)),
warning_signs=bool(random.getrandbits(1)),
ear_plugs=bool(random.getrandbits(1)),
hs_location="Locked away safely",
extinguishers_location="Somewhere, I forgot",
earthing=bool(random.getrandbits(1)),
pat=bool(random.getrandbits(1)),
date=timezone.now(), venue=random.choice(self.venues))

View File

@@ -1,52 +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
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 sys cron 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,
allow_loading_external_files=True,
).transform()
msg.attach_alternative(html, "text/html")
msg.send()

View File

@@ -31,3 +31,4 @@ class Migration(migrations.Migration):
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.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.25 on 2024-11-20 20:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0051_alter_payment_method'),
]
operations = [
migrations.AddField(
model_name='event',
name='parking_and_access',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 3.2.25 on 2024-11-20 21:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0052_event_parking_and_access'),
]
operations = [
migrations.AddField(
model_name='riskassessment',
name='parking_and_access',
field=models.BooleanField(default=False, help_text='Are there additional requirements for parking and access to the venue? (i.e. campus parking permits, event access wristbands)'),
preserve_default=False,
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ import urllib.parse
import urllib.request
from io import BytesIO
from pypdf import PdfReader, PdfWriter
from PyPDF2 import PdfFileReader, PdfFileMerger
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.cache import cache
@@ -24,18 +24,18 @@ 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,
'object': instance.event,
'receipt': True,
'current_user': False,
}
template = get_template("event_print.xml")
merger = PdfWriter()
template = get_template('event_print.xml')
merger = PdfFileMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfReader(buffer))
merger.append(PdfFileReader(buffer))
buffer.close()
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
@@ -46,36 +46,34 @@ def send_eventauthorisation_success_email(instance):
# Produce email content
context = {
"object": instance,
'object': instance,
}
if instance.event.person is not None and instance.email == instance.event.person.email:
context["to_name"] = instance.event.person.name
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
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),
get_template("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,
allow_loading_external_files=True,
).transform()
client_email.attach_alternative(html, "text/html")
css = finders.find('css/email.css')
html = Premailer(get_template("eventauthorisation_client_success.html").render(context),
external_styles=css).transform()
client_email.attach_alternative(html, 'text/html')
escapedEventName = re.sub(r"[^a-zA-Z0-9 \n\.]", "", instance.event.name)
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"
)
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
@@ -83,7 +81,9 @@ def send_eventauthorisation_success_email(instance):
mic_email_address = settings.AUTHORISATION_NOTIFICATION_ADDRESS
mic_email = EmailMessage(
subject, get_template("email/eventauthorisation_mic_success.txt").render(context), to=[mic_email_address]
subject,
get_template("eventauthorisation_mic_success.txt").render(context),
to=[mic_email_address]
)
# Now we have both emails successfully generated, send them out
@@ -109,26 +109,22 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
# 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")
+ f"?is_approved__exact=0&date_joined__date={timezone.now().date()}",
"number_of_users": models.Profile.users_awaiting_approval_count(),
"to_name": admin.first_name,
'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),
get_template("admin_awaiting_approval.txt").render(context),
to=[admin.email],
reply_to=[user.email],
)
css = finders.find("css/email.css")
html = Premailer(
get_template("email/admin_awaiting_approval.html").render(context),
external_styles=css,
allow_loading_external_files=True,
).transform()
email.attach_alternative(html, "text/html")
css = finders.find('css/email.css')
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
external_styles=css).transform()
email.attach_alternative(html, 'text/html')
email.send()
# Update last sent

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 66 KiB

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,6 @@
<initialize>
<color id="LightGray" RGB="#D3D3D3"/>
<color id="DarkGray" RGB="#707070"/>
<color id="Brand" RGB="#3853a4"/>
</initialize>
<paraStyle name="style.para" fontName="OpenSans" />
@@ -22,17 +21,12 @@
<paraStyle name="center" alignment="center"/>
<paraStyle name="page-head" alignment="center" fontName="OpenSans-Bold" fontSize="16" leading="18" spaceAfter="0"/>
{% block extrastyles %}
{% endblock %}
<paraStyle name="style.event_description" fontName="OpenSans" textColor="DarkGray" />
<paraStyle name="style.item_description" fontName="OpenSans" textColor="DarkGray" leftIndent="10" />
<paraStyle name="style.specific_description" fontName="OpenSans" textColor="DarkGray" fontSize="10" />
<paraStyle name="style.times" fontName="OpenSans" fontSize="10" />
<paraStyle name="style.head_titles" fontName="OpenSans-Bold" fontSize="10" />
<paraStyle name="style.head_numbers" fontName="OpenSans" fontSize="10" />
<paraStyle name="style.emheader" fontName="OpenSans" textColor="White" fontSize="12" backColor="Brand" leading="20" borderPadding="4"/>
<paraStyle name="style.breakbefore" parent="emheader" pageBreakBefore="1"/>
<blockTableStyle id="eventSpecifics">
<blockValign value="top"/>
@@ -87,12 +81,10 @@
<listStyle name="ul"
start="bulletchar"
leftIndent="0"
bulletDedent="10"
bulletFontSize="10"/>
</stylesheet>
<template title="{{filename}}"> {# Note: page is 595x842 points (1 point=1/72in) #}
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
<pageTemplate id="Headed" >
<pageGraphics>
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
@@ -142,7 +134,6 @@
<nextFrame/>
{% block content %}
{% endblock %}
<namedString id="lastPage"><pageNumber/></namedString>
</story>
</document>

View File

@@ -6,7 +6,6 @@
{% load total_invoices_todo from filters %}
{% block titleheader %}
<a class="navbar-brand" style="margin-left: auto; margin-right: auto;" href="/">RIGS</a>
{% endblock %}
@@ -14,7 +13,7 @@
{% if user.is_authenticated %}
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownRigboard" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownRigboard" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Rigboard
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownRigboard">
@@ -35,22 +34,30 @@
</div>
</li>
{% if perms.RIGS.view_riskassessment %}
<li class="nav-item"><a class="nav-link" href="{% url 'hs_list' %}">H&S</a></li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownHS" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
H&S
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownHS">
<a class="dropdown-item" href="{% url 'hs_list' %}"><span class="fas fa-eye"></span> Overview</a>
<a class="dropdown-item" href="{% url 'ra_list' %}"><span class="fas fa-file-medical"></span> Risk Assessments</a>
<a class="dropdown-item" href="{% url 'ec_list' %}"><span class="fas fa-tasks"></span> Event Checklists</a>
</div>
</li>
{% endif %}
{% if perms.RIGS.view_invoice %}
<li class="nav-item dropdown">
{% total_invoices_todo as todo %}
{% invoices_waiting as waiting %}
{% invoices_outstanding as outstanding %}
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownInvoices" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Invoices <span class="badge {% if todo == 0 %}text-bg-success{% else %}text-bg-danger{% endif %} rounded-pill">{{ todo }}</span>
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownInvoices" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Invoices <span class="badge {% if todo == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ todo }}</span>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownInvoices">
<a class="dropdown-item" href="{% url 'invoice_dashboard' %}"><span class="fas fa-chart-line"></span> Dashboard</a>
{% if perms.RIGS.add_invoice %}
<a class="dropdown-item text-nowrap" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting <span class="badge {% if waiting == 0 %}text-bg-success{% else %}text-bg-danger{% endif %} rounded-pill">{{ waiting }}</span></a>
<a class="dropdown-item text-nowrap" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting <span class="badge {% if waiting == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ waiting }}</span></a>
{% endif %}
<a class="dropdown-item" href="{% url 'invoice_list' %}"><span class="fas fa-pound-sign text-warning"></span> Outstanding <span class="badge {% if outstanding == 0 %}text-bg-success{% else %}text-bg-danger{% endif %} rounded-pill">{{ outstanding }}</span></a>
<a class="dropdown-item" href="{% url 'invoice_list' %}"><span class="fas fa-pound-sign text-warning"></span> Outstanding <span class="badge {% if outstanding == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ outstanding }}</span></a>
<a class="dropdown-item" href="{% url 'invoice_archive' %}"><span class="fas fa-book"></span> Archive</a>
</div>
</li>
@@ -74,4 +81,11 @@
{% block js %}
{{ block.super }}
<script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/popover.js' %}"></script>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
})
</script>
{% endblock %}

View File

@@ -26,8 +26,7 @@
var calendarEl = document.getElementById('calendar');
calendar = new FullCalendar.Calendar(calendarEl, {
firstDay: 1,
themeSystem: 'standard',
themeSystem: 'bootstrap',
aspectRatio: 1.5,
eventTimeFormat: {
'hour': '2-digit',
@@ -38,13 +37,14 @@
editable: false,
dayMaxEventRows: true, // allow "more" link when too many events
events: function(fetchInfo, successCallback, failureCallback) {
var params = new URLSearchParams({
start: moment(fetchInfo.startStr).format("YYYY-MM-DD[T]HH:mm:ss"),
end: moment(fetchInfo.endStr).format("YYYY-MM-DD[T]HH:mm:ss")
});
fetch('/api/event?' + params, {credentials: 'same-origin'})
.then(function (response) { return response.json(); })
.then(function (doc) {
$.ajax({
url: '/api/event',
dataType: 'json',
data: {
start: moment(fetchInfo.startStr).format("YYYY-MM-DD[T]HH:mm:ss"),
end: moment(fetchInfo.endStr).format("YYYY-MM-DD[T]HH:mm:ss")
},
success: function(doc) {
var events = [];
colours = {
'Provisional': '#FFE89B',
@@ -53,8 +53,8 @@
'Cancelled': 'grey' ,
'non-rig': '#25AAE2'
};
doc.forEach(function (item) {
end = item.latest
$(doc).each(function() {
end = $(this).attr('latest')
allDay = false
if(end.indexOf("T") < 0){ //If latest does not contain a time
end = moment(end + " 23:59").format("YYYY-MM-DD[T]HH:mm:ss")
@@ -62,64 +62,84 @@
}
thisEvent = {
'start': item.earliest,
'start': $(this).attr('earliest'),
'end': end,
'className': 'modal-href',
'title': item.title,
'url': item.url,
'title': $(this).attr('title'),
'url': $(this).attr('url'),
'allDay': allDay
}
if(item.is_rig===true || item.status === "Cancelled"){
thisEvent['color'] = colours[item.status];
if($(this).attr('is_rig')===true || $(this).attr('status') === "Cancelled"){
thisEvent['color'] = colours[$(this).attr('status')];
}else{
thisEvent['color'] = colours['non-rig'];
}
events.push(thisEvent);
});
successCallback(events);
})
.catch(failureCallback);
}
});
},
datesSet: function(info) {
var view = info.view;
// Set the title of the view
document.getElementById('calendar-header').textContent = view.title;
$('#calendar-header').text(view.title);
// Enable/Disable "Today" button as required
//Today is within the current view
document.getElementById('today-button').disabled = moment().isBetween(view.currentStart, view.currentEnd);
let $today = $('#today-button');
if(moment().isBetween(view.currentStart, view.currentEnd)){
//Today is within the current view
$today.prop('disabled', true);
}else{
$today.prop('disabled', false);
}
// Set active view select button
var buttons = {
dayGridMonth: document.getElementById('month-button'),
timeGridWeek: document.getElementById('week-button'),
timeGridDay: document.getElementById('day-button')
};
Object.keys(buttons).forEach(function (type) {
buttons[type].classList.toggle('active', type === view.type);
});
let $month = $('#month-button');
let $week = $('#week-button');
let $day = $('#day-button');
switch(view.type){
case 'dayGridMonth':
$month.addClass('active');
$week.removeClass('active');
$day.removeClass('active');
break;
case 'timeGridWeek':
$month.removeClass('active');
$week.addClass('active');
$day.removeClass('active');
break;
case 'timeGridDay':
$month.removeClass('active');
$week.removeClass('active');
$day.addClass('active');
break;
}
history.replaceState(null,null,"{% url 'web_calendar' %}"+viewToUrl[view.type]+'/'+moment(view.currentStart).format('YYYY-MM-DD')+'/');
}
});
calendar.render();
});
ready(function() {
$(document).ready(function() {
// set some button listeners
var click = function (id, fn) { document.getElementById(id).addEventListener('click', fn); };
click('next-button', function(){ calendar.next(); });
click('prev-button', function(){ calendar.prev(); });
click('today-button', function(){ calendar.today(); });
click('month-button', function(){ calendar.changeView('dayGridMonth'); });
click('week-button', function(){ calendar.changeView('timeGridWeek'); });
click('day-button', function(){ calendar.changeView('timeGridDay'); });
var dateInput = document.getElementById('go-to-date-input');
var dateButton = document.getElementById('go-to-date-button');
dateInput.addEventListener('change', function(){
dateButton.disabled = !moment(dateInput.value).isValid();
$('#next-button').click(function(){ calendar.next(); });
$('#prev-button').click(function(){ calendar.prev(); });
$('#today-button').click(function(){ calendar.today(); });
$('#month-button').click(function(){ calendar.changeView('dayGridMonth'); });
$('#week-button').click(function(){ calendar.changeView('timeGridWeek'); });
$('#day-button').click(function(){ calendar.changeView('timeGridDay'); });
$('#go-to-date-input').change(function(){
if(moment($('#go-to-date-input').val()).isValid()){
$('#go-to-date-button').prop('disabled', false);
} else{
$('#go-to-date-button').prop('disabled', true);
}
});
dateButton.addEventListener('click', function(){
day = moment(dateInput.value);
$('#go-to-date-button').click(function(){
day = moment($('#go-to-date-input').val());
if(day.isValid()){
calendar.gotoDate(day.format("YYYY-MM-DD"));
} else{
@@ -143,23 +163,25 @@
{% block content %}
<div class="row">
<div class="col-sm-12 d-flex flex-wrap justify-content-between align-items-center">
<div>
<div class="col-sm-12">
<div class="pull-left">
<span id="calendar-header" class="h2"></span>
</div>
<div class="d-flex align-items-center flex-wrap gap-2 btn-page my-3">
<div class="input-group w-auto">
<div class="form-inline float-right btn-page my-3">
<div class="input-group mx-2">
<input type="date" class="form-control" id="go-to-date-input" placeholder="Go to date...">
<button class="btn btn-success" id="go-to-date-button" type="button" disabled>Go!</button>
<span class="input-group-append">
<button class="btn btn-success" id="go-to-date-button" type="button" disabled>Go!</button>
</span>
</div>
<div class="btn-group">
<div class="btn-group mx-2">
<button type="button" class="btn btn-primary" id="today-button">Today</button>
</div>
<div class="btn-group">
<div class="btn-group mx-2">
<button type="button" class="btn btn-secondary" id="prev-button"><span class="fas fa-chevron-left"></span></button>
<button type="button" class="btn btn-secondary" id="next-button"><span class="fas fa-chevron-right"></span></button>
</div>
<div class="btn-group">
<div class="btn-group ml-2">
<button type="button" class="btn btn-light" id="month-button">Month</button>
<button type="button" class="btn btn-light" id="week-button">Week</button>
<button type="button" class="btn btn-light" id="day-button">Day</button>

View File

@@ -1,16 +0,0 @@
{% extends 'base_client_email.html' %}
{% block content %}
<p>Hi {{event.mic.get_full_name|default_if_none:"Productions Manager"}},</p>
{% if event.mic %}
<p>Just to let you know your event {{event.display_id}} <em>requires<em> a pre-event risk assessment completing prior to the event. Please do so as soon as possible.</p>
{% else %}
<p>This is a reminder that event {{event.display_id}} requires a MIC assigning and a risk assessment completing.</p>
{% endif %}
<p>Fill it out here:</p>
<a href="{{url}}" class="btn btn-info"><span class="fas fa-paperclip"></span> Create Risk Assessment</a>
<p>TEC PA &amp; Lighting</p>
{% endblock %}

View File

@@ -1,9 +0,0 @@
Hi {{event.mic.get_full_name|default_if_none:"Productions Manager"}},
{% if event.mic %}
Just to let you know your event {{event.display_id}} requires a risk assessment completing prior to the event. Please do so as soon as possible.
{% else %}
This is a reminder that event {{event.display_id}} requires a MIC assigning and a risk assessment completing.
{% endif %}
The TEC Rig Information Gathering System

View File

@@ -11,26 +11,32 @@
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/selects.js' %}" async></script>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12 py-2">
<form class="d-flex align-items-center flex-wrap gap-2" method="GET">
<form class="form-inline" method="GET">
<div class="input-group mx-2">
<span class="input-group-text">Start</span>
<div class="input-group-prepend">
<span class="input-group-text">Start</span>
</div>
<input type="date" name="start" id="start" value="{{ start|default_if_none:'' }}" placeholder="Start" class="form-control" />
</div>
<div class="input-group mx-2">
<span class="input-group-text">End</span>
<div class="input-group-prepend">
<span class="input-group-text">End</span>
</div>
<input type="date" name="end" id="end" value="{{ end|default_if_none:'' }}" placeholder="End" class="form-control" />
</div>
<div class="input-group mx-2">
<span class="input-group-text">Keyword</span>
<div class="input-group-prepend">
<span class="input-group-text">Keyword</span>
</div>
<input type="search" name="q" placeholder="Keyword" value="{{ request.GET.q }}" class="form-control" />
</div>
<select class="ts-select pe-3" multiple data-actions-box="true" data-none-selected-text="Status" data-actions-box="true" id="status" name="status">
<select class="selectpicker pr-3" multiple data-actions-box="true" data-none-selected-text="Status" data-actions-box="true" id="status" name="status">
{% for status in statuses %}
<option value="{{status.0}}" {% if status.0|safe in request.GET|get_list:'status' %}selected=""{% endif %}>{{status.1}}</option>
{% endfor %}
@@ -40,7 +46,7 @@
</div>
</div>
<div class="row">
<div class="col-sm-12" style="container-type: inline-size;">
<div class="col-sm-12">
{% with object_list as events %}
{% include 'partials/event_table.html' %}
{% endwith %}

View File

@@ -1,37 +1,20 @@
{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load markdown_tags %}
{% load static %}
{% block js %}
{{ block.super }}
<script>
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.keyCode == 80) {
window.open("{% url 'event_print' object.pk %}", '_blank');
e.preventDefault();
}
});
</script>
{% endblock %}
{% block content %}
<div class="row my-3 py-3">
{% if not is_ajax %}
{% if not request.is_ajax %}
{% if perms.RIGS.view_event %}
<div class="col-sm-12 text-end">
<div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %}
</div>
{% endif %}
{% endif %}
{% if object.is_rig and perms.RIGS.view_event %}
{# only need contact details for a rig #}
<div class="col-md-6 mb-3">
<div class="col-md-6">
{% include 'partials/contact_details.html' %}
{% if object.parking_and_access or object.riskassessment.parking_and_access %}
{% include 'partials/parking_and_access.html' %}
{% endif %}
</div>
{% endif %}
<div class="col-md-6">
@@ -49,8 +32,8 @@
</div>
{% endif %}
{% endif %}
{% if not is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-end">
{% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %}
</div>
{% endif %}
@@ -69,24 +52,21 @@
</div>
</div>
</div>
{% include 'partials/crew_list.html' %}
{% if not is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-end">
{% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
{% include 'partials/event_detail_buttons.html' %}
</div>
{% endif %}
{% endif %}
{% if not is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-end">
{% if not request.is_ajax and perms.RIGS.view_event %}
<div class="col-sm-12 text-right">
{% include 'partials/last_edited.html' with target="event_history" %}
</div>
{% endif %}
</div>
{% endblock %}
{% if is_ajax %}
{% if request.is_ajax %}
{% block footer %}
{% if perms.RIGS.view_event %}
{% include 'partials/last_edited.html' with target="event_history" %}

View File

@@ -7,7 +7,7 @@
{% endblock %}
{% block content %}
<span class="float-end">
<span class="float-right">
{% if object.mic %}
<div class="text-center">
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo rounded"/>
@@ -39,8 +39,8 @@
</small>
</h3>
{% include 'partials/event_status.html' %}
<div class="row ms-2">
<div class="col-6 pe-2">
<div class="row ml-2">
<div class="col-xs-6 pr-2">
<p>
{% if object.is_rig %}
<strong>Client:</strong> {{ object.person.name }}
@@ -61,7 +61,7 @@
{% endif %}
</p>
</div>
<div class="col-6 px-2">
<div class="col-xs-6 px-2">
{% if object.meet_at %}
<p>
<strong>Crew meet:</strong>

View File

@@ -19,64 +19,64 @@
{% block js %}
{{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
const matches = window.matchMedia("(prefers-reduced-motion: reduce)").matches || window.matchMedia("(update: slow)").matches;
ready(function () {
$(document).ready(function () {
dur = matches ? 0 : 500;
var all = function (selector) { return document.querySelectorAll(selector); };
{% if object.pk %}
// Editing
{% if not object.is_rig %}
all('.form-is_rig').forEach(function (el) { el.style.display = 'none'; });
$('.form-is_rig').hide();
{% endif %}
//Creation
{% else %}
// If there were errors, apply the previous Rig/not-Rig selection
{% if form.errors %}
all('.form-hws').forEach(function (el) { slideDown(el, 0); });
if (document.getElementById('{{form.is_rig.auto_id}}').getAttribute('checked') !== 'checked') {
all('.form-is_rig').forEach(function (el) { el.style.display = 'none'; });
$('.form-hws').show();
if ($('#{{form.is_rig.auto_id}}').attr('checked') !== 'checked') {
$('.form-is_rig').hide();
}
{% else %}
//Initial hide
all('.form-hws').forEach(function (el) { slideUp(el, dur); });
$('.form-hws').slideUp(dur);
{% endif %}
//Button handling
all('#is_rig-selector button').forEach(function (button) {
button.addEventListener('click', function () {
var wasHidden = isHidden(document.querySelector('.form-non_rig'));
all('.form-non_rig').forEach(function (el) { slideDown(el, dur); }); //Non rig stuff also needed for rig, so always slide down
if (this.dataset.is_rig === '1') {
document.getElementById('{{form.is_rig.auto_id}}').checked = true;
all('.form-is_rig').forEach(function (el) {
if (wasHidden) {
el.style.display = '';
} else {
slideDown(el, dur);
}
});
all('.form-hws, .form-hws .form-is_rig').forEach(function (el) { el.style.overflow = 'visible'; });
$('#is_rig-selector button').on('click', function () {
$('.form-non_rig').slideDown(dur); //Non rig stuff also needed for rig, so always slide down
if ($(this).data('is_rig') === 1) {
$('#{{form.is_rig.auto_id}}').prop('checked', true);
if ($('.form-non_rig').is(':hidden')) {
$('.form-is_rig').show();
} else {
document.getElementById('{{form.is_rig.auto_id}}').checked = false;
all('.form-is_rig').forEach(function (el) { slideUp(el, dur); });
$('.form-is_rig').slideDown(dur);
}
});
$('.form-hws, .form-hws .form-is_rig').css('overflow', 'visible');
} else {
$('#{{form.is_rig.auto_id}}').prop('checked', false);
$('.form-is_rig').slideUp(dur);
}
});
{% endif %}
});
ready(function () {
$(document).ready(function () {
setupMDE('#id_description');
setupMDE('#id_notes');
setupMDE('#item_description');
document.getElementById('itemModal').addEventListener('shown.bs.modal', function () {
var description = document.getElementById('item_description');
description.mde_editor.value(description.value);
$('#itemModal').on('shown.bs.modal', function (e) {
$('#item_description').data('mde_editor').value(
$('#item_description').val()
);
});
setupItemTable(document.getElementById("{{ form.items_json.id_for_label }}").value);
setupItemTable($("#{{ form.items_json.id_for_label }}").val());
});
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{% endblock %}
@@ -98,11 +98,11 @@
<div class="col-sm-12">
<div class="card text-center" id="is_rig-selector">
<div class="card-body">
<span data-bs-toggle="tooltip"
<span data-toggle="tooltip"
title="Anything that involves TEC kit, crew, or otherwise us providing a service to anyone.">
<button type="button" class="btn btn-primary w-25" data-is_rig="1">Rig</button>
</span>
<span data-bs-toggle="tooltip"
<span data-toggle="tooltip"
title="Things that aren't service-based, like training, meetings and site visits.">
<button type="button" class="btn btn-info w-25" data-is_rig="0">Non-Rig</button>
</span>
@@ -116,13 +116,13 @@
<div class="card form-hws form-is_rig {% if object.pk and not object.is_rig %}hidden{% endif %} mb-3" {% if not object.pk and not form.errors %}style="display: none;"{% endif%}>
<div class="card-header">Contact Details</div>
<div class="card-body">
<div class="mb-3" data-bs-toggle="tooltip" title="The main contact for the event, can be left blank if purely an organisation">
<div class="form-group" data-toggle="tooltip" title="The main contact for the event, can be left blank if purely an organisation">
<label for="{{ form.person.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.person.label }}</label>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="ts-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}">
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='person' %}">
{% if person %}
<option value="{{form.person.value}}" selected="selected" data-update_url="{% url 'person_update' form.person.value %}">{{ person }}</option>
{% endif %}
@@ -131,10 +131,10 @@
<div class="col-sm-3 col-md-5 col-lg-4 align-right">
<div class="btn-group">
<a href="{% url 'person_create' %}" class="btn btn-success modal-href"
data-select-target="#{{ form.person.id_for_label }}">
data-target="#{{ form.person.id_for_label }}">
<span class="fas fa-plus"></span>
</a>
<a {% if form.person.value %}href="{% url 'person_update' form.person.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.person.id_for_label }}-update" data-select-target="#{{ form.person.id_for_label }}">
<a {% if form.person.value %}href="{% url 'person_update' form.person.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.person.id_for_label }}-update" data-target="#{{ form.person.id_for_label }}">
<span class="fas fa-user-edit"></span>
</a>
</div>
@@ -142,14 +142,14 @@
</div>
</div>
</div>
<div class="mb-3" data-bs-toggle="tooltip" title="The client organisation, leave blank if client is an individual">
<div class="form-group" data-toggle="tooltip" title="The client organisation, leave blank if client is an individual">
<label for="{{ form.organisation.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.organisation.label }}</label>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.organisation.id_for_label }}" name="{{ form.organisation.name }}" class="ts-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}" >
<select id="{{ form.organisation.id_for_label }}" name="{{ form.organisation.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='organisation' %}" >
{% if organisation %}
<option value="{{form.organisation.value}}" selected="selected" data-update_url="{% url 'organisation_update' form.organisation.value %}">{{ organisation }}</option>
{% endif %}
@@ -158,10 +158,10 @@
<div class="col-sm-3 col-md-5 col-lg-4 align-right">
<div class="btn-group">
<a href="{% url 'organisation_create' %}" class="btn btn-success modal-href"
data-select-target="#{{ form.organisation.id_for_label }}">
data-target="#{{ form.organisation.id_for_label }}">
<span class="fas fa-plus"></span>
</a>
<a {% if form.organisation.value %}href="{% url 'organisation_update' form.organisation.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.organisation.id_for_label }}-update" data-select-target="#{{ form.organisation.id_for_label }}">
<a {% if form.organisation.value %}href="{% url 'organisation_update' form.organisation.value %}"{% endif %} class="btn btn-warning modal-href" id="{{ form.organisation.id_for_label }}-update" data-target="#{{ form.organisation.id_for_label }}">
<span class="fas fa-edit"></span>
</a>
</div>
@@ -174,7 +174,7 @@
<div class="card form-hws form-non_rig mb-3" {% if not object.pk and not form.errors %}style="display: none;"{% endif%}>
<div class="card-header">Event Description</div>
<div class="card-body">
<div class="mb-3" data-bs-toggle="tooltip" title="A short description of the event, shown on rigboard and on paperwork">
<div class="form-group" data-toggle="tooltip" title="A short description of the event, shown on rigboard and on paperwork">
<label for="{{ form.description.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.description.label }}</label>
@@ -192,7 +192,7 @@
<div class="card-header">Event Details</div>
<div class="card-body">
<div id="form-hws">
<div class="mb-3" data-bs-toggle="tooltip" title="Name of the event, displays on rigboard and on paperwork">
<div class="form-group" data-toggle="tooltip" title="Name of the event, displays on rigboard and on paperwork">
<label for="{{ form.name.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.name.label }}</label>
@@ -200,26 +200,26 @@
{% render_field form.name class+="form-control" %}
</div>
</div>
<div class="mb-3" data-bs-toggle="tooltip" title="The venue for the rig, leave blank if unknown (e.g. for a dry hire)">
<div class="form-group" data-toggle="tooltip" title="The venue for the rig, leave blank if unknown (e.g. for a dry hire)">
<label for="{{ form.venue.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.venue.label }}</label>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-8">
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="ts-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %}
<option value="{{venue.id}}" selected="selected" data-update_url="{% url 'venue_update' venue.id %}">{{ venue }}</option>
<option value="{{form.venue.value}}" selected="selected" data-update_url="{% url 'venue_update' form.venue.value %}">{{ venue }}</option>
{% endif %}
</select>
</div>
<div class="col-sm-3 col-md-5 col-lg-4 align-right">
<div class="btn-group">
<a href="{% url 'venue_create' %}" class="btn btn-success modal-href"
data-select-target="#{{ form.venue.id_for_label }}">
data-target="#{{ form.venue.id_for_label }}">
<span class="fas fa-plus"></span>
</a>
<a href="{% if object.venue %}{% url 'venue_update' object.venue.pk %}{% endif %}" class="btn btn-warning modal-href" id="{{ form.venue.id_for_label }}-update" data-select-target="#{{ form.venue.id_for_label }}">
<a href="{% if object.venue %}{% url 'venue_update' object.venue.pk %}{% endif %}" class="btn btn-warning modal-href" id="{{ form.venue.id_for_label }}-update" data-target="#{{ form.venue.id_for_label }}">
<span class="fas fa-edit"></span>
</a>
</div>
@@ -227,31 +227,31 @@
</div>
</div>
</div>
<div class="mb-3">
<div class="form-group">
<label for="{{ form.start_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.start_date.label }}</label>
<div class="col-sm-10">
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12 col-md-7" data-bs-toggle="tooltip" title="Start date for event, required">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="Start date for event, required">
{% render_field form.start_date class+="form-control" %}
</div>
<div class="col-sm-12 col-md-5" data-bs-toggle="tooltip" title="Start time of event, can be left blank">
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="Start time of event, can be left blank">
{% render_field form.start_time class+="form-control" step="60" %}
</div>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-group">
<label for="{{ form.end_date.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.end_date.label }}</label>
<div class="col-sm-10">
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12 col-md-7" data-bs-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
<div class="col-sm-12 col-md-7" data-toggle="tooltip" title="End date of event, leave blank if unknown or same as start date">
{% render_field form.end_date class+="form-control" %}
</div>
<div class="col-sm-12 col-md-5" data-bs-toggle="tooltip" title="End time of event, leave blank if unknown">
<div class="col-sm-12 col-md-5" data-toggle="tooltip" title="End time of event, leave blank if unknown">
{% render_field form.end_time class+="form-control" step="60" %}
</div>
</div>
@@ -260,14 +260,14 @@
{# Rig only information #}
<div class="form-is_rig {% if object.pk and not object.is_rig %}hidden{% endif %}">
<div class="mb-3" data-bs-toggle="tooltip" title="The date/time at which TEC have access to the venue">
<div class="form-group" data-toggle="tooltip" title="The date/time at which TEC have access to the venue">
<label for="{{ form.access_at.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.access_at.label }}</label>
<div class="col-sm-8">
{% render_field form.access_at class+="form-control" step="60" %}
</div>
</div>
<div class="mb-3" data-bs-toggle="tooltip" title="The date/time at which crew should meet for this event">
<div class="form-group" data-toggle="tooltip" title="The date/time at which crew should meet for this event">
<label for="{{ form.meet_at.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.meet_at.label }}</label>
@@ -275,24 +275,17 @@
{% render_field form.meet_at class+="form-control" step="60" %}
</div>
</div>
<div class="mb-3">
<div class="form-group">
<div class="col-sm-offset-4 col-sm-8">
<label data-bs-toggle="tooltip" title="Mark this event as a dry-hire, so it needs to be checked in at the end">
<label data-toggle="tooltip" title="Mark this event as a dry-hire, so it needs to be checked in at the end">
{{ form.dry_hire.label }} {% render_field form.dry_hire %}
</div>
</div>
<div class="mb-3">
<div class="col-sm-offset-4 col-sm-8">
<label data-bs-toggle="tooltip" title="Do we need to secure campus parking permits, wristbands for backstage access or other non-standard requirements?">
{{ form.parking_and_access.label }} {% render_field form.parking_and_access %}
</label>
</div>
</div>
</div>
</div>
{# Status is needed on all events types and it looks good here in the form #}
<div class="mb-3" data-bs-toggle="tooltip" title="The current status of the event. Only mark as booked once paperwork is received">
<div class="form-group" data-toggle="tooltip" title="The current status of the event. Only mark as booked once paperwork is received">
<label for="{{ form.status.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.status.label }}</label>
@@ -302,12 +295,12 @@
</div>
<div class="form-is_rig {% if object.pk and not object.is_rig %}hidden{% endif %}">
<div class="mb-3" data-bs-toggle="tooltip" title="The Member in Charge of this event">
<div class="form-group" data-toggle="tooltip" title="The Member in Charge of this event">
<label for="{{ form.mic.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.mic.label }}</label>
<div class="col-sm-8">
<select id="{{ form.mic.id_for_label }}" name="{{ form.mic.name }}" class="px-0 ts-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
<select id="{{ form.mic.id_for_label }}" name="{{ form.mic.name }}" class="px-0 selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if mic %}
<option value="{{form.mic.value}}" selected="selected" >{{ mic.name }}</option>
{% endif %}
@@ -316,12 +309,12 @@
</div>
{% if object.dry_hire %}
<div class="mb-3" data-bs-toggle="tooltip" title="The person who checked-in this dry hire">
<div class="form-group" data-toggle="tooltip" title="The person who checked-in this dry hire">
<label for="{{ form.checked_in_by.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.checked_in_by.label }}</label>
<div class="col-sm-8">
<select id="{{ form.checked_in_by.id_for_label }}" name="{{ form.checked_in_by.name }}" class="px-0 ts-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
<select id="{{ form.checked_in_by.id_for_label }}" name="{{ form.checked_in_by.name }}" class="px-0 selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if checked_in_by %}
<option value="{{form.checked_in_by.value}}" selected="selected" >{{ checked_in_by.name }}</option>
{% endif %}
@@ -330,7 +323,7 @@
</div>
{% endif %}
<div class="mb-3" data-bs-toggle="tooltip" title="The student ID of the client who collected the dry-hire">
<div class="form-group" data-toggle="tooltip" title="The student ID of the client who collected the dry-hire">
<label for="{{ form.collector.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.collector.label }}</label>
@@ -339,28 +332,14 @@
</div>
</div>
<div class="mb-3" data-bs-toggle="tooltip" title="The purchase order number (for external clients)">
<div class="form-group" data-toggle="tooltip" title="The purchase order number (for external clients)">
<label for="{{ form.purchase_order.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.purchase_order.label }}</label>
class="col-sm-4 col-fitem_tableorm-label">{{ form.purchase_order.label }}</label>
<div class="col-sm-8">
{% render_field form.purchase_order class+="form-control" %}
</div>
</div>
<div class="mb-3" data-bs-toggle="tooltip" title="The thread for this event on the TEC Forum">
<label for="{{ form.forum_url.id_for_label }}"
class="col-sm-4 col-form-label">Forum Thread</label>
<div class="col-sm-12">
<p class="small mb-0">Paste URL</p>
{% render_field form.forum_url class+="form-control" %}
{% if object.pk %}
<p class="small mb-0">or</p>
<a href="{% url 'event_thread' object.pk %}" class="btn btn-primary" title="Create Forum Thread" target="_blank">
<span class="fas fa-plus"></span> <span class="d-none d-sm-inline">Create Forum Thread</span></a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
@@ -372,7 +351,7 @@
<div class="card card-default form-hws form-is_rig {% if object.pk and not object.is_rig %}hidden{% endif %}" {% if not object.pk and not form.errors %}style="display: none;"{% endif%}>
<div class="card-body">
<div class="col-sm-12">
<div class="mb-3" data-bs-toggle="tooltip" title="Notes on the event. This is only visible to keyholders, and is not displayed on the paperwork">
<div class="form-group" data-toggle="tooltip" title="Notes on the event. This is only visible to keyholders, and is not displayed on the paperwork">
<label for="{{ form.notes.id_for_label }}">{{ form.notes.label }}</label>
{% render_field form.notes class+="form-control md-enabled" %}
</div>
@@ -381,7 +360,7 @@
</div>
</div>
</div>
<div class="col-sm-12 text-end form-hws form-non_rig my-3" {% if not object.pk and not form.errors %}style="display: none;"{% endif%}>
<div class="col-sm-12 text-right form-hws form-non_rig my-3" {% if not object.pk and not form.errors %}style="display: none;"{% endif%}>
{% button 'submit' %}
</div>
</div>

View File

@@ -3,13 +3,15 @@
{% load static %}
{% block js %}
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
document.querySelectorAll('form').forEach(function (form) {
form.addEventListener('submit', function () {
var loading = document.getElementById('loading-modal');
if (loading) {
bootstrap.Modal.getOrCreateInstance(loading, {backdrop: 'static'}).show();
}
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
$('form').on('submit', function () {
$('#loading-modal').modal({
backdrop: 'static',
show: true
});
});
</script>

View File

@@ -23,7 +23,7 @@
</div>
<div class="col-sm-12 col-md-6">
<div class="col-sm-12 mb-3 row g-2" data-bs-toggle="tooltip"
<div class="col-sm-12 form-group form-row" data-toggle="tooltip"
title="Your name as the person authorising the event.">
<label for="{{ form.name.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.name.label }}</label>
@@ -33,7 +33,7 @@
</div>
</div>
<div class="col-sm-12 mb-3 row g-2" data-bs-toggle="tooltip"
<div class="col-sm-12 form-group form-row" data-toggle="tooltip"
title="Your Student ID or Staff username as the person authorising the event.">
<label for="{{ form.uni_id.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.uni_id.label }}</label>
@@ -44,7 +44,7 @@
</div>
<div class="col-sm-12 col-md-6">
<div class="col-sm-12 mb-3 row g-2" data-bs-toggle="tooltip"
<div class="col-sm-12 form-group form-row" data-toggle="tooltip"
title="The Students' Union account code you wish this event to be charged to.">
<label for="{{ form.account_code.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.account_code.label }}</label>
@@ -53,13 +53,15 @@
</div>
</div>
<div class="col-sm-12 mb-3 row g-2" data-bs-toggle="tooltip"
<div class="col-sm-12 form-group form-row" data-toggle="tooltip"
title="The full amount chargable for this event as displayed above, including VAT.">
<label for="{{ form.amount.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.amount.label }}</label>
<div class="col-sm-8">
<div class="input-group mb-3">
<span class="input-group-text">£</span>
<div class="input-group-prepend">
<span class="input-group-text">£</span>
</div>
{% render_field form.amount class+="form-control" %}
</div>
</div>
@@ -67,8 +69,8 @@
</div>
<div class="col-sm-12">
<div class="col-sm-12 col-md-8 mb-3">
<div class="col-sm-offset-4 col-sm-8 col-md-offset-3" data-bs-toggle="tooltip"
<div class="col-sm-12 col-md-8 form-group">
<div class="col-sm-offset-4 col-sm-8 col-md-offset-3" data-toggle="tooltip"
title="In order to book an event you must agree to the TEC Terms of Hire.">
<div class="checkbox">
<label>
@@ -79,9 +81,9 @@
</div>
</div>
<div class="text-end">
<div class="text-right">
<div class="btn-group">
<button class="btn btn-primary btn-lg" type="submit" {% if preview %}disabled="" data-bs-toggle="tooltip" title="This is only a preview!"{%endif%}>Authorise</button>
<button class="btn btn-primary btn-lg" type="submit" {% if preview %}disabled="" data-toggle="tooltip" title="This is only a preview!"{%endif%}>Authorise</button>
</div>
</div>
</div>

View File

@@ -1,10 +1,25 @@
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load button from filters %}
{% block title %}Request Authorisation{% endblock %}
{% block js %}
<script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/popover.js' %}"></script>
<script src="{% static 'js/clipboard.min.js' %}"></script>
<script>
var clipboard = new ClipboardJS('.btn');
clipboard.on('success', function(e) {
$(e.trigger).popover('show');
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
e.clearSelection();
});
</script>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12">
@@ -18,11 +33,11 @@
<dl class="dl-horizontal">
{% if object.person.email %}
<dt>Person Email</dt>
<dd><span id="person-email" class="pe-1">{{ object.person.email }}</span> {% button 'copy' id='#person-email' %}</dd>
<dd><span id="person-email">{{ object.person.email }}</span>{% button 'copy' id='#person-email' %}</dd>
{% endif %}
{% if object.organisation.email %}
<dt>Organisation Email</dt>
<dd><span id="org-email" class="pe-1">{{ object.organisation.email }}</span> {% button 'copy' id='#org-email' %}</dd>
<dd><span id="org-email">{{ object.organisation.email }}</span>{% button 'copy' id='#org-email' %}</dd>
{% endif %}
</dl>
{% else %}
@@ -32,7 +47,7 @@
<form action="{{ form.action|default:request.path }}" method="POST" id="auth-request-form">
{% csrf_token %}
{% include 'form_errors.html' %}
<div class="mb-3 row">
<div class="form-group row">
<label class="col-sm-4 col-form-label"
for="{{ form.email.id_for_label }}">{{ form.email.label }}</label>
<div class="col-sm-8">
@@ -42,24 +57,16 @@
</form>
</div>
</div>
<script src="{% static 'js/clipboard.min.js' %}"></script>
<script>
document.getElementById('auth-request-form').addEventListener('submit', function () {
this.querySelectorAll('button').forEach(function (button) { button.disabled = true; });
});
var clipboard = new ClipboardJS('.btn');
clipboard.on('success', function(e) {
var popover = bootstrap.Popover.getOrCreateInstance(e.trigger, {trigger: 'manual'});
popover.show();
window.setTimeout(function () {popover.hide()}, 3000);
e.clearSelection();
<script>
$('#auth-request-form').on('submit', function () {
$('#auth-request-form button').attr('disabled', true);
});
</script>
{% endblock %}
{% block footer %}
<div class="row g-2">
<div class="form-row">
<div class="btn-group" role="group">
<a type="button" target="_blank" href="{% url 'event_authorise_preview' object.pk %}" class="btn btn-info text-nowrap"><span class="fas fa-drafting-compass"></span> Preview</a>
<button type="submit" class="form-control btn btn-primary" form="auth-request-form">

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% block title %}TEC Email Address Required{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load help_text from filters %}
{% load profile_by_index from filters %}
{% load yesnoi from filters %}
@@ -6,11 +6,9 @@
{% block content %}
<div class="row">
<div class="col-12 text-end my-3">
<div class="col-12 text-right my-3">
{% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
<a href="{% url 'event_pt' object.event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="d-none d-sm-inline">Create Power Test</span></a>
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div>
</div>
@@ -32,7 +30,21 @@
</a>
{% endif %}
</dd>
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
<dd class="col-6">
{% if object.power_mic %}
<a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a>
{% else %}
None
{% endif %}
</dd>
</dl>
<p>List vehicles and their drivers</p>
<ul>
{% for i in object.vehicles.all %}
<li>{{i}}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
@@ -69,18 +81,172 @@
</div>
</div>
</div>
{% include 'partials/crew_list.html' with event=object.event %}
</div>
<div class="col-12 text-end mt-4">
<div class="card mb-3">
<div class="card-header">Crew Record</div>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Crewmember</th>
<th scope="col">Start Time</th>
<th scope="col">Role</th>
<th scope="col">End Time</th>
</tr>
</thead>
<tbody id="crewmemberst">
{% for crew in object.crew.all %}
<tr>
<td>{{crew.crewmember}}</td>
<td>{{crew.start}}</td>
<td>{{crew.role}}</td>
<td>{{crew.end}}</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center bg-warning">Apparently this event happened by magic...</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card mb-3">
<div class="card-header">Power {% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
<div class="card-body">
{% if object.event.riskassessment.event_size == 0 %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
<dd class="col-2">
{{ object.rcds|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
<dd class="col-2">
{{ object.supply_test|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
<dd class="col-2">
{{ object.earthing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
<dd class="col-2">
{{ object.pat|yesnoi }}
</dd>
</dl>
{% else %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt>
<dd class="col-2">
{{ object.source_rcd|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'labelling'|safe }}</dt>
<dd class="col-2">
{{ object.labelling|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
<dd class="col-2">
{{ object.earthing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
<dd class="col-2">
{{ object.pat|yesnoi }}
</dd>
</dl>
<hr>
<p>Tests at first distro</p>
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" colspan="3" class="text-center">Value</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small></th>
<th>{{ object|help_text:'fd_voltage_l1' }}</th>
<th>{{ object|help_text:'fd_voltage_l2' }}</th>
<th>{{ object|help_text:'fd_voltage_l3' }}</th>
</tr>
<tr>
<td>{{ object.fd_voltage_l1 }}</td>
<td>{{ object.fd_voltage_l2 }}</td>
<td>{{ object.fd_voltage_l3 }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_phase_rotation'|safe }}</th>
<td colspan="3">{{ object.fd_phase_rotation|yesnoi }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_earth_fault'|safe}}</th>
<td colspan="3">{{ object.fd_earth_fault }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_pssc'}}</th>
<td colspan="3">{{ object.fd_pssc }}</td>
</tr>
</tbody>
</table>
<hr>
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" class="text-center">Point 1</th>
<th scope="col" class="text-center">Point 2</th>
<th scope="col" class="text-center">Point 3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">{{ object|help_text:'w1_description'|safe}}</th>
<td>{{ object.w1_description }}</td>
<td>{{ object.w2_description|default:'' }}</td>
<td>{{ object.w3_description|default:'' }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_polarity'|safe}}</th>
<td>{{ object.w1_polarity|yesnoi }}</td>
<td>{{ object.w2_polarity|default:''|yesnoi }}</td>
<td>{{ object.w3_polarity|default:''|yesnoi }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_voltage'|safe}}</th>
<td>{{ object.w1_voltage }}</td>
<td>{{ object.w2_voltage|default:'' }}</td>
<td>{{ object.w3_voltage|default:'' }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_earth_fault'|safe}}</th>
<td>{{ object.w1_earth_fault }}</td>
<td>{{ object.w2_earth_fault|default:'' }}</td>
<td>{{ object.w3_earth_fault|default:'' }}</td>
</tr>
</tbody>
</table>
<hr>
<dl class="row">
<dt class="col-10">{{ object|help_text:'all_rcds_tested'|safe }}</dt>
<dd class="col-2">
{{ object.all_rcds_tested|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'public_sockets_tested'|safe }}</dt>
<dd class="col-2">
{{ object.public_sockets_tested|yesnoi }}
</dd>
</dl>
<hr>
{% include 'partials/ec_power_info.html' %}
{% endif %}
</div>
</div>
<div class="col-12 text-right">
{% button 'edit' url='ec_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.pk text="Event" %}
<a href="{% url 'event_pt' object.event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="d-none d-sm-inline">Create Power Test</span></a>
{% include 'partials/review_status.html' with perm=perms.RIGS.review_eventchecklist review='ec_review' %}
</div>
<div class="col-12 text-end">
<div class="col-12 text-right">
{% include 'partials/last_edited.html' with target="eventchecklist_history" %}
</div>
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load help_text from filters %}
@@ -13,18 +13,57 @@
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/interaction.js' %}"></script>
{% endblock %}
{% block js %}
{{ block.super }}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
$(document).ready(function () {
$('button[data-action=add]').on('click', function (event) {
event.preventDefault();
let target = $($(this).attr('data-target'));
let newID = Number(target.attr('data-pk'));
let newRow = $($(this).attr('data-clone'))
.clone().attr('style', "")
.attr('id', function(i, val){
return val.split("_")[0] + '_' + newID;
})
.appendTo(target);
newRow.find('select,input').attr('name', function(i, val){
return val.split("_")[0] + '_' + newID;
})//Disabled is to prevent the hidden row being sent to the form
.removeAttr('disabled');
newRow.find('button[data-action=delete]').attr('data-id', newID);
newRow.find('select').addClass('selectpicker');
newRow.find('.selectpicker').selectpicker('refresh');
$(".selectpicker").each(function(){initPicker($(this))});
initDatetime();
$(target).attr('data-pk', newID - 1);
});
$(document).on('click', 'button[data-action=delete]', function(event) {
event.preventDefault();
$(this).closest('tr').remove();
});
//Somewhat rudimentary way of ensuring people fill in completely (if it hits the database validation the whole table row disappears when the page reloads...)
//the not is to avoid adding it to some of bootstrap-selects extra crap
$('#vehiclest,#crewmemberst').on('change', 'select,input', function () {
$(this).closest('tr').find("select,input").not(':input[type=search]').attr('required', 'true');
});
});
</script>
{% endblock %}
{% block content %}
<div class="col-12">
{% include 'form_errors.html' %}
<form role="form" method="POST" action="{% if edit %}{% url 'ec_edit' pk=object.pk %}{% else %}{% url 'event_ec' pk=event.pk %}{% endif %}">
{% if edit %}
<form role="form" method="POST" action="{% url 'ec_edit' pk=object.pk %}">
{% else %}
<form role="form" method="POST" action="{% url 'event_ec' pk=event.pk %}">
{% endif %}
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
value="{{event.pk}}"/>
{% csrf_token %}
@@ -43,7 +82,7 @@
<dt class="col-4">Event Size</dt>
<dd class="col-8">{% include 'partials/event_size.html' with object=event.riskassessment %}</dd>
</dl>
<div class="mb-3 row g-2">
<div class="form-group form-row">
<label for="{{ form.date.id_for_label }}"
class="col-4 col-form-label">{{ form.date.label }}</label>
{% if not form.date.value %}
@@ -52,15 +91,63 @@
{% render_field form.date class+="form-control col-8" %}
{% endif %}
</div>
<div class="mb-3 row g-2" id="{{ form.venue.id_for_label }}-group">
<div class="form-group form-row" id="{{ form.venue.id_for_label }}-group">
<label for="{{ form.venue.id_for_label }}"
class="col-4 col-form-label">{{ form.venue.label }}</label>
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="ts-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %}
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
{% elif event.venue %}
<option value="{{event.venue.pk}}" selected="selected">{{ event.venue.name }}</option>
{% endif %}
</select>
</div>
<div class="form-group form-row" id="{{ form.power_mic.id_for_label }}-group">
<label for="{{ form.power_mic.id_for_label }}"
class="col-4 col-form-label">{{ form.power_mic.help_text }}</label>
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="form-control selectpicker col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true">
{% if power_mic %}
<option value="{{power_mic.pk}}" selected="selected">{{ power_mic.name }}</option>
{% elif event.riskassessment.power_mic %}
<option value="{{event.riskassessment.power_mic.pk}}" selected="selected">{{ event.riskassessment.power_mic.name }}</option>
{% endif %}
</select>
</div>
<p class="pt-3 font-weight-bold">List vehicles and their drivers</p>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Vehicle</th>
<th scope="col">Driver</th>
<th scope="col"></th>
</tr>
</thead>
<tbody id="vehiclest" data-pk="-1">
<tr id="vehicles_new" style="display: none;">
<td><input type="text" class="form-control" name="vehicle_new" disabled="true"/></td>
<td><select data-container="body" class="form-control" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" name="driver_new" disabled="true"></select></td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button></td>
</tr>
{% for i in object.vehicles.all %}
<tr id="vehicles_{{i.pk}}">
<td><input name="vehicle_{{i.pk}}" type="text" class="form-control" value="{{ i.vehicle }}"/></td>
<td>
<select data-container="body" name="driver_{{i.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if i.driver != '' %}
<option value="{{i.driver.pk}}" selected="selected">{{ i.driver.name }}</option>
{% endif %}
</select>
</td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{i.pk}}' data-action='delete' data-target='#vehicle'><span class="fas fa-times"></span></button></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="text-right">
<button type="button" class="btn btn-secondary" id="vehicle-add" data-action='add' data-target='#vehiclest' data-clone='#vehicles_new'><span class="fas fa-plus"></span> Add Vehicle</button>
</div>
</div>
</div>
</div>
@@ -88,8 +175,178 @@
</div>
</div>
</div>
<div class="row my-3">
<div class="col-12">
<div class="card">
<div class="card-header">Crew Record</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Person</th>
<th scope="col">Start Time</th>
<th scope="col">Role</th>
<th scope="col">End Time</th>
<th scope="col"></th>
</tr>
</thead>
<tbody id="crewmemberst" data-pk="-1">
<tr id="crew_new" style="display: none;">
<td>
<select name="crewmember_new" class="form-control" data-container="body" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" disabled="true"></select>
</td>
<td style="min-width: 15ch"><input name="start_new" type="datetime-local" class="form-control" value="{{ i.start }}" disabled=""/></td>
<td style="min-width: 15ch"><input name="role_new" type="text" class="form-control" value="{{ i.role }}" disabled="true"/></td>
<td style="min-width: 15ch"><input name="end_new" type="datetime-local" class="form-control" value="{{ i.end }}" disabled="true" /></td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{crew.pk}}' data-action='delete' data-target='#crewmember'><span class="fas fa-times"></span></button></td>
</tr>
{% for crew in object.crew.all %}
<tr id="crew_{{crew.pk}}">
<td>
<select data-container="body" name="crewmember_{{crew.pk}}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if crew.crewmember != '' %}
<option value="{{crew.crewmember.pk}}" selected="selected">{{ crew.crewmember.name }}</option>
{% endif %}
</select>
</td>
<td><input name="start_{{crew.pk}}" type="datetime-local" class="form-control" value="{{ crew.start|date:'Y-m-d' }}T{{ crew.start|date:'H:i:s' }}"/></td>
<td><input name="role_{{crew.pk}}" type="text" class="form-control" value="{{ crew.role }}"/></td>
<td><input name="end_{{crew.pk}}" type="datetime-local" class="form-control" value="{{ crew.end|date:'Y-m-d' }}T{{ crew.end|date:'H:i:s' }}"/></td>
<td><button type="button" class="btn btn-danger btn-sm mt-1" data-id='{{crew.pk}}' data-action='delete' data-target='#crewmember'><span class="fas fa-times"></span></button></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card-footer">
<div class="text-right">
<button type="button" class="btn btn-secondary" data-action='add' data-target='#crewmemberst' data-clone='#crew_new'><span class="fas fa-plus"></span> Add Crewmember</button>
</div>
</div>
</div>
</div>
</div>
{% if event.riskassessment.event_size == 0 %}
<div class="row my-3" id="size-0">
<div class="col-12">
<div class="card border-success">
<div class="card-header">Electrical Checks <small>for ‘Small’ TEC Events <6kVA (approx. 26A)</small></div>
<div class="card-body">
{% include 'partials/checklist_checkbox.html' with formitem=form.rcds %}
{% include 'partials/checklist_checkbox.html' with formitem=form.supply_test %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
</div>
</div>
</div>
</div>
{% else %}
<div class="row my-3" id="size-1">
<div class="col-12">
{% if event.riskassessment.event_size == 1 %}
<div class="card border-warning">
<div class="card-header">Electrical Checks <small>for ‘Medium’ TEC Events </small></div>
<div class="card-body">
{% else %}
<div class="card border-danger">
<div class="card-header">Electrical Checks <small>for ‘Large’ TEC Events</small></div>
<div class="card-body">
<div class="alert alert-danger"><strong>Here be dragons. Ensure you have appeased the Power Gods before continuing... (If you didn't check with a Supervisor, <em>you cannot continue your event!</em>)</strong></div>
{% endif %}
{% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %}
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
<hr>
<p>Tests at first distro</p>
<div class="table-responsive">
<table class="table table-bordered table-sm">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" colspan="3" class="text-center">Value</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small></th>
<th class="text-center">{{ form.fd_voltage_l1.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l2.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l3.help_text }}</th>
</tr>
<tr>
<td>{% render_field form.fd_voltage_l1 class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.fd_voltage_l2 class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.fd_voltage_l3 class+="form-control" style="min-width: 5rem;" %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_phase_rotation.help_text|safe}}</th>
<td colspan="3">{% include 'partials/checklist_checkbox.html' with formitem=form.fd_phase_rotation %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_earth_fault.help_text|safe}}</th>
<td colspan="3">{% render_field form.fd_earth_fault class+="form-control" %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_pssc.help_text|safe}}</th>
<td colspan="3">{% render_field form.fd_pssc class+="form-control" %}</td>
</tr>
</tbody>
</table>
</div>
<hr>
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" class="text-center">Point 1</th>
<th scope="col" class="text-center">Point 2</th>
<th scope="col" class="text-center">Point 3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">{{form.w1_description.help_text|safe}}</th>
<td>{% render_field form.w1_description class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.w2_description class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.w3_description class+="form-control" style="min-width: 5rem;" %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_polarity.help_text|safe}}</th>
<td>{% render_field form.w1_polarity %}</td>
<td>{% render_field form.w2_polarity %}</td>
<td>{% render_field form.w3_polarity %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_voltage.help_text|safe}}</th>
<td>{% render_field form.w1_voltage class+="form-control" %}</td>
<td>{% render_field form.w2_voltage class+="form-control" %}</td>
<td>{% render_field form.w3_voltage class+="form-control" %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_earth_fault.help_text|safe}}</th>
<td>{% render_field form.w1_earth_fault class+="form-control" %}</td>
<td>{% render_field form.w2_earth_fault class+="form-control" %}</td>
<td>{% render_field form.w3_earth_fault class+="form-control" %}</td>
</tr>
</tbody>
</table>
</div>
<hr/>
{% include 'partials/checklist_checkbox.html' with formitem=form.all_rcds_tested %}
{% include 'partials/checklist_checkbox.html' with formitem=form.public_sockets_tested %}
{% include 'partials/ec_power_info.html' %}
</div>
</div>
</div>
</div>
{% endif %}
<div class="row mt-3">
<div class="col-sm-12 text-end">
<div class="col-sm-12 text-right">
{% button 'submit' %}
</div>
</div>

View File

@@ -1,105 +0,0 @@
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load button from filters %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/easymde.min.css' %}">
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/easymde.min.js' %}"></script>
{% endblock %}
{% block js %}
{{ block.super }}
<script src="{% static 'js/interaction.js' %}"></script>
{% endblock %}
{% block content %}
<div class="col-12">
{% include 'form_errors.html' %}
<form id="checkin" role="form" method="POST" action="{{ form.action|default:request.path }}">
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
value="{{event.pk}}"/>
{% if not is_ajax and request.user.pk == event.mic.pk %}
<div class="mb-3">
<label for="{{ form.person.id_for_label }}"
class="col-sm-4 col-form-label">{{ form.person.label }}</label>
<div class="col-sm-8">
<select id="{{ form.person.id_for_label }}" name="{{ form.person.name }}" class="px-0 ts-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if form.instance.person %}
<option value="{{ form.instance.person.pk }}" selected="selected">{{ form.instance.person.name }}</option>
{% else %}
<option value="{{ request.user.pk }}" selected="selected">{{ request.user.name }}</option>
{% endif %}
</select>
</div>
</div>
{% else %}
<input type="hidden" name="{{ form.person.name }}" id="{{ form.person.id_for_label }}"
value="{{request.user.pk}}"/>
{% endif %}
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.time.id_for_label }}"
class="col-sm-4 col-form-label">Start Time</label>
<div class="col-sm-8">
{% render_field form.time class+="form-control" %}
</div>
</div>
<div class="mb-3">
<label for="{{ form.role.id_for_label }}" class="col col-form-label">Role</label>
<div class="row ps-3">
<div class="col-md-6 col-sm-12">
<button type="button" class="btn btn-primary" onclick="document.getElementById('id_role').value='MIC'">MIC</button>
<button type="button" class="btn btn-danger" onclick="document.getElementById('id_role').value='Power MIC'">Power MIC</button>
<button type="button" class="btn btn-info" onclick="document.getElementById('id_role').value='Crew'">Crew</button>
</div>
<div class="col-md-6 mt-2">
{% render_field form.role class+="form-control" placeholder="Other (enter text)" %}
</div>
</div>
</div>
<div class="mb-3">
<label for="{{ form.vehicle.id_for_label }}" class="col col-form-label">Vehicle (if applicable)</label>
<div class="row ps-3">
<div class="col-md-6 col-sm-12">
<button type="button" class="btn btn-primary" onclick="document.getElementById('id_vehicle').value='Virgil'"><span class="fas fa-truck-moving"></span> Virgil</button>
<button type="button" class="btn btn-secondary" onclick="document.getElementById('id_vehicle').value='Virgil + Erms'"><span class="fas fa-trailer"></span><span class="fas fa-truck-moving"></span> Virgil + Erms</button>
</div>
<div class="col-md-6 mt-2">
{% render_field form.vehicle class+="form-control" placeholder="Other (enter text)" %}
</div>
</div>
</div>
{% if edit or manual %}
<div class="mb-3">
<label for="{{ form.end_time.id_for_label }}"
class="col-sm-4 col-form-label">End Time</label>
<div class="col-sm-8">
{% render_field form.end_time class+="form-control" %}
</div>
</div>
{% endif %}
{% if not is_ajax %}
<div class="row mt-3">
<div class="col-sm-12 text-end">
{% button 'submit' %}
</div>
</div>
{% endif %}
</form>
</div>
{% endblock %}
{% block footer %}
<div class="col-sm-12 text-end pe-0">
<button type="submit" class="btn btn-primary" title="Save" form="checkin"
><span class="fas fa-save align-middle"></span> <span class="d-none d-sm-inline align-middle">Save</span></button>
</div>
{% endblock %}

View File

@@ -12,7 +12,6 @@
<th scope="col">Dates</th>
<th scope="col">RA</th>
<th scope="col">Checklists</th>
<th scope="col">Power Records</th>
</tr>
</thead>
<tbody>
@@ -36,14 +35,6 @@
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="d-none d-sm-inline">Create</span></a>
</td>
<td>
{% for record in event.power_tests.all %}
{% include 'partials/hs_status.html' with event=event object=record view='pt_detail' edit='pt_edit' create='event_pt' review='pt_review' perm=perms.RIGS.review_power %}
<br/>
{% endfor %}
<a href="{% url 'event_pt' event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="d-none d-sm-inline">Create</span></a>
</td>
</tr>
{% empty %}
<tr class="bg-warning text-dark">

View File

@@ -1,176 +0,0 @@
{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load help_text from filters %}
{% load profile_by_index from filters %}
{% load yesnoi from filters %}
{% load button from filters %}
{% block content %}
<div class="row">
<div class="col-12 text-end my-3">
{% button 'edit' url='pt_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %}
</div>
</div>
<div class="card mb-3">
<div class="card-header">{% include 'partials/event_size.html' with object=object.event.riskassessment %}</div>
<div class="card-body">
<dl class="row">
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
<dd class="col-6">
{% if object.power_mic %}
<a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a>
{% else %}
None
{% endif %}
</dd>
<dt class="col-6">Venue</dt>
<dd class="col-6">
{% if object.venue %}
<a href="{% url 'venue_detail' object.venue.pk %}" class="modal-href">
{{ object.venue }}
</a>
{% endif %}
</dd>
<dt class="col-6">Notes</dt>
<dd class="col-6">
{{ object.notes }}
</dd>
</dl>
{% if object.event.riskassessment.event_size == 0 %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'rcds'|safe }}</dt>
<dd class="col-2">
{{ object.rcds|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'supply_test'|safe }}</dt>
<dd class="col-2">
{{ object.supply_test|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
<dd class="col-2">
{{ object.earthing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
<dd class="col-2">
{{ object.pat|yesnoi }}
</dd>
</dl>
{% else %}
<dl class="row">
<dt class="col-10">{{ object|help_text:'source_rcd'|safe }}</dt>
<dd class="col-2">
{{ object.source_rcd|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'labelling'|safe }}</dt>
<dd class="col-2">
{{ object.labelling|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'earthing'|safe }}</dt>
<dd class="col-2">
{{ object.earthing|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'pat'|safe }}</dt>
<dd class="col-2">
{{ object.pat|yesnoi }}
</dd>
</dl>
<hr>
<p>Tests at first distro</p>
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" colspan="3" class="text-center">Value</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small> / V</th>
<th>{{ object|help_text:'fd_voltage_l1' }}</th>
<th>{{ object|help_text:'fd_voltage_l2' }}</th>
<th>{{ object|help_text:'fd_voltage_l3' }}</th>
</tr>
<tr>
<td>{{ object.fd_voltage_l1 }}</td>
<td>{{ object.fd_voltage_l2 }}</td>
<td>{{ object.fd_voltage_l3 }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_phase_rotation'|safe }}</th>
<td colspan="3">{{ object.fd_phase_rotation|yesnoi }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_earth_fault'|safe}}</th>
<td colspan="3">{{ object.fd_earth_fault }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'fd_pssc'}}</th>
<td colspan="3">{{ object.fd_pssc }}</td>
</tr>
</tbody>
</table>
<hr>
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" class="text-center">Point 1</th>
<th scope="col" class="text-center">Point 2</th>
<th scope="col" class="text-center">Point 3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">{{ object|help_text:'w1_description'|safe}}</th>
<td>{{ object.w1_description }}</td>
<td>{{ object.w2_description|default:'' }}</td>
<td>{{ object.w3_description|default:'' }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_polarity'|safe}}</th>
<td>{{ object.w1_polarity|yesnoi }}</td>
<td>{{ object.w2_polarity|default:''|yesnoi }}</td>
<td>{{ object.w3_polarity|default:''|yesnoi }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_voltage'|safe}}</th>
<td>{{ object.w1_voltage }}</td>
<td>{{ object.w2_voltage|default:'' }}</td>
<td>{{ object.w3_voltage|default:'' }}</td>
</tr>
<tr>
<th scope="row">{{ object|help_text:'w1_earth_fault'|safe}}</th>
<td>{{ object.w1_earth_fault }}</td>
<td>{{ object.w2_earth_fault|default:'' }}</td>
<td>{{ object.w3_earth_fault|default:'' }}</td>
</tr>
</tbody>
</table>
<hr>
<dl class="row">
<dt class="col-10">{{ object|help_text:'all_rcds_tested'|safe }}</dt>
<dd class="col-2">
{{ object.all_rcds_tested|yesnoi }}
</dd>
<dt class="col-10">{{ object|help_text:'public_sockets_tested'|safe }}</dt>
<dd class="col-2">
{{ object.public_sockets_tested|yesnoi }}
</dd>
</dl>
<hr>
{% include 'partials/ec_power_info.html' %}
{% endif %}
</div>
</div>
<div class="col-12 text-end">
{% button 'print' 'pt_print' object.pk %}
{% button 'edit' url='pt_edit' pk=object.pk %}
{% button 'view' url='event_detail' pk=object.event.pk text="Event" %}
{% include 'partials/review_status.html' with perm=perms.RIGS.review_power review='pt_review' %}
</div>
<div class="col-12 text-end">
{% include 'partials/last_edited.html' with target="powertestrecord_history" %}
</div>
{% endblock %}

View File

@@ -1,197 +0,0 @@
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load help_text from filters %}
{% load profile_by_index from filters %}
{% load button from filters %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
{% endblock %}
{% block preload_js %}
{{ block.super }}
<script src="{% static 'js/selects.js' %}"></script>
{% endblock %}
{% block js %}
{{ block.super }}
{% endblock %}
{% block content %}
<div class="col-12">
{% include 'form_errors.html' %}
{% if edit %}
<form role="form" method="POST" action="{% url 'pt_edit' pk=object.pk %}">
{% else %}
<form role="form" method="POST" action="{% url 'event_pt' pk=event.pk %}">
{% endif %}
<input type="hidden" name="{{ form.event.name }}" id="{{ form.event.id_for_label }}"
value="{{event.pk}}"/>
{% csrf_token %}
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">Event Information</div>
<div class="card-body">
<dl class="row">
<dt class="col-4">Event Date</dt>
<dd class="col-8">{{ event.start_date}}{%if event.end_date %}-{{ event.end_date}}{%endif%}</dd>
<dt class="col-4">Event Name</dt>
<dd class="col-8">{{ event.name }}</dd>
<dt class="col-4">Client</dt>
<dd class="col-8">{{ event.person }}</dd>
<dt class="col-4">Event Size</dt>
<dd class="col-8">{% include 'partials/event_size.html' with object=event.riskassessment %}</dd>
</dl>
<hr>
<div class="mb-3 row g-2" id="{{ form.power_mic.id_for_label }}-group">
<label for="{{ form.power_mic.id_for_label }}"
class="col-4 col-form-label">{{ form.power_mic.help_text }}</label>
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="ts-select col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required="true">
{% if power_mic %}
<option value="{{power_mic.pk}}" selected="selected">{{ power_mic.name }}</option>
{% endif %}
</select>
</div>
<div class="mb-3 row g-2" id="{{ form.venue.id_for_label }}-group">
<label for="{{ form.venue.id_for_label }}"
class="col-4 col-form-label">{{ form.venue.label }}</label>
<select id="{{ form.venue.id_for_label }}" name="{{ form.venue.name }}" class="ts-select col-8" data-live-search="true" data-sourceurl="{% url 'api_secure' model='venue' %}">
{% if venue %}
<option value="{{venue.pk}}" selected="selected">{{ venue.name }}</option>
{% endif %}
</select>
</div>
<label for="{{ form.notes.id_for_label }}">Notes</label>
{% render_field form.notes class+="form-control" %}
</div>
</div>
</div>
</div>
{% if event.riskassessment.event_size == 0 %}
<div class="row my-3" id="size-0">
<div class="col-12">
<div class="card border-success">
<div class="card-header">Electrical Checks <small>for ‘Small’ TEC Events <6kVA (approx. 26A)</small></div>
<div class="card-body">
{% include 'partials/checklist_checkbox.html' with formitem=form.rcds %}
{% include 'partials/checklist_checkbox.html' with formitem=form.supply_test %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
</div>
</div>
</div>
</div>
{% else %}
<div class="row my-3" id="size-1">
<div class="col-12">
{% if event.riskassessment.event_size == 1 %}
<div class="card border-warning">
<div class="card-header">Electrical Checks <small>for ‘Medium’ TEC Events </small></div>
<div class="card-body">
{% else %}
<div class="card border-danger">
<div class="card-header">Electrical Checks <small>for ‘Large’ TEC Events</small></div>
<div class="card-body">
<div class="alert alert-danger"><strong>Here be dragons. Ensure you have appeased the Power Gods before continuing... (If you didn't check with a Supervisor, <em>you cannot continue your event!</em>)</strong></div>
{% endif %}
{% include 'partials/checklist_checkbox.html' with formitem=form.source_rcd %}
{% include 'partials/checklist_checkbox.html' with formitem=form.labelling %}
{% include 'partials/checklist_checkbox.html' with formitem=form.earthing %}
{% include 'partials/checklist_checkbox.html' with formitem=form.pat %}
<hr>
<p>Tests at first distro</p>
<div class="table-responsive">
<table class="table table-bordered table-sm">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" colspan="3" class="text-center">Value</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" rowspan="2">Voltage<br><small>(cube meter)</small> / V</th>
<th class="text-center">{{ form.fd_voltage_l1.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l2.help_text }}</th>
<th class="text-center">{{ form.fd_voltage_l3.help_text }}</th>
</tr>
<tr>
<td>{% render_field form.fd_voltage_l1 class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.fd_voltage_l2 class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.fd_voltage_l3 class+="form-control" style="min-width: 5rem;" %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_phase_rotation.help_text|safe}}</th>
<td colspan="3">{% include 'partials/checklist_checkbox.html' with formitem=form.fd_phase_rotation %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_earth_fault.help_text|safe}}</th>
<td colspan="3">{% render_field form.fd_earth_fault class+="form-control" %}</td>
</tr>
<tr>
<th scope="row">{{form.fd_pssc.help_text|safe}}</th>
<td colspan="3">{% render_field form.fd_pssc class+="form-control" %}</td>
</tr>
</tbody>
</table>
</div>
<hr>
<p>Tests at 'Worst Case' points (at least 1 point required)</p>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" class="text-center">Test</th>
<th scope="col" class="text-center">Point 1</th>
<th scope="col" class="text-center">Point 2</th>
<th scope="col" class="text-center">Point 3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">{{form.w1_description.help_text|safe}}</th>
<td>{% render_field form.w1_description class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.w2_description class+="form-control" style="min-width: 5rem;" %}</td>
<td>{% render_field form.w3_description class+="form-control" style="min-width: 5rem;" %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_polarity.help_text|safe}}</th>
<td>{% render_field form.w1_polarity %}</td>
<td>{% render_field form.w2_polarity %}</td>
<td>{% render_field form.w3_polarity %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_voltage.help_text|safe}}</th>
<td>{% render_field form.w1_voltage class+="form-control" %}</td>
<td>{% render_field form.w2_voltage class+="form-control" %}</td>
<td>{% render_field form.w3_voltage class+="form-control" %}</td>
</tr>
<tr>
<th scope="row">{{form.w1_earth_fault.help_text|safe}}</th>
<td>{% render_field form.w1_earth_fault class+="form-control" %}</td>
<td>{% render_field form.w2_earth_fault class+="form-control" %}</td>
<td>{% render_field form.w3_earth_fault class+="form-control" %}</td>
</tr>
</tbody>
</table>
</div>
<hr/>
{% include 'partials/checklist_checkbox.html' with formitem=form.all_rcds_tested %}
{% include 'partials/checklist_checkbox.html' with formitem=form.public_sockets_tested %}
{% include 'partials/ec_power_info.html' %}
</div>
</div>
</div>
</div>
{% endif %}
<div class="row mt-3">
<div class="col-sm-12 text-end">
{% button 'submit' %}
</div>
</div>
</form>
</div>
{% endblock %}

View File

@@ -1,235 +0,0 @@
{% extends 'base_print.xml' %}
{% load filters %}
{% block extrastyles %}
<paraStyle name="style.powerReviewed" borderPadding="3" alignment="center" backColor="green" textColor="white"/>
<paraStyle name="style.powerUnreviewed" borderPadding="3" alignment="center" backColor="red" textColor="white"/>
<paraStyle name="style.smallText" fontSize="8"/>
<paraStyle leftIndent="2in" rightIndent="2in" name="style.smallEvent" fontSize="10" alignment="center" backColor="green" textColor="white" borderPadding="4" borderColor="black"/>
<paraStyle leftIndent="2in" rightIndent="2in" name="style.mediumEvent" fontSize="10" alignment="center" backColor="orange" textColor="white" borderPadding="4" borderColor="black"/>
<paraStyle leftIndent="2in" rightIndent="2in" name="style.largeEvent" fontSize="10" alignment="center" backColor="red" textColor="white" borderPadding="4" borderColor="black"/>
<blockTableStyle id="powerTable">
<blockValign value="middle"/>
<lineStyle kind="LINEABOVE" colorName="black" thickness="1"/>
<lineStyle kind="LINEBELOW" colorName="black" thickness="1"/>
<lineStyle kind="LINEAFTER" colorName="black" thickness="1"/>
<lineStyle kind="LINEBEFORE" colorName="black" thickness="1"/>
</blockTableStyle>
<blockTableStyle id="voltageTable">
<blockValign value="middle"/>
</blockTableStyle>
{% endblock %}
{% block content %}
<spacer length="15"/>
<h1>Power Test Record for <strong>{{ object.event }}</strong></h1>
<spacer length="15"/>
<h2>Client: {{ object.event.person|default:object.event.organisation }} | Venue: {{ object.event.venue }} | MIC: {{ object.event.mic }}</h2>
<spacer length="15"/>
<hr/>
<spacer length="15"/>
{% if object.reviewed_by %}
<para style="style.powerReviewed"><strong>Reviewed by: {{ object.reviewed_by }} at {{ object.reviewed_at|date:"D d/m/Y" }}</strong></para>
{% else %}
<para style="style.powerUnreviewed"><strong>Power test results not yet reviewed</strong></para>
{% endif %}
<spacer length="15"/>
<hr/>
<spacer length="15"/>
<h2 fontSize="16">Power Plan Information</h2>
<spacer length="15"/>
{% if object.event.riskassessment.event_size == 0 %}
<para style="style.smallEvent"><strong>Small Event</strong></para>
{% elif object.event.riskassessment.event_size == 1 %}
<para style="style.mediumEvent"><strong>Medium Event</strong></para>
{% elif object.event.riskassessment.event_size == 2 %}
<para style="style.largeEvent"><strong>Large Event</strong></para>
{% endif %}
<spacer length="15"/>
<blockTable colWidths="250,250">
<tr>
<td><para><strong>Power MIC:</strong> {{ object.power_mic }}</para></td>
<td><para><strong>Venue:</strong> {{ object.event.venue }}</para></td>
</tr>
<tr>
<td><para><strong>Event Date:</strong> {{ object.event.start_date |date:"D d/m/Y" }}</para></td>
<td><para><strong>Generators:</strong> {{ object.event.riskassessment.generators|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Power Test taken at:</strong> {{ object.date_created|date:"D d/m/Y H:i" }}</para></td>
<td><para><strong>Other Companies Power:</strong> {{ object.event.riskassessment.other_companies_power|yesno|capfirst }}</para></td>
</tr>
</blockTable>
<spacer length="15"/>
{% if object.notes %}
<hr/>
<spacer length="15"/>
<para><strong>Additional Notes:</strong></para>
<spacer length="15"/>
<para>{{ object.notes }}</para>
<spacer length="15"/>
{% endif %}
<hr/>
<spacer length="15"/>
{% comment %}
0 - Small event
1 - Medium event (extra power records)
2 - Large event (extra power records)
{% endcomment %}
{% if object.event.riskassessment.event_size >= 1 %}
<para alignment="center"><strong>Power Test results enclosed on next page</strong></para>
<condPageBreak height="10in"/>
<h2 fontSize="16">Event Power Checklist</h2>
<spacer length="15"/>
<blockTable colWidths="250,270" style="powerTable">
<tr>
<td><para><strong>All circuit RCDs tested?</strong></para><para style="style.smallText">(using test button)</para></td>
<td><para>{{ object.all_rcds_tested|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Public/performer accessible circuits tested?</strong></para><para style="style.smallText">(using socket tester)</para></td>
<td><para>{{ object.public_sockets_tested|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Source RCD protected?</strong></para><para style="style.smallText">(if cable is more than 3m long)</para></td>
<td><para>{{ object.source_rcd|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Appropriate and clear labelling on distribution and cabling?</strong></para></td>
<td><para>{{ object.labelling|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Equipment appropriately earthed?</strong></para><para style="style.smallText">(truss, stage, generators, etc.)</para></td>
<td><para>{{ object.earthing|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>All equipment in PAT period?</strong><br/><br/></para></td>
<td><para>{{ object.pat|yesno|capfirst }}</para></td>
</tr>
</blockTable>
<spacer length="15"/>
<h2 fontSize="14">Power tests (First Distro)</h2>
<spacer length="5"/>
<blockTable colWidths="100,410" style="voltageTable">
<tr>
<td><para><strong>Voltage</strong></para><para style="style.smallText">(cube meter) / V</para></td>
<td>
<blockTable colWidths="100,100,100" style="powerTable">
<tr>
<td><para><strong>L1 - N</strong></para></td>
<td><para><strong>L2 - N</strong></para></td>
<td><para><strong>L3 - N</strong></para></td>
</tr>
<tr>
<td>{{ object.fd_voltage_l1}}</td>
<td>{{ object.fd_voltage_l2}}</td>
<td>{{ object.fd_voltage_l3}}</td>
</tr>
</blockTable>
</td>
</tr>
</blockTable>
<spacer length="10"/>
<blockTable colWidths="100,100,190,120" style="voltageTable">
<tr>
<td><para><strong>Phase Rotation</strong></para><para style="style.smallText">(if required)</para></td>
<td><para>{{ object.fd_phase_rotation|yesno|capfirst }}</para></td>
<td><para><strong>Earth Fault Loop Impedance (Z<sub>s</sub>) / Ω</strong></para></td>
<td><para>{{ object.fd_earth_fault }}</para></td>
</tr>
</blockTable>
<spacer length="15"/>
<para><strong>Prospective Short Circuit Current (PSCC)</strong> {{ object.fd_pssc }} A</para>
<spacer length="15"/>
<h2 fontSize="14">Power Tests (Worst Case Points)</h2>
<spacer length="15"/>
<blockTable colWidths="100,100,190,120" style="powerTable">
<tr>
<td><para><strong>Description</strong></para></td>
<td><para><strong>Polarity checked?</strong></para></td>
<td><para><strong>Voltage / V</strong></para></td>
<td><para><strong>Earth Fault Loop Impedance (Z<sub>s</sub>) / Ω</strong></para></td>
</tr>
{% if object.w1_description %}
<tr>
<td><para><strong>{{ object.w1_description }}</strong></para></td>
<td><para>{{ object.w1_polarity|yesno|capfirst }}</para></td>
<td><para>{{ object.w1_voltage }} V</para></td>
<td><para>{{ object.w1_earth_fault }}</para></td>
</tr>
{% endif %}
{% if object.w2_description %}
<tr>
<td><para><strong>{{ object.w2_description }}</strong></para></td>
<td><para>{{ object.w2_polarity|yesno|capfirst }}</para></td>
<td><para>{{ object.w2_voltage }} V</para></td>
<td><para>{{ object.w2_earth_fault }}</para></td>
</tr>
{% endif %}
{% if object.w3_description %}
<tr>
<td><para><strong>{{ object.w3_description }}</strong></para></td>
<td><para>{{ object.w3_polarity|yesno|capfirst }}</para></td>
<td><para>{{ object.w3_voltage }} V</para></td>
<td><para>{{ object.w3_earth_fault }}</para></td>
</tr>
{% endif %}
</blockTable>
{% else %}
{% comment %}
Small power test
{% endcomment %}
<h2 fontSize="16">Power Checklist</h2>
<spacer length="15"/>
<blockTable colWidths="250,270" style="powerTable">
<tr>
<td><para><strong>RCDs installed where needed and tested?</strong><br/><br/></para></td>
<td><para>{{ object.rcds|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Electrical supplies tested?</strong><br/><br/></para></td>
<td><para>{{ object.supply_test|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>Equipment appropriately earthed?</strong></para><para style="style.smallText">(truss, stage, generators, etc.)</para></td>
<td><para>{{ object.earthing|yesno|capfirst }}</para></td>
</tr>
<tr>
<td><para><strong>All equipment in PAT period?</strong><br/><br/></para></td>
<td><para>{{ object.pat|yesno|capfirst }}</para></td>
</tr>
</blockTable>
{% endif %}
{% endblock %}

View File

@@ -124,13 +124,6 @@
<td><para>{{ object|help_text:'persons_responsible_structures'|striptags }}</para></td>
<td><para>{{ object.persons_responsible_structures|default:'N/A' }}</para></td>
</tr>
<tr>
<td colspan="2"><h3><strong>Venue Access</strong></h3></td>
</tr>
<tr>
<td><para>{{ object|help_text:'parking_and_access'|striptags }}</para></td>
<td><para>{{ object.parking_and_access|yesno|capfirst }}</para></td>
</tr>
</blockTable>
<spacer length="15"/>\
<hr/>

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
{% load filters %}
{% block content %}
@@ -151,21 +151,10 @@
</dl>
</div>
</div>
<div class="card card-default mb-3">
<div class="card-header">Venue Access</div>
<div class="card-body">
<dl class="row">
<dt class="col-10">{{ object|help_text:'parking_and_access' }}</dt>
<dd class="col-2">
{{ object.parking_and_access|yesnoi:'invert' }}
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 text-end">
<div class="col-12 text-right">
{% button 'print' 'ra_print' object.pk %}
<a href="{% url 'ra_edit' object.pk %}" class="btn btn-warning my-3"><span class="fas fa-edit"></span> <span
class="d-none d-sm-inline">Edit</span></a>

View File

@@ -1,4 +1,4 @@
{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %}
{% load widget_tweaks %}
{% load static %}
{% load help_text from filters %}
@@ -9,39 +9,31 @@
{% endblock %}
{% block preload_js %}
<script src="{% static 'js/selects.js' %}"></script>
<script src="{% static 'js/selects.js' %}" async></script>
{% endblock %}
{% block js %}
<script src="{% static 'js/autocompleter.js' %}"></script>
<script src="{% static 'js/tooltip.js' %}"></script>
<script>
function parseBool(str) {
return str.toLowerCase() == 'true';
}
function onRadioChange(names, handler) {
names.forEach(function (name) {
document.querySelectorAll('input[type=radio][name=' + name + ']').forEach(function (radio) {
radio.addEventListener('change', handler);
});
});
}
onRadioChange(['big_power'], function() {
document.getElementById('{{ form.power_mic.id_for_label }}').required = parseBool(this.value);
$('input[type=radio][name=big_power]').change(function() {
$('#{{ form.power_mic.id_for_label }}').prop('required', parseBool(this.value));
});
onRadioChange(['outside', 'generators', 'other_companies_power', 'nonstandard_equipment_power', 'multiple_electrical_environments'], function() {
document.getElementById('{{ form.power_notes.id_for_label }}').required = parseBool(this.value);
$('input[type=radio][name=outside], input[type=radio][name=generators], input[type=radio][name=other_companies_power], input[type=radio][name=nonstandard_equipment_power], input[type=radio][name=multiple_electrical_environments]').change(function() {
$('#{{ form.power_notes.id_for_label }}').prop('required', parseBool(this.value));
$('#{{ form.power_plan.id_for_label }}').prop('required', parseBool(this.value));
});
onRadioChange(['special_structures'], function() {
var field = document.getElementById('{{ form.persons_responsible_structures.id_for_label }}');
field.hidden = !parseBool(this.value);
field.required = parseBool(this.value);
document.querySelector('label[for={{ form.persons_responsible_structures.id_for_label }}]').hidden = !parseBool(this.value);
$('input[type=radio][name=special_structures]').change(function() {
$('#{{ form.persons_responsible_structures.id_for_label }}').prop('hidden', !parseBool(this.value)).prop('required', parseBool(this.value));
$('label[for={{ form.persons_responsible_structures.id_for_label }}]').prop('hidden', !parseBool(this.value));
});
onRadioChange(['suspended_structures'], function() {
var field = document.getElementById('{{ form.rigging_plan.id_for_label }}');
field.hidden = !parseBool(this.value);
field.required = parseBool(this.value);
document.querySelector('label[for={{ form.rigging_plan.id_for_label }}]').hidden = !parseBool(this.value);
$('input[type=radio][name=suspended_structures]').change(function() {
$('#{{ form.rigging_plan.id_for_label }}').prop('hidden', !parseBool(this.value)).prop('required', parseBool(this.value));
$('label[for={{ form.rigging_plan.id_for_label }}]').prop('hidden', !parseBool(this.value));
});
</script>
{% endblock %}
@@ -50,13 +42,13 @@
<div class="col">
{% if form.errors %}
<div class="alert alert-danger alert-dismissable">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<dl>
{% with form|nice_errors as qq %}
{% for error_name,desc in qq.items %}
{% if error_name == 'General form errors' %}
{{ form.non_field_errors.0|safe }}
<div class="text-end">
<div class="text-right">
{% render_field form.supervisor_consulted class+="form-check-input" form="form" %}<label class="form-check-label" for="{{ form.supervisor_consulted.id_for_label}}">Please confirm you've done so.</label><br>
<button class="btn btn-primary mt-2" type="submit" form="form"><span class="fas fa-thumbs-up"></span> Confirm</button>
</div>
@@ -106,7 +98,7 @@
<label for="{{ form.power_mic.id_for_label }}"
class="col col-form-label">{{ form.power_mic.help_text|safe }}</label>
<div class="col-6">
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="ts-select" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
<select id="{{ form.power_mic.id_for_label }}" name="{{ form.power_mic.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
{% if power_mic %}
<option value="{{form.power_mic.value}}" selected="selected">{{ power_mic }}</option>
{% endif %}
@@ -170,19 +162,8 @@
</div>
</div>
</div>
<div class="row my-3">
<div class="col-12">
<div class="card">
<div class="card-header">Venue Access</div>
<div class="card-body">
<p><strong>If yes to the below, ensure you have communicated with the client and secured all necessary access prior to the event commencing.</strong></p>
{% include 'partials/yes_no_radio.html' with formitem=form.parking_and_access %}
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-sm-12 text-end">
<div class="col-sm-12 text-right">
<div class="btn-group">
<button type="submit" class="btn btn-primary" title="Save"><i
class="fas fa-save"></i> Save

View File

@@ -8,7 +8,7 @@
<p>Are you sure you wish to delete invoice {{ object.pk }}?</p>
<div class="text-end">
<div class="text-right">
<form action="{{ action_link }}" method="post">
{% csrf_token %}
<input type="hidden" name="next" value="{% url 'invoice_list' %}"/>

View File

@@ -1,106 +0,0 @@
{% extends 'base_rigs.html' %}
{% load humanize %}
{% block content %}
<form method="GET" action="{% url 'invoice_dashboard' %}">
<div class="row g-2">
<div class="mb-3 col-md-4">
<label for="time_filter">Time Filter</label>
<select id="time_filter" name="time_filter" class="form-control">
<option value="week" {% if time_filter == 'week' %}selected{% endif %}>Last Week (7 days)</option>
<option value="month" {% if time_filter == 'month' %}selected{% endif %}>Last Month (30 days)</option>
<option value="year" {% if time_filter == 'year' %}selected{% endif %}>Last Year</option>
<option value="all" {% if time_filter == 'all' %}selected{% endif %}>All Time</option>
</select>
</div>
</div>
</form>
<script>
document.getElementById('time_filter').addEventListener('change', function () {
this.closest('form').submit();
});
</script>
<h3>Overview</h3>
<!-- big cards in 2x2 grid with total_outstanding, total_events, total_invoices and total_payments, different backgrounds -->
<div class="row row-cols-1 row-cols-md-4 g-3">
<div class="card">
<a href="{% url 'invoice_waiting' %}" class="text-decoration-none text-white">
<div class="card-body bg-primary">
<h5 class="card-title text-center">Total Waiting</h5>
<p class="card-text text-center h3"><strong>£{{ total_waiting|floatformat:"2g" }}</strong></p>
</div>
</a>
</div>
<div class="card">
<a href="{% url 'invoice_list' %}" class="text-decoration-none text-dark">
<div class="card-body bg-info">
<h5 class="card-title text-center">Total Outstanding</h5>
<p class="card-text text-center h3"><strong>£{{ total_outstanding|floatformat:"2g" }}</strong></p>
</div>
</a>
</div>
<div class="card">
<div class="card-body bg-danger">
<h5 class="card-title text-center">Total Events</h5>
<p class="card-text text-center h3"><strong>{{ total_events }}</strong></p>
</div>
</div>
<div class="card">
<div class="card-body bg-success">
<h5 class="card-title text-center">Total Invoices</h5>
<p class="card-text text-center h3"><strong>{{ total_invoices }}</strong></p>
</div>
</div>
</div>
<br />
<h3>Payments</h3>
<br/>
<h4>Sources</h4>
<br/>
{% for source in payment_methods %}
<div class="card">
<div class="card-body">
<h5 class="card-title"><strong>{{ source.method }}</strong></h5>
<p class="card-text h3">£{{ source.total|floatformat:"2g" }}</p>
</div>
</div>
{% endfor %}
<br/>
<h4>Total</h4>
<br/>
<div class="card">
<div class="card-body">
<h5 class="card-title text-center">Total Income</h5>
<p class="card-text text-center h3"><strong>£{{ total_income|floatformat:"2g" }}</strong></p>
</div>
</div>
<br/>
<h4>Invoice Payment Time</h4>
<br/>
<div class="card">
<div class="card-body">
<h5 class="card-title text-center">Average Time to Pay</h5>
<p class="card-text text-center h3"><strong>{{ mean_invoice_to_payment|floatformat:"2g" }} days</strong></p>
</div>
</div>
{% endblock %}

View File

@@ -3,7 +3,7 @@
{% block content %}
<div class="row py-4">
<div class="col-sm-12 text-end px-0">
<div class="col-sm-12 text-right px-0">
<div class="btn-group">
<a href="{% url 'event_detail' object.event.pk %}" class="btn btn-primary">Open Event Page <span class="fas fa-eye"></span></a>
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-danger" title="Delete Invoice">
@@ -37,10 +37,10 @@
<div class="col-sm-6">
<div class="card card-default">
<div class="card-body">
<div class="text-end py-3">
<div class="text-right py-3">
<a href="{% url 'payment_create' %}?invoice={{ object.pk }}"
class="btn btn-success modal-href"
data-select-target="#{{ form.person.id_for_label }}">
data-target="#{{ form.person.id_for_label }}">
<span class="fas fa-plus"></span> Add
</a>
</div>
@@ -65,7 +65,7 @@
</tr>
{% endfor %}
<tr>
<td class="text-end"><strong>Balance:</strong></td>
<td class="text-right"><strong>Balance:</strong></td>
<td>{{ object.balance|floatformat:2 }}</td>
<td></td>
<td></td>
@@ -83,7 +83,7 @@
</div>
</div>
</div>
<div class="col-12 text-end">
<div class="col-12 text-right">
{% include 'partials/last_edited.html' with target="invoice_history" %}
</div>
{% endblock %}

View File

@@ -48,7 +48,7 @@
<span class="text-muted">{{ invoice.event.purchase_order }}</span>
{% endif %}
</td>
<td class="text-end">
<td class="text-right">
{% button 'view' url='invoice_detail' pk=invoice.pk %}
</td>
</tr>

View File

@@ -3,6 +3,12 @@
{% load static %}
{% block js %}
<script src="{% static "js/tooltip.js" %}"></script>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{% endblock %}
{% block content %}
@@ -25,7 +31,7 @@
{% for event in object_list %}
<tr class="{{event.status_color}}">
<th scope="row"><a href="{% url 'event_detail' event.pk %}">{{ event.display_id }}</a><br>
<span class="{% if event.get_status_display == 'Cancelled' %}text-danger{% endif %}">{{ event.get_status_display }}</span></th>
<span class="text-muted">{{ event.get_status_display }}</span></th>
<td>{{ event.start_date }}</td>
<td>
{{ event.name }}
@@ -59,7 +65,7 @@
<span class="fas fa-exclamation"></span>
{% endif %}
</td>
<td class="text-end">
<td class="text-right">
<div class="btn-group">
<a href="{% url 'invoice_event' event.pk %}"
class="btn btn-primary">

View File

@@ -3,7 +3,7 @@
<div class="card-header">Associated Organisations</div>
<ul class="list-group list-group-flush">
{% for organisation,count in object.organisations %}
<a class="list-group-item list-group-item-action" href="{% url 'organisation_detail' organisation.pk %}">{{ organisation.pk|stringformat:"05d" }} | {{ organisation.name }} <span class="badge text-bg-secondary" title="{{count}} events with {{object.name}} for {{organisation.name}}">{{count}}</span></a>
<a class="list-group-item list-group-item-action" href="{% url 'organisation_detail' organisation.pk %}">{{ organisation.pk|stringformat:"05d" }} | {{ organisation.name }} <span class="badge badge-secondary" title="{{count}} events with {{object.name}} for {{organisation.name}}">{{count}}</span></a>
{% endfor %}
</ul>
</div>

View File

@@ -3,7 +3,7 @@
<div class="card-header">Associated People</div>
<div class="list-group list-group-flush">
{% for person,count in object.persons %}
<a class="list-group-item list-group-item-action" href="{% url 'person_detail' person.pk %}">{{ person.pk|stringformat:"05d" }} | {{ person.name }} <span class="badge text-bg-secondary" title="{{count}} events with {{person.name}} for {{object.name}}">{{count}}</span></a>
<a class="list-group-item list-group-item-action" href="{% url 'person_detail' person.pk %}">{{ person.pk|stringformat:"05d" }} | {{ person.name }} <span class="badge badge-secondary" title="{{count}} events with {{person.name}} for {{object.name}}">{{count}}</span></a>
{% empty %}
<div class="card-body">None</div>
{% endfor %}

View File

@@ -23,7 +23,7 @@
</div>
{% endif %}
{% if object.organisation %}
<div class="card card-default mb-3">
<div class="card card-default">
<div class="card-header">Organisation Details</div>
<div class="card-body">
<dl class="row">

View File

@@ -1,50 +0,0 @@
{% load button from filters %}
{% if event.can_check_in %}
<div class="col-sm-12">
<div class="card mt-3">
<div class="card-header">Crew Record</div>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Vehicle</th>
<th scope="col">Start Time</th>
<th scope="col">Role</th>
<th scope="col">End Time</th>
<th scope="col">{% if request.user.pk is event.mic.pk %}<a
href="{% url 'event_checkin_override' event.pk %}" class="btn btn-sm btn-success"><span
class="fas fa-plus"></span> Add</a>{% endif %}</th>
</tr>
</thead>
<tbody id="crewmembers">
{% for crew in event.crew.all %}
<tr>
<td>{{crew.person}}</td>
<td>{{crew.vehicle|default:"None"}}</td>
<td>{{crew.time}}</td>
<td>{{crew.role}}</td>
<td>{% if crew.end_time %}
{{crew.end_time}}
{% else %}
<span class="text-success fas fa-clock" data-bs-toggle="tooltip"
title="This person is currently checked into this event"></span>{% endif %}
</td>
<td>{% if crew.end_time %}
{% if crew.person.pk == request.user.pk or event.mic.pk == request.user.pk %}
{% button 'edit' 'edit_checkin' crew.pk clazz='btn-sm modal-href' %}
{% endif %}
{% endif %}</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center bg-warning">Apparently this event happened by magic...</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}

View File

@@ -1,6 +1,6 @@
<h5 class="py-3"><a class="btn btn-info" data-bs-toggle="collapse" href="#values" aria-expanded="false" aria-controls="values">View Threshold Values</a></h5>
<h5 class="py-3"><a class="btn btn-info" data-toggle="collapse" href="#values" aria-expanded="false" aria-controls="values">View Threshold Values</a></h5>
<div class="row collapse" id="values">
<div class="col-md-6 col-sm-12">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
@@ -33,23 +33,20 @@
<thead>
<tr>
<th scope="row">Distro</th>
<th scope="row">Max PSCC with Single Phase Supply (kA)</th>
<th scope="row">Max PSCC with Three Phase Supply (kA)</th>
<th scope="row">Max PSSC (kA)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Intel & Toblerone distros</td>
<td>6</td>
<td>3</td>
</tr>
<tr>
<td>All other distros</td>
<td>10</td>
<td>5</td>
</tr>
</tbody>
</table>
<p><strong>Voltage Drop on Circuit:</strong> &#8804;5% (approx. 12v)</p>
<p><strong>Voltage Drop on Circuit:</strong> 5% (approx. 12v)</p>
</div>
</div>

View File

@@ -48,14 +48,6 @@
<span class="d-none d-sm-inline">Invoice</span></a>
{% endif %}
<a href="https://nottinghamtec.sharepoint.com/:l:/s/Office/JAAo7384Ro8-TKhPoZMLLMLNASrutYAeLOeQYPZO8DT-Zog?nav=MDkwZTAwN2YtODVkZi00YzBhLWFhMzEtNGYxNTc4MzgyYzRj" class="btn btn-danger"><span class="fas fa-file-invoice-dollar"></span> <span class="d-none d-sm-inline">Subhire Insurance Form</span></a>
{% if event.can_check_in %}
{% if request.user.current_event %}
<a href="{% url 'event_checkout' %}" class="btn btn-warning">Check Out</a>
{% else %}
<a href="{% url 'event_checkin' event.pk %}" class="btn btn-success modal-href"><span class="fas fa-user-clock"></span> <span class="d-none d-sm-inline">Check In</span></a>
{% endif %}
{% endif %}
<a href="https://docs.google.com/forms/d/e/1FAIpQLSf-TBOuJZCTYc2L8DWdAaC3_Werq0ulsUs8-6G85I6pA9WVsg/viewform" class="btn btn-danger"><span class="fas fa-file-invoice-dollar"></span> <span class="d-none d-sm-inline">Subhire Insurance Form</span></a>
{% endif %}
</div>

View File

@@ -11,7 +11,6 @@
{{ object.venue|namewithnotes:'venue_detail' }}
</a>
{% endif %}
{% if object.parking_and_access or object.riskassessment.parking_and_access %}<span class="badge text-bg-warning">Additional Access Requirements</span>{% endif %}
</dd>
{% if object.venue %}
<dt class="col-sm-6">Venue Notes</dt>
@@ -78,15 +77,6 @@
<dt class="col-sm-6">PO</dt>
<dd class="col-sm-6">{{ object.purchase_order }}</dd>
{% endif %}
<dt class="col-6">Forum Thread</dt>
{% if object.forum_url %}
<dd class="col-6"><a href="{{object.forum_url}}">{{object.forum_url}}</a></dd>
{% else %}
<a href="{% url 'event_thread' object.pk %}" class="btn btn-primary" title="Create Forum Thread" target="_blank"><span
class="fas fa-plus"></span> <span
class="d-none d-sm-inline">Create Forum Thread</span></a>
{% endif %}
</dl>
</div>
</div>

View File

@@ -1,147 +0,0 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<div class="card h-100 border-3 {{ border_class }} event-row">
<div class="card-header {{ header_bg }} {{ header_text }} py-3">
<div class="d-flex justify-content-between align-items-center">
<span class="d-flex align-items-center">
<h5 class="mb-0 me-3">
<a href="{% url 'event_detail' event.pk %}"
class="{{ header_text }} text-decoration-underline fw-bold">
<strong>{{ event.display_id }}</strong> - {{ event.name }}
</a>
</h5>
{% if event.dry_hire %}
<span class="badge px-3 py-2 rounded-pill fs-6 text-dark bg-light">Dry Hire</span>
{% endif %}
</span>
<span class="badge fs-6 px-3 py-2 bg-light text-dark rounded-pill">{{ event.get_status_display }}</span>
</div>
</div>
<div class="card-body">
<div class="row align-items-start">
<div class="col-md-2 border-end event-dates">
<div class="mb-2">
<small class="text-muted">Meet at:</small>
{% if event.meet_at %}
<p class="mb-1">{{ event.meet_at|date:"D j M Y, H:i" }}</p>
{% else %}
<p class="mb-1">Not specified</p>
{% endif %}
</div>
<div class="mb-2">
<small class="text-muted">Access from:</small>
{% if event.access_at %}
<p class="mb-1">{{ event.access_at|date:"D j M Y, H:i" }}</p>
{% else %}
<p class="mb-1">Not specified</p>
{% endif %}
</div>
<div class="mb-2">
<small class="text-muted">Start:</small>
<p class="mb-1">
{% if event.start_date and event.start_time %}
{{ event.start_date|date:"D j M Y" }}, {{ event.start_time|date:"H:i" }}
{% elif event.start_date %}
{{ event.start_date|date:"D j M Y" }}
{% elif event.start_time %}
{{ event.start_time|date:"H:i" }}
{% else %}
Not specified
{% endif %}
</p>
</div>
<div class="mb-2">
<small class="text-muted">End:</small>
<p class="mb-1">
{% if event.end_date and event.end_time %}
{{ event.end_date|date:"D j M Y" }}, {{ event.end_time|date:"H:i" }}
{% elif event.end_date %}
{{ event.end_date|date:"D j M Y" }}
{% elif event.end_time %}
{{ event.end_time|date:"H:i" }}
{% else %}
Not specified
{% endif %}
</p>
</div>
</div>
<div class="col-md-10">
<div class="row">
<div class="col-md-6">
{% if event.venue %}
<div class="mb-3">
<small class="text-muted">Venue:</small>
<p class="mb-1">{{ event.venue|namewithnotes:'venue_detail' }}</p>
</div>
{% endif %}
{% if event.is_rig %}
<div class="mb-3">
<small class="text-muted">Client:</small>
<p class="mb-1">
{% if event.person %}
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
{% if event.organisation %}
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation }}</a>
{% endif %}
{% elif event.organisation %}
<a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation }}</a>
{% else %}
No client specified
{% endif %}
</p>
</div>
{% endif %}
{% if event.mic or event.needs_mic %}
<div class="mb-3">
<small class="text-muted">Member in Charge (MIC):</small>
<div class="d-flex align-items-center mt-1">
{% if event.mic %}
<img src="{{ event.mic.profile_picture }}" alt="{{ event.mic.name }}"
class="rounded-circle me-1" width="32" height="32">
<span>
{% if perms.RIGS.view_profile %}
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
{% endif %}
{{ event.mic.name }}
{% if perms.RIGS.view_profile %}
</a>
{% endif %}
</span>
{% else %}
<span class="text-danger">No MIC assigned</span>
{% endif %}
</div>
</div>
{% endif %}
</div>
<div class="col-md-6">
<div class="mb-3">
<small class="text-muted">Description:</small>
<p class="mb-1">{{ event.description|markdown }}</p>
</div>
<div class="mb-3">
<small class="text-muted">Status:</small>
<div class="mt-1">
{% include "partials/event_status.html" with status=event.status %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
{% if object.event_size == 2 %}
<span class="badge text-bg-danger p-2 my-3">Large Event</span>
<span class="badge badge-danger p-2 my-3">Large Event</span>
{% elif object.event_size == 1 %}
<span class="badge text-bg-warning p-2 my-3">Medium Event</span>
<span class="badge badge-warning p-2 my-3">Medium Event</span>
{%else%}
<span class="badge text-bg-success p-2 my-3">Small Event</span>
<span class="badge badge-success p-2 my-3">Small Event</span>
{%endif%}

View File

@@ -1,57 +1,46 @@
<div id="event_status">
<span class="badge text-bg-{% if event.confirmed %}success{% elif event.cancelled %}dark{% else %}warning{% endif %}">Status: {{ event.get_status_display }}</span>
<div>
<span class="badge badge-{% if event.confirmed %}success{% elif event.cancelled %}dark{% else %}warning{% endif %}">Status: {{ event.get_status_display }}</span>
{% if event.is_rig %}
{% if event.sum_total > 0 %}
{% if event.organisation.union_account %}
{% if event.authorised %}
<span class="badge text-bg-success">Authorisation: Complete <span class="fas fa-check"></span></span>
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
<span class="badge text-bg-warning"> Authorisation: Issue <span class="fas fa-exclamation-circle"></span></span>
{% elif event.auth_request_to %}
<span class="badge text-bg-info"> Authorisation: Sent <span class="fas fa-paper-plane"></span></span>
{% else %}
<span class="badge text-bg-danger">Authorisation: <span class="fas fa-times"></span></span>
{% endif %}
{% if event.purchase_order %}
<span class="badge badge-success">PO: {{ event.purchase_order }}</span>
{% elif event.authorised %}
<span class="badge badge-success">Authorisation: Complete <span class="fas fa-check"></span></span>
{% elif event.authorisation and event.authorisation.amount != event.total and event.authorisation.last_edited_at > event.auth_request_at %}
<span class="badge badge-warning"> Authorisation: Issue <span class="fas fa-exclamation-circle"></span></span>
{% elif event.auth_request_to %}
<span class="badge badge-info"> Authorisation: Sent <span class="fas fa-paper-plane"></span></span>
{% else %}
{% if event.purchase_order %}
<span class="badge text-bg-success">PO: Received <span class="fas fa-check"></span></span>
{% else %}
<span class="badge text-bg-danger">PO: <span class="fas fa-times"></span></span>
{% endif %}
<span class="badge badge-danger">Authorisation: <span class="fas fa-times"></span></span>
{% endif %}
{% endif %}
{% if not event.dry_hire %}
{% if event.riskassessment %}
<a href="{{ event.riskassessment.get_absolute_url }}"><span class="badge text-bg-success">RA: <span class="fas fa-check{% if event.riskassessment.reviewed_by %}-double{%endif%}"></span></a>
<a href="{{ event.riskassessment.get_absolute_url }}"><span class="badge badge-success">RA: <span class="fas fa-check{% if event.riskassessment.reviewed_by %}-double{%endif%}"></span></a>
{% else %}
<span class="badge text-bg-danger">RA: <span class="fas fa-times"></span></span>
<span class="badge badge-danger">RA: <span class="fas fa-times"></span></span>
{% endif %}
{% if event.has_checklist %}
<span class="badge text-bg-success">Checklist: <span class="fas fa-check"></span> {% if event.checklists.count > 1 %}({{event.checklists.count}}){% endif %}</span>
{% endif %}
{% if not event.dry_hire %}
{% if event.hs_done %}
{# TODO Display status of all checklists #}
<span class="badge badge-success">Checklist: <span class="fas fa-check"></span></span>
{% else %}
<span class="badge text-bg-danger">Checklist: <span class="fas fa-times"></span></span>
{% endif %}
{% if event.has_power %}
<span class="badge text-bg-success">Power Record: <span class="fas fa-check"></span> {% if event.power_tests.count > 1 %}({{event.power_tests.count}}){% endif %}</span>
{% else %}
<span class="badge text-bg-danger">Power Record: <span class="fas fa-times"></span></span>
<span class="badge badge-danger">Checklist: <span class="fas fa-times"></span></span>
{% endif %}
{% endif %}
{% if perms.RIGS.view_invoice %}
{% if event.invoice %}
{% if event.invoice.void %}
<span class="badge text-bg-secondary">Invoice: Void</span>
<span class="badge badge-secondary">Invoice: Void</span>
{% elif event.invoice.is_closed %}
<span class="badge text-bg-success">Invoice: Paid</span>
<span class="badge badge-success">Invoice: Paid</span>
{% else %}
<span class="badge text-bg-warning">Invoice: Outstanding</span>
<span class="badge badge-warning">Invoice: Outstanding</span>
{% endif %}
{% else %}
<span class="badge text-bg-info">Invoice: Not Generated</span>
<span class="badge badge-info">Invoice: Not Generated</span>
{% endif %}
{% endif %}
{% if event.parking_and_access %}
<span class="badge text-bg-warning">Addititional Access Requirements</span>
{% endif %}
{% endif %}
</div>

View File

@@ -1,70 +1,105 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<style>
.light-link {
color: #ebf5ff !important;
}
.dark-link {
color: #0b3a8c !important;
}
.link-on-green {
color: #ffffff !important;
}
</style>
<div class="row">
{% for event in events %}
<div class="col-12 mb-4">
{% comment %} Determine card style based on event status {% endcomment %}
{% if event.cancelled %}
{% with border_class="border-secondary" header_bg="bg-secondary" header_text="light-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% elif not event.is_rig %}
{% with border_class="border-primary" header_bg="bg-primary" header_text="light-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% elif not event.mic %}
{% with border_class="border-danger" header_bg="bg-danger" header_text="light-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% elif event.confirmed and event.authorised %}
{% if event.dry_hire or event.riskassessment %}
{% with border_class="border-success" header_bg="bg-success" header_text="link-on-green" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% else %}
{% with border_class="border-warning" header_bg="bg-warning" header_text="dark-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% endif %}
{% else %}
{% with border_class="border-warning" header_bg="bg-warning" header_text="dark-link" %}
{% include "partials/event_row.html" %}
{% endwith %}
{% endif %}
</div>
{% empty %}
<div class="col-12">
<div class="alert alert-info">
No events currently scheduled.
</div>
</div>
{% endfor %}
<div class="table-responsive">
<table class="table mb-0" id="event_table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Dates & Times</th>
<th scope="col">Event Details</th>
<th scope="col">MIC</th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr class="{% if event.cancelled %}
table-secondary
{% elif not event.is_rig %}
table-info
{% elif not event.mic %}
table-danger
{% elif event.confirmed and event.authorised %}
{% if event.dry_hire or event.riskassessment %}
table-success
{% else %}
table-warning
{% endif %}
{% else %}
table-warning
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<!---Number-->
<th scope="row" id="event_number">{{ event.display_id }}</th>
<!--Dates & Times-->
<td id="event_dates" style="text-align: justify;">
{% if not event.cancelled %}
{% if event.meet_at %}
<span class="text-nowrap">Meet: <strong>{{ event.meet_at|date:"D d/m/Y H:i" }}</strong></span>
{% endif %}
{% if event.access_at %}
<br><span class="text-nowrap">Access: <strong>{{ event.access_at|date:"D d/m/Y H:i" }}</strong></span>
{% endif %}
{% endif %}
<span class="text-nowrap">Start: <strong>{{ event.start_date|date:"D d/m/Y" }}
{% if event.has_start_time %}
{{ event.start_time|date:"H:i" }}
{% endif %}</strong>
</span>
{% if event.end_date %}
<br>
<span class="text-nowrap">End: {% if event.end_date != event.start_date %}<strong>{{ event.end_date|date:"D d/m/Y" }}{% endif %}
{% if event.has_end_time %}
{{ event.end_time|date:"H:i" }}
{% endif %}</strong>
</span>
{% endif %}
</td>
<!---Details-->
<td id="event_details" class="w-100">
<h4>
<a href="{% url 'event_detail' event.pk %}">
{{ event.name }}
</a>
{% if event.venue %}
<small>at {{ event.venue|namewithnotes:'venue_detail' }}</small>
{% endif %}
{% if event.dry_hire %}
<span class="badge badge-secondary">Dry Hire</span>
{% endif %}
</h4>
{% if event.is_rig and not event.cancelled %}
<h5>
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
{% if event.organisation %}
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
{% endif %}
</h5>
{% endif %}
{% if not event.cancelled and event.description %}
<p>{{ event.description|markdown }}</p>
{% endif %}
{% include 'partials/event_status.html' %}
</td>
<!---MIC-->
<td id="event_mic" class="text-nowrap">
{% if event.mic %}
{% if perms.RIGS.view_profile %}
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
{% endif %}
<img src="{{ event.mic.profile_picture }}" class="event-mic-photo"/>
{{ event.mic }}
{% if perms.RIGS.view_profile %}
</a>
{% endif %}
{% elif event.is_rig %}
<span class="fas fa-user-slash"></span>
{% endif %}
</td>
</tr>
{% empty %}
<tr class="bg-warning">
<td colspan="4">No events found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View File

@@ -10,18 +10,10 @@
<hr>
<h5>Event Checklists:</h5>
{% for checklist in event.checklists.all %}
{% include 'partials/hs_status.html' with event=event object=checklist view='ec_detail' edit='ec_edit' create='event_ec' review='ec_review' perm=perms.RIGS.review_eventchecklist %}
<br/>
{% endfor %}
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info mt-2"><span class="fas fa-paperclip"></span> <span
class="d-none d-sm-inline">Create</span></a>
<hr>
<h5>Power Test Records:</h5>
{% for record in event.power_tests.all %}
{% include 'partials/hs_status.html' with event=event object=record view='pt_detail' edit='pt_edit' create='event_pt' review='pt_review' perm=perms.RIGS.review_power %}
<br/>
{% endfor %}
<a href="{% url 'event_pt' event.pk %}" class="btn btn-info mt-2"><span class="fas fa-paperclip"></span> <span
class="d-none d-sm-inline">Create</span></a>
{% include 'partials/hs_status.html' with event=event object=checklist view='ec_detail' edit='ec_edit' create='event_ec' review='ec_review' perm=perms.RIGS.review_eventchecklist %}
<br/>
{% endfor %}
<a href="{% url 'event_ec' event.pk %}" class="btn btn-info mt-2"><span class="fas fa-paperclip"></span> <span
class="hidden-xs">Create</span></a>
</div>
</div>

View File

@@ -7,5 +7,5 @@
{% include 'partials/review_status.html' %}
{% elif event != None %}
<a href="{% url create event.pk %}" class="btn btn-info"><span class="fas fa-paperclip"></span> <span
class="d-none d-sm-inline">Create</span></a>
class="hidden-xs">Create</span></a>
{% endif %}

View File

@@ -3,27 +3,29 @@
<div class="modal-content">
<div class="modal-header">
<h4>{{ object.name|default:"New Event" }}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="item-form">
<div class="modal-body">
<div class="mb-3 row g-2">
<div class="form-group form-row">
<label for="item_name" class="col-sm-2 col-form-label">Item Name</label>
<div class="col-sm-10">
<input type="text" placeholder="Item Name" class="form-control" required maxlength="255"
id="item_name"/>
</div>
</div>
<div class="mb-3 row g-2" data-bs-toggle="tooltip" title="A detailed description of the kit. MD enabled.">
<div class="form-group form-row" data-toggle="tooltip" title="A detailed description of the kit. MD enabled.">
<label for="item_description" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<textarea type="text" placeholder="Description" class="form-control md-enabled"
id="item_description" rows="8"></textarea>
</div>
</div>
<div class="row g-2">
<div class="form-row">
<div class="col-sm-6">
<div class="mb-3 row g-2">
<div class="form-group form-row">
<label for="item_quantity" class="col-sm-4 col-form-label">Quantity</label>
<div class="col-sm-8">
<input type="number" placeholder="Quantity" class="form-control" required
@@ -32,12 +34,12 @@
</div>
</div>
<div class="col-sm-6">
<div class="mb-3 row g-2">
<div class="form-group form-row">
<label for="item_cost" class="col-sm-4 col-form-label">Cost</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-text">£</span>
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
<input type="number" placeholder="Cost" class="form-control" required
min="-99999999.99" max="99999999.99" step="0.01"
id="item_cost"/>

View File

@@ -14,11 +14,11 @@
<td>£<span class="sub-total" data-subtotal="{{item.total_cost}}">{{item.total_cost|floatformat:2}}</span></td>
{% endif %}
{% if edit %}
<td class="vert-align text-end">
<td class="vert-align text-right">
<div class="btn-group" role="group" aria-label="Action buttons">
<button type="button" class="item-edit btn btn-sm btn-warning"
data-pk="{{item.pk}}"
data-modal-target="#itemModal">
data-toggle="modal" data-target="#itemModal">
<span class="fas fa-edit"></span>
</button>
<button type="button" class="item-delete btn btn-sm btn-danger"

View File

@@ -11,9 +11,10 @@
<th scope="col">Sub-total</th>
{% endif %}
{% if edit %}
<th scope="col" class="text-end align-self-start">
<th scope="col" class="text-right align-self-start">
<button type="button" class="btn btn-success btn-sm item-add"
data-modal-target="#itemModal">
data-toggle="modal"
data-target="#itemModal">
<i class="fas fa-plus"></i> Add Item
</button>
</th>
@@ -64,11 +65,11 @@
<td class="quantity"></td>
<td>£<span class="sub-total"></span></td>
{% if edit %}
<td class="vert-align text-end">
<td class="vert-align text-right">
<div class="btn-group" role="group" aria-label="Action buttons">
<button type="button" class="item-edit btn btn-sm btn-warning"
data-pk="{{item.pk}}"
data-modal-target="#itemModal">
data-toggle="modal" data-target="#itemModal">
<span class="fas fa-edit"></span>
</button>
<button type="button" class="item-delete btn btn-sm btn-danger"

View File

@@ -1,195 +0,0 @@
{% load namewithnotes from filters %}
{% load markdown_tags %}
<style>
#event_table {
display: grid;
grid-template-columns: max-content min-content minmax(max-content, 1fr) max-content;
column-gap: 1em;
}
.eventgrid {
display: inherit;
grid-column: 1/5;
grid-template-columns: subgrid;
padding: 1em;
dt, dd { display: block; float: left; }
dt { clear: both; }
dd { float: right; }
}
.grid-header {
border-bottom: 1px solid grey;
border-top: 1px solid grey;
}
#event_status {
grid-column-start: 3;
}
#event_mic {
grid-row-start: 1;
grid-column-start: 4;
}
.c-none {
display: none;
}
.c-inline {
display: inline;
}
@container (width <= 500px) {
#event_table {
grid-template-columns: 1fr !important;
}
.eventgrid {
grid-column: 1/1 !important;
padding: 0.5em;
}
.grid-header {
display: none;
}
#event_dates {
order: 2;
}
#event_status {
order: 3;
}
#event_mic {
grid-row-start: auto;
grid-column-start: 4;
}
}
@container (width <= 700px) {
#event_table {
grid-template-columns: max-content;
column-gap: 0.5em;
}
.eventgrid {
grid-column: 1/3;
border: 1px solid grey;
}
#event_dates {
grid-row: 2;
grid-column: 1;
}
#event_number {
grid-row: 1;
grid-column: 1;
}
#event_mic {
grid-column: 2;
}
#event_status {
grid-column: span 2;
}
.grid-header, .c-md-none {
display: none;
}
}
@container (width > 700px) {
.c-lg-block {
display: block;
}
.c-lg-inline {
display: inline;
}
.c-lg-none, .c-md-none {
display: none;
}
}
</style>
<div id="event_table">
<div class="eventgrid grid-header fw-bold">
<div id="event_number">#</div>
<div id="event_dates">Dates & Times</div>
<div>Event Details</div>
<div id="event_mic">MIC</div>
</div>
{% for event in events %}
<div class="eventgrid {% if event.cancelled %}
table-secondary
{% elif not event.is_rig %}
table-info
{% elif not event.mic %}
table-danger
{% elif event.confirmed and event.authorised %}
{% if event.dry_hire or event.riskassessment %}
table-success
{% else %}
table-warning
{% endif %}
{% else %}
table-warning
{% endif %}" {% if event.cancelled %}style="opacity: 50% !important;"{% endif %} id="event_row">
<!---Number-->
<div class="fw-bold c-none c-lg-block" id="event_number">{{ event.display_id }}</div>
<!--Dates & Times-->
<div id="event_dates" style="min-width: 180px;">
<dl>
{% if not event.cancelled %}
{% if event.meet_at %}
<dt class="fw-normal">Meet:</dt>
<dd class="text-nowrap fw-bold text-lg-right">{{ event.meet_at|date:"D d/m/Y H:i" }}</dd>
{% endif %}
{% if event.access_at %}
<dt class="fw-normal">Access:</dt>
<dd class="text-nowrap fw-bold text-lg-right">{{ event.access_at|date:"D d/m/Y H:i" }}</dd>
{% endif %}
{% endif %}
<dt class="fw-normal">Start:</dt>
<dd class="text-nowrap fw-bold text-lg-right">{{ event.start_date|date:"D d/m/Y" }}
{% if event.has_start_time %}
{{ event.start_time|date:"H:i" }}
{% endif %}
</dd>
{% if event.end_date %}
<dt class="fw-normal">End:</dt>
<dd class="text-nowrap fw-bold text-lg-right">{{ event.end_date|date:"D d/m/Y" }}
{% if event.has_end_time %}
{{ event.end_time|date:"H:i" }}
{% endif %}
</dd>
{% endif %}
</dl>
</div>
<!---Details-->
<div id="event_details" class="w-100">
<h4>
<a href="{% url 'event_detail' event.pk %}">
<span class="c-inline c-lg-none">{{ event }}</span><span class="c-none c-lg-inline">{{ event.name }}</span>
</a>
{% if event.dry_hire %}
<span class="badge text-bg-secondary">Dry Hire</span>
{% endif %}
<br class="c-none c-lg-inline">
{% if event.venue %}
<small>at {{ event.venue|namewithnotes:'venue_detail' }}</small>
{% endif %}
</h4>
{% if event.is_rig and not event.cancelled %}
<h5>
<a href="{{ event.person.get_absolute_url }}">{{ event.person.name }}</a>
{% if event.organisation %}
for <a href="{{ event.organisation.get_absolute_url }}">{{ event.organisation.name }}</a>
{% endif %}
</h5>
{% endif %}
{% if not event.cancelled and event.description %}
<p>{{ event.description|markdown }}</p>
{% endif %}
</div>
{% include 'partials/event_status.html' %}
<!---MIC-->
<div id="event_mic" class="text-nowrap">
<span class="c-md-none align-middle">MIC:</span>
{% if event.mic %}
{% if perms.RIGS.view_profile %}
<a href="{% url 'profile_detail' event.mic.pk %}" class="modal-href">
{% endif %}
<img src="{{ event.mic.profile_picture }}" class="event-mic-photo"/>
{{ event.mic }}
{% if perms.RIGS.view_profile %}
</a>
{% endif %}
{% elif event.is_rig %}
<span class="fas fa-exclamation"></span>
{% endif %}
</div>
</div>
{% endfor %}
</div>

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