diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..ab4728cf
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,30 @@
+# 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
diff --git a/.env.example b/.env.example
new file mode 100644
index 00000000..4635ea30
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,27 @@
+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=
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..ba2f192e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,32 @@
+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"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..0fea0e31
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,120 @@
+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@v4
+ 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 }}
+
+ docker:
+ name: Docker image
+ runs-on: ubuntu-latest
+ needs: [lint, test]
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-qemu-action@v3
+
+ - uses: docker/setup-buildx-action@v3
+
+ # 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@v3
+ 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
+
+ - name: Build (and push on master)
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ platforms: linux/amd64,linux/arm64
+ push: ${{ github.event_name == 'push' }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
diff --git a/.github/workflows/combine-prs.yml b/.github/workflows/combine-prs.yml
index c0a09e05..1d50ee6f 100644
--- a/.github/workflows/combine-prs.yml
+++ b/.github/workflows/combine-prs.yml
@@ -115,7 +115,7 @@ jobs:
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) {
@@ -133,7 +133,7 @@ jobs:
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;
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
deleted file mode 100644
index 621e01d2..00000000
--- a/.github/workflows/deploy.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: Manual Deploy
-
-on: workflow_dispatch
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: akhileshns/heroku-deploy@v3.12.12 # This is the action
- with:
- heroku_api_key: ${{secrets.HEROKU_API_KEY}}
- heroku_app_name: "pyrigs" #Must be unique in Heroku
- heroku_email: "aj@aronajones.com"
diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml
deleted file mode 100644
index 107e68a2..00000000
--- a/.github/workflows/django.yml
+++ /dev/null
@@ -1,59 +0,0 @@
-name: Django CI
-
-on:
- push:
- branches: [master]
- pull_request:
- branches: [master]
-
-jobs:
- build:
- if: "!contains(github.event.head_commit.message, '[ci skip]')"
- runs-on: ubuntu-latest
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- PYTHONDONTWRITEBYTECODE: 1
- steps:
- - uses: actions/checkout@v4
- - name: Install build dependencies
- run: |
- sudo apt-get install libcairo2-dev
-
- - name: "Set up Python"
- uses: actions/setup-python@v5
- with:
- python-version-file: ".python-version"
-
- - name: Install uv
- uses: astral-sh/setup-uv@v6
-
- - name: Install Dependencies
- run: uv sync --locked --all-extras --dev
-
- - name: Cache Static Files
- id: static-cache
- uses: actions/cache@v4
- 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: |
- uv run pycodestyle . --exclude=.venv,migrations,node_modules
- uv run python3 manage.py check
- uv run python3 manage.py makemigrations --check --dry-run
- uv run python3 manage.py collectstatic --noinput
- - name: Run Tests
- run: uv run pytest -n auto --cov
- - uses: actions/upload-artifact@v4
- if: failure()
- with:
- name: failure-screenshots ${{ matrix.test-group }}
- path: screenshots/
- retention-days: 5
- - name: Coveralls
- run: uv run coveralls --service=github
diff --git a/.gitignore b/.gitignore
index 6a078ac5..45899c8b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -104,3 +104,15 @@ screenshots/
# Virutal Environments
.venv/
+
+# Environment variable files (keep .env.example)
+.env
+.env.*
+!.env.example
+
+nginx/certs/
+nginx/certbot/
+nginx/*.conf
+!nginx/default.dev.conf
+*.pem
+*.key
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..1aa750d9
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,38 @@
+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]
diff --git a/.python-version b/.python-version
index c8cfe395..6324d401 100644
--- a/.python-version
+++ b/.python-version
@@ -1 +1 @@
-3.10
+3.14
diff --git a/.slugignore b/.slugignore
deleted file mode 100644
index be7815d3..00000000
--- a/.slugignore
+++ /dev/null
@@ -1,6 +0,0 @@
-*.sqlite3
-*.md
-**/tests
-conftest.py
-pytest.ini
-Dockerfile
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..87811728
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,68 @@
+# Stage 1: build frontend assets (multi-arch: official node image)
+FROM node:24-slim AS assets
+WORKDIR /app
+
+COPY package.json package-lock.json ./
+RUN npm ci
+
+COPY gulpfile.js ./
+COPY pipeline/source_assets ./pipeline/source_assets
+RUN npm run build
+
+# Stage 2: build the Python environment (multi-arch: official python image)
+FROM python:3.14-slim-trixie AS builder
+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 apt-get update \
+ && apt-get install -y --no-install-recommends build-essential pkg-config libcairo2-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+# Set up py environment
+# DEBUG must never be on in a built image; enable it explicitly via the environment if needed
+ENV DEBUG=false \
+ PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ UV_COMPILE_BYTECODE=1 \
+ UV_LINK_MODE=copy
+
+# Copy uv project files first (for better caching)
+COPY pyproject.toml uv.lock ./
+
+# Install the project's dependencies using the lockfile and settings
+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
+
+# Then, add the rest of the project source code and install it
+# Installing separately from its dependencies allows optimal layer caching
+COPY . /app
+COPY --from=assets /app/pipeline/built_assets /app/pipeline/built_assets
+RUN --mount=type=cache,target=/root/.cache/uv \
+ uv sync --frozen --no-dev
+
+# Placeholder values only satisfy settings that are mandatory when DEBUG is off; they are not kept in the image
+RUN EMAIL_HOST=build EMAIL_HOST_USER=build EMAIL_HOST_PASSWORD=build EMAIL_FROM=build@example.com \
+ uv run python manage.py collectstatic --noinput
+
+FROM python:3.14-slim-trixie
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends libcairo2 \
+ && rm -rf /var/lib/apt/lists/*
+RUN addgroup --system app \
+ && adduser --system --group --home /home/app app \
+ && mkdir -p /home/app \
+ && chown app:app /home/app
+COPY --from=builder --chown=app:app /app /app
+WORKDIR /app
+ENV DEBUG=false \
+ PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1
+ENV PATH="/app/.venv/bin:$PATH"
+
+USER app
+EXPOSE 8000
+CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "PyRIGS.wsgi"]
diff --git a/LICENSE b/LICENSE
index f080ee7b..7b88283e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -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.
\ No newline at end of file
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Procfile b/Procfile
deleted file mode 100644
index 91e43543..00000000
--- a/Procfile
+++ /dev/null
@@ -1,2 +0,0 @@
-release: python manage.py migrate
-web: gunicorn PyRIGS.wsgi --log-file -
diff --git a/PyRIGS/decorators.py b/PyRIGS/decorators.py
index fe7d1825..c6b88e66 100644
--- a/PyRIGS/decorators.py
+++ b/PyRIGS/decorators.py
@@ -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,6 +44,7 @@ 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):
@@ -54,9 +55,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
@@ -86,10 +87,10 @@ def api_key_required(function):
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:
@@ -99,7 +100,7 @@ def api_key_required(function):
try:
user_object = models.Profile.objects.get(pk=userid)
- kwargs = {**kwargs, 'user': user_object}
+ kwargs = {**kwargs, "user": user_object}
except models.Profile.DoesNotExist:
return error_resp
@@ -117,8 +118,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)
diff --git a/PyRIGS/formats/en/formats.py b/PyRIGS/formats/en/formats.py
index 0c2f21d6..5d25c48d 100644
--- a/PyRIGS/formats/en/formats.py
+++ b/PyRIGS/formats/en/formats.py
@@ -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"
diff --git a/PyRIGS/settings.py b/PyRIGS/settings.py
index fa992c5a..03bb4fbf 100644
--- a/PyRIGS/settings.py
+++ b/PyRIGS/settings.py
@@ -10,7 +10,6 @@ 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
@@ -20,235 +19,231 @@ 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)
+DEBUG = env("DEBUG", cast=bool, default=True)
+STAGING = env("STAGING", cast=bool, default=False)
+CI = env("CI", cast=bool, default=False)
-ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
-
-if STAGING:
- ALLOWED_HOSTS.append('.herokuapp.com')
+ALLOWED_HOSTS = [
+ host.strip() for host in env("DJANGO_ALLOWED_HOSTS", default="rigs.nottinghamtec.co.uk").split(",") if host.strip()
+]
if DEBUG:
- ALLOWED_HOSTS.append('localhost')
- ALLOWED_HOSTS.append('example.com')
- ALLOWED_HOSTS.append('127.0.0.1')
- ALLOWED_HOSTS.append('.app.github.dev')
- CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
+ 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]
-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')
+DOMAIN = env("DOMAIN", default="example.com")
-ADMINS = [('IT Manager', f'it@{DOMAIN}'), ('Arona Jones', f'arona.jones@{DOMAIN}')]
+ADMINS = [("IT Manager", f"it@{DOMAIN}")]
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',
-
- # 'debug_toolbar',
- '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",
+ "registration",
+ "reversion",
+ "widget_tweaks",
+ "hcaptcha",
+ "massadmin",
)
MIDDLEWARE = (
- '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',
+ "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",
)
-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.sqlite3',
- 'NAME': str(BASE_DIR / 'db.sqlite3'),
+ "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),
}
}
-if not DEBUG:
- import dj_database_url
-
- if env("FRANKENRIGS_DATABASE_URL") is not None:
- DATABASES['default'] = dj_database_url.config(env="FRANKENRIGS_DATABASE_URL")
- else:
- 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'
-
-USE_L10N = True
+FORMAT_MODULE_PATH = "PyRIGS.formats"
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)
-STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
-STATIC_URL = '/static/'
-STATIC_ROOT = str(BASE_DIR / 'static/')
+STORAGES = {
+ "default": {
+ "BACKEND": "django.core.files.storage.FileSystemStorage",
+ },
+ "staticfiles": {
+ "BACKEND": "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",
@@ -257,8 +252,9 @@ TEMPLATES = [
"django.template.context_processors.tz",
"django.template.context_processors.request",
"django.contrib.messages.context_processors.messages",
+ "PyRIGS.views.ajax_context",
],
- 'debug': DEBUG
+ "debug": DEBUG,
},
},
]
@@ -266,13 +262,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'
-
-SECURE_HSTS_SECONDS = 3600
-SECURE_HSTS_INCLUDE_SUBDOMAINS = True
-SECURE_CONTENT_TYPE_NOSNIFF = True
-SESSION_COOKIE_SECURE = env('SESSION_COOKIE_SECURE_ENABLED', True)
-CSRF_COOKIE_SECURE = env('CSRF_COOKIE_SECURE_ENABLED', True)
-SECURE_HSTS_PRELOAD = True
+DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
diff --git a/PyRIGS/tests/base.py b/PyRIGS/tests/base.py
index bcf1824c..2f96865c 100644
--- a/PyRIGS/tests/base.py
+++ b/PyRIGS/tests/base.py
@@ -1,16 +1,8 @@
-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
@@ -23,6 +15,7 @@ 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")
@@ -30,76 +23,30 @@ 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(f"Error in test {screenshot_name} is at path {screenshot_file}", file=sys.stderr)
- raise e
-
- return wrapper_func
-
-
-def screenshot_failure_cls(cls):
- for attr in cls.__dict__:
- if callable(getattr(cls, attr)) and attr.startswith("test"):
- setattr(cls, attr, screenshot_failure(getattr(cls, attr)))
- return cls
-
-
def assert_times_almost_equal(first_time, second_time):
assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0)
def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url):
# Test the meta tag is in place
- response = client.get(event_url, follow=True, HTTP_HOST='example.com')
- assertContains(response, 'application/json+oembed')
+ 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
diff --git a/PyRIGS/tests/pages.py b/PyRIGS/tests/pages.py
deleted file mode 100644
index dec5e20e..00000000
--- a/PyRIGS/tests/pages.py
+++ /dev/null
@@ -1,86 +0,0 @@
-from pypom import Page
-from selenium.common.exceptions import NoSuchElementException
-from selenium.webdriver.common.action_chains import ActionChains
-from selenium.webdriver.common.by import By
-
-from PyRIGS.tests import regions
-
-
-class BasePage(Page):
- form_items = {}
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- def __getattr__(self, name):
- if name in self.form_items:
- element = self.form_items[name]
- form_element = element[0](self, self.find_element(*element[1]))
- return form_element.value
- else:
- return super().__getattribute__(name)
-
- def __setattr__(self, name, value):
- if name in self.form_items:
- element = self.form_items[name]
- form_element = element[0](self, self.find_element(*element[1]))
- form_element.set_value(value)
- else:
- self.__dict__[name] = value
-
-
-class FormPage(BasePage):
- _errors_selector = (By.CLASS_NAME, "alert-danger")
- _submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]")
-
- def remove_all_required(self):
- self.driver.execute_script(
- "Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
- self.driver.execute_script(
- "Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});")
-
- def submit(self):
- previous_errors = self.errors
- submit = self.find_element(*self._submit_locator)
- ActionChains(self.driver).move_to_element(submit).perform()
- submit.click()
- self.wait.until(animation_is_finished())
- self.wait.until(lambda x: self.errors != previous_errors or self.success)
-
- @property
- def errors(self):
- try:
- error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector))
- return error_page.errors
- except NoSuchElementException:
- return None
-
-
-class LoginPage(BasePage):
- URL_TEMPLATE = '/user/login'
-
- _username_locator = (By.ID, 'id_username')
- _password_locator = (By.ID, 'id_password')
- _submit_locator = (By.ID, 'id_submit')
- _error_locator = (By.CSS_SELECTOR, '.errorlist>li')
-
- def login(self, username, password):
- username_element = self.find_element(*self._username_locator)
- username_element.clear()
- username_element.send_keys(username)
-
- password_element = self.find_element(*self._password_locator)
- password_element.clear()
- password_element.send_keys(password)
-
- self.find_element(*self._submit_locator).click()
-
-
-class animation_is_finished():
- def __call__(self, driver):
- number_animating = driver.execute_script('return $(":animated").length')
- finished = number_animating == 0
- if finished:
- import time
- time.sleep(0.1)
- return finished
diff --git a/PyRIGS/tests/regions.py b/PyRIGS/tests/regions.py
deleted file mode 100644
index 75afbe81..00000000
--- a/PyRIGS/tests/regions.py
+++ /dev/null
@@ -1,262 +0,0 @@
-import datetime
-
-from django.conf import settings
-from pypom import Region
-from selenium.common.exceptions import NoSuchElementException
-from selenium.webdriver.common.by import By
-from selenium.webdriver.common.keys import Keys
-from selenium.webdriver.support import expected_conditions
-from selenium.webdriver.support.select import Select
-
-
-def parse_bool_from_string(string):
- # Used to convert from attribute strings to boolean values, written after I found this:
- # >>> bool("false")
- # True
- if string == "true":
- return True
- else:
- return False
-
-
-def get_time_format():
- # Default
- time_format = "%H%M"
- if settings.CI: # The CI is American
- time_format = "%I%M%p"
- return time_format
-
-
-def get_date_format():
- date_format = "%d%m%Y"
- if settings.CI: # And try as I might I can't stop it being so
- date_format = "%m%d%Y"
- return date_format
-
-
-class BootstrapSelectElement(Region):
- _main_button_locator = (By.CSS_SELECTOR, 'button.dropdown-toggle')
- _option_box_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu')
- _option_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu.inner>li>a.dropdown-item')
- _select_all_locator = (By.CLASS_NAME, 'bs-select-all')
- _deselect_all_locator = (By.CLASS_NAME, 'bs-deselect-all')
- _search_locator = (By.CSS_SELECTOR, '.bs-searchbox>input')
- _status_locator = (By.CLASS_NAME, 'status')
-
- @property
- def is_open(self):
- return parse_bool_from_string(self.find_element(*self._main_button_locator).get_attribute("aria-expanded"))
-
- def toggle(self):
- original_state = self.is_open
- option_box = self.find_element(*self._option_box_locator)
- if not original_state:
- self.wait.until(expected_conditions.invisibility_of_element(option_box))
- else:
- self.wait.until(expected_conditions.visibility_of(option_box))
- return self.find_element(*self._main_button_locator).click()
-
- def open(self):
- if not self.is_open:
- self.toggle()
-
- def close(self):
- if self.is_open:
- self.toggle()
-
- def select_all(self):
- self.find_element(*self._select_all_locator).click()
-
- def deselect_all(self):
- self.find_element(*self._deselect_all_locator).click()
-
- def search(self, query):
- # self.wait.until(expected_conditions.visibility_of_element_located(self._status_locator))
- search_box = self.find_element(*self._search_locator)
- self.open()
- search_box.clear()
- search_box.send_keys(query)
- self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator))
-
- @property
- def options(self):
- options = list(self.find_elements(*self._option_locator))
- return [self.BootstrapSelectOption(self, i) for i in options]
-
- def set_option(self, name, selected):
- options = [x for x in self.options if x.name == name]
- assert len(options) == 1
- options[0].set_selected(selected)
-
- class BootstrapSelectOption(Region):
- _text_locator = (By.CLASS_NAME, 'text')
-
- @property
- def selected(self):
- return parse_bool_from_string(self.root.get_attribute("aria-selected"))
-
- def toggle(self):
- self.root.click()
-
- def set_selected(self, selected):
- if self.selected != selected:
- self.toggle()
-
- @property
- def name(self):
- return self.find_element(*self._text_locator).text
-
-
-class TextBox(Region):
- @property
- def value(self):
- return self.root.get_attribute("value")
-
- def set_value(self, value):
- self.root.clear()
- self.root.send_keys(value)
-
-
-class SimpleMDETextArea(Region):
- @property
- def value(self):
- return self.driver.execute_script("return document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().getValue();", self.root.get_attribute("id"))
-
- def set_value(self, value):
- self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
-
-
-class CheckBox(Region):
- def toggle(self):
- self.root.click()
-
- @property
- def value(self):
- return parse_bool_from_string(self.root.get_attribute("checked"))
-
- def set_value(self, value):
- if value != self.value:
- self.toggle()
-
-
-class RadioSelect(Region): # Currently only works for yes/no radio selects
- def set_value(self, value):
- if value:
- value = "0"
- else:
- value = "1"
- self.find_element(By.XPATH, f"//label[@for='{self.root.get_attribute('id')}_{value}']").click()
-
- @property
- def value(self):
- try:
- return parse_bool_from_string(self.find_element(By.CSS_SELECTOR, '.custom-control-input:checked').get_attribute("value").lower())
- except NoSuchElementException:
- return None
-
-
-class DatePicker(Region):
- @property
- def value(self):
- return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d")
-
- def set_value(self, value):
- self.root.clear()
- self.root.send_keys(value.strftime(get_date_format()))
-
-
-class TimePicker(Region):
- @property
- def value(self):
- return datetime.datetime.strptime(self.root.get_attribute("value"), "%H:%M")
-
- def set_value(self, value):
- self.root.clear()
- self.root.send_keys(value.strftime(get_time_format()))
-
-
-class DateTimePicker(Region):
- @property
- def value(self):
- return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d %H:%M")
-
- def set_value(self, value):
- self.root.clear()
-
- date = value.date().strftime(get_date_format())
- time = value.time().strftime(get_time_format())
-
- self.root.send_keys(date)
- self.root.send_keys(Keys.TAB)
- self.root.send_keys(time)
-
-
-class SingleSelectPicker(Region):
- @property
- def value(self):
- picker = Select(self.root)
- return picker.first_selected_option.text
-
- def set_value(self, value):
- picker = Select(self.root)
- picker.select_by_visible_text(value)
-
-
-class ErrorPage(Region):
- _error_item_selector = (By.CSS_SELECTOR, "dl>span")
-
- class ErrorItem(Region):
- _field_selector = (By.CSS_SELECTOR, "dt")
- _error_selector = (By.CSS_SELECTOR, "dd>ul>li")
-
- @property
- def field_name(self):
- return self.find_element(*self._field_selector).text
-
- @property
- def errors(self):
- return [x.text for x in self.find_elements(*self._error_selector)]
-
- @property
- def errors(self):
- error_items = [self.ErrorItem(self, x) for x in self.find_elements(*self._error_item_selector)]
- errors = {}
- for error in error_items:
- errors[error.field_name] = error.errors
- return errors
-
-
-class Modal(Region):
- _submit_locator = (By.CSS_SELECTOR, '.btn-primary')
- _header_selector = (By.TAG_NAME, 'h4')
-
- form_items = {
- 'name': (TextBox, (By.ID, 'id_name'))
- }
-
- @property
- def header(self):
- return self.find_element(*self._header_selector).text
-
- @property
- def is_open(self):
- return self.root.is_displayed()
-
- def submit(self):
- self.root.find_element(*self._submit_locator).click()
-
- def __getattr__(self, name):
- if name in self.form_items:
- element = self.form_items[name]
- form_element = element[0](self, self.find_element(*element[1]))
- return form_element.value
- else:
- return super().__getattribute__(name)
-
- def __setattr__(self, name, value):
- if name in self.form_items:
- element = self.form_items[name]
- form_element = element[0](self, self.find_element(*element[1]))
- form_element.set_value(value)
- else:
- self.__dict__[name] = value
diff --git a/PyRIGS/tests/test_unit.py b/PyRIGS/tests/test_unit.py
index 12207d7d..bbd65dbe 100644
--- a/PyRIGS/tests/test_unit.py
+++ b/PyRIGS/tests/test_unit.py
@@ -1,21 +1,16 @@
import pytest
from django.core.management import call_command
-from django.template.defaultfilters import striptags
from django.urls import URLPattern, URLResolver
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from pytest_django.asserts import assertRedirects, assertContains, assertNotContains
-from pytest_django.asserts import assertTemplateUsed, assertInHTML
+from pytest_django.asserts import assertTemplateUsed
from PyRIGS import urls
-from RIGS.models import Event, Profile
+from RIGS.models import Event
from assets.models import Asset
-from training.tests.test_unit import get_response
-from django.db import connection
-from django.template.defaultfilters import striptags
-from django.urls.exceptions import NoReverseMatch
-from django.test import TestCase, TransactionTestCase
+from django.test import TestCase
from django.test.utils import override_settings
@@ -36,18 +31,28 @@ 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)
@@ -55,10 +60,10 @@ 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')
+ call_command("deleteSampleData")
assert not Asset.objects.all().exists()
assert not Event.objects.all().exists()
@@ -66,81 +71,82 @@ class TestSampleDataGenerator(TestCase):
@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}"
else:
expected_url = f"{reverse('login')}?next={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)
diff --git a/PyRIGS/urls.py b/PyRIGS/urls.py
index 03d42b50..93b6a513 100644
--- a/PyRIGS/urls.py
+++ b/PyRIGS/urls.py
@@ -1,43 +1,33 @@
from django.conf import settings
-from django.conf.urls import include
from django.contrib import admin
from django.contrib.auth.decorators import login_required
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
-from django.urls import path
+from django.urls import include, 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/ "+text+"{''.join([str(elem) for elem in unexpected_values])}
require consulting with a supervisor.", code='unusual_answers')
+ if len(unexpected_values) > 0 and not self.cleaned_data.get("supervisor_consulted"):
+ raise forms.ValidationError(
+ f"Your answers to these questions: {''.join([str(elem) for elem in unexpected_values])}
require consulting with a supervisor.",
+ code="unusual_answers",
+ )
return super().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()
related_models = {
- 'venue': models.Venue,
+ "venue": models.Venue,
}
class Meta:
model = models.EventChecklist
- fields = '__all__'
- exclude = ['reviewed_at', 'reviewed_by']
+ fields = "__all__"
+ exclude = ["reviewed_at", "reviewed_by"]
class PowerTestRecordForm(forms.ModelForm):
@@ -220,29 +263,29 @@ class PowerTestRecordForm(forms.ModelForm):
field.widget = forms.CheckboxInput()
related_models = {
- 'venue': models.Venue,
- 'power_mic': models.Profile,
+ "venue": models.Venue,
+ "power_mic": models.Profile,
}
class Meta:
model = models.PowerTestRecord
- fields = '__all__'
- exclude = ['reviewed_at', 'reviewed_by']
+ 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"
+ self.fields["time"].initial = timezone.now()
+ self.fields["role"].initial = "Crew"
class Meta:
model = models.EventCheckIn
- fields = '__all__'
- exclude = ['end_time']
+ fields = "__all__"
+ exclude = ["end_time"]
class EditCheckInForm(forms.ModelForm):
class Meta:
model = models.EventCheckIn
- fields = '__all__'
+ fields = "__all__"
diff --git a/RIGS/management/commands/deleteSampleData.py b/RIGS/management/commands/deleteSampleData.py
index 1e996a45..d5be2926 100644
--- a/RIGS/management/commands/deleteSampleData.py
+++ b/RIGS/management/commands/deleteSampleData.py
@@ -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)
diff --git a/RIGS/management/commands/generateSampleData.py b/RIGS/management/commands/generateSampleData.py
index 237c0cb0..9b7357e4 100644
--- a/RIGS/management/commands/generateSampleData.py
+++ b/RIGS/management/commands/generateSampleData.py
@@ -1,15 +1,13 @@
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")
diff --git a/RIGS/management/commands/generateSampleRIGSData.py b/RIGS/management/commands/generateSampleRIGSData.py
index 1292fb67..69db214d 100644
--- a/RIGS/management/commands/generateSampleRIGSData.py
+++ b/RIGS/management/commands/generateSampleRIGSData.py
@@ -1,7 +1,6 @@
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
@@ -11,7 +10,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 = []
@@ -25,13 +24,14 @@ 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,18 +39,72 @@ 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()))
@@ -72,32 +126,131 @@ 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()))
@@ -113,7 +266,9 @@ 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"
@@ -122,18 +277,76 @@ 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))
@@ -158,32 +371,81 @@ 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
@@ -222,9 +484,10 @@ 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)
@@ -253,39 +516,49 @@ 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(),
+ method=random.choice(models.Payment.METHODS)[0],
+ )
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)),
+ parking_and_access=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,
+ 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),
+ )
diff --git a/RIGS/management/commands/send_reminders.py b/RIGS/management/commands/send_reminders.py
index 4f38d734..467eae87 100644
--- a/RIGS/management/commands/send_reminders.py
+++ b/RIGS/management/commands/send_reminders.py
@@ -4,7 +4,7 @@ import datetime
from django.template.loader import get_template
from django.contrib.staticfiles import finders
from django.conf import settings
-from django.core.management.base import BaseCommand, CommandError
+from django.core.management.base import BaseCommand
from django.core.mail import EmailMultiAlternatives
from django.utils import timezone
from django.urls import reverse
@@ -13,17 +13,27 @@ from RIGS import models
class Command(BaseCommand):
- help = 'Sends email reminders as required. Triggered daily through heroku-scheduler in production.'
+ 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')
+ 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)))
+ 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'):
+ 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})
+ "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(
@@ -32,7 +42,11 @@ class Command(BaseCommand):
to=[target],
reply_to=[f"h.s.manager@{settings.DOMAIN}"],
)
- css = finders.find('css/email.css')
- html = premailer.Premailer(get_template("email/ra_reminder.html").render(context), external_styles=css).transform()
- msg.attach_alternative(html, 'text/html')
+ 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()
diff --git a/RIGS/migrations/0042_auto_20211007_2338.py b/RIGS/migrations/0042_auto_20211007_2338.py
index 20343cdf..4b83450e 100644
--- a/RIGS/migrations/0042_auto_20211007_2338.py
+++ b/RIGS/migrations/0042_auto_20211007_2338.py
@@ -31,4 +31,3 @@ class Migration(migrations.Migration):
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (ZS)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
),
]
-
diff --git a/RIGS/models.py b/RIGS/models.py
index ecdf96a2..2b564800 100644
--- a/RIGS/models.py
+++ b/RIGS/models.py
@@ -8,7 +8,7 @@ from urllib.parse import urlparse
import pytz
from django import forms
-from django.db.models import Q, F
+from django.db.models import Q
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
@@ -17,7 +17,6 @@ from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from reversion import revisions as reversion
-from reversion.models import Version
from versioning.versioning import RevisionMixin
@@ -34,9 +33,13 @@ def filter_by_pk(filt, query):
class Profile(AbstractUser):
initials = models.CharField(max_length=5, null=True, blank=False)
- phone = models.CharField(max_length=13, blank=True, default='')
- api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
- is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.")
+ phone = models.CharField(max_length=13, blank=True, default="")
+ api_key = models.CharField(max_length=40, blank=True, editable=False, default="")
+ is_approved = models.BooleanField(
+ default=False,
+ verbose_name="Approval Status",
+ help_text="Designates whether a staff member has approved this user.",
+ )
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
last_emailed = models.DateTimeField(blank=True, null=True)
dark_theme = models.BooleanField(default=False)
@@ -48,15 +51,18 @@ class Profile(AbstractUser):
def make_api_key(cls):
size = 20
chars = string.ascii_letters + string.digits
- new_api_key = ''.join(random.choice(chars) for x in range(size))
+ new_api_key = "".join(random.choice(chars) for x in range(size))
return new_api_key
@property
def profile_picture(self):
url = ""
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
- url = "https://www.gravatar.com/avatar/" + hashlib.md5(
- self.email.encode('utf-8')).hexdigest() + "?d=wavatar&s=500"
+ url = (
+ "https://www.gravatar.com/avatar/"
+ + hashlib.md5(self.email.encode("utf-8")).hexdigest()
+ + "?d=wavatar&s=500"
+ )
return url
@property
@@ -68,7 +74,11 @@ class Profile(AbstractUser):
@property
def latest_events(self):
- return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists')
+ return (
+ self.event_mic.order_by("-start_date")
+ .select_related("person", "organisation", "venue", "mic", "riskassessment", "invoice")
+ .prefetch_related("checklists")
+ )
@classmethod
def admins(cls):
@@ -77,22 +87,30 @@ class Profile(AbstractUser):
@classmethod
def users_awaiting_approval_count(cls):
# last_login = None ensures we only pick up genuinely new users, not those that have been deactivated for inactivity
- return Profile.objects.filter(is_approved=False, last_login=None, date_joined_date=timezone.now().date()).count()
+ return Profile.objects.filter(
+ is_approved=False, last_login=None, date_joined_date=timezone.now().date()
+ ).count()
def __str__(self):
return self.name
def current_event(self):
q = EventCheckIn.objects.filter(person=self, end_time=None)
- return q.latest('time') if q.exists() else None
+ return q.latest("time") if q.exists() else None
class ContactableManager(models.Manager):
def search(self, query=None):
qs = self.get_queryset()
if query is not None:
- or_lookup = Q(name__icontains=query) | Q(email__icontains=query) | Q(address__icontains=query) | Q(notes__icontains=query) | Q(
- phone__startswith=query) | Q(phone__endswith=query)
+ or_lookup = (
+ Q(name__icontains=query)
+ | Q(email__icontains=query)
+ | Q(address__icontains=query)
+ | Q(notes__icontains=query)
+ | Q(phone__startswith=query)
+ | Q(phone__endswith=query)
+ )
or_lookup = filter_by_pk(or_lookup, query)
@@ -102,10 +120,10 @@ class ContactableManager(models.Manager):
class Person(models.Model, RevisionMixin):
name = models.CharField(max_length=50)
- phone = models.CharField(max_length=15, blank=True, default='')
- email = models.EmailField(blank=True, default='')
- address = models.TextField(blank=True, default='')
- notes = models.TextField(blank=True, default='')
+ phone = models.CharField(max_length=15, blank=True, default="")
+ email = models.EmailField(blank=True, default="")
+ address = models.TextField(blank=True, default="")
+ notes = models.TextField(blank=True, default="")
objects = ContactableManager()
@@ -119,7 +137,7 @@ class Person(models.Model, RevisionMixin):
@property
def organisations(self):
o = []
- for e in Event.objects.filter(person=self).select_related('organisation'):
+ for e in Event.objects.filter(person=self).select_related("organisation"):
if e.organisation:
o.append(e.organisation)
@@ -130,18 +148,18 @@ class Person(models.Model, RevisionMixin):
@property
def latest_events(self):
- return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
+ return self.event_set.order_by("-start_date").select_related("person", "organisation", "venue", "mic")
def get_absolute_url(self):
- return reverse('person_detail', kwargs={'pk': self.pk})
+ return reverse("person_detail", kwargs={"pk": self.pk})
class Organisation(models.Model, RevisionMixin):
name = models.CharField(max_length=50)
- phone = models.CharField(max_length=15, blank=True, default='')
- email = models.EmailField(blank=True, default='')
- address = models.TextField(blank=True, default='')
- notes = models.TextField(blank=True, default='')
+ phone = models.CharField(max_length=15, blank=True, default="")
+ email = models.EmailField(blank=True, default="")
+ address = models.TextField(blank=True, default="")
+ notes = models.TextField(blank=True, default="")
union_account = models.BooleanField(default=False)
objects = ContactableManager()
@@ -156,7 +174,7 @@ class Organisation(models.Model, RevisionMixin):
@property
def persons(self):
p = []
- for e in Event.objects.filter(organisation=self).select_related('person'):
+ for e in Event.objects.filter(organisation=self).select_related("person"):
if e.person:
p.append(e.person)
@@ -167,10 +185,10 @@ class Organisation(models.Model, RevisionMixin):
@property
def latest_events(self):
- return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
+ return self.event_set.order_by("-start_date").select_related("person", "organisation", "venue", "mic")
def get_absolute_url(self):
- return reverse('organisation_detail', kwargs={'pk': self.pk})
+ return reverse("organisation_detail", kwargs={"pk": self.pk})
class VatManager(models.Manager):
@@ -201,8 +219,8 @@ class VatRate(models.Model, RevisionMixin):
return self.rate * 100
class Meta:
- ordering = ['-start_at']
- get_latest_by = 'start_at'
+ ordering = ["-start_at"]
+ get_latest_by = "start_at"
def __str__(self):
return f"{self.comment} {self.start_at} @ {self.as_percent}%"
@@ -210,11 +228,11 @@ class VatRate(models.Model, RevisionMixin):
class Venue(models.Model, RevisionMixin):
name = models.CharField(max_length=255)
- phone = models.CharField(max_length=15, blank=True, default='')
- email = models.EmailField(blank=True, default='')
+ phone = models.CharField(max_length=15, blank=True, default="")
+ email = models.EmailField(blank=True, default="")
three_phase_available = models.BooleanField(default=False)
- notes = models.TextField(blank=True, default='')
- address = models.TextField(blank=True, default='')
+ notes = models.TextField(blank=True, default="")
+ address = models.TextField(blank=True, default="")
objects = ContactableManager()
@@ -226,68 +244,85 @@ class Venue(models.Model, RevisionMixin):
@property
def latest_events(self):
- return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
+ return self.event_set.order_by("-start_date").select_related("person", "organisation", "venue", "mic")
def get_absolute_url(self):
- return reverse('venue_detail', kwargs={'pk': self.pk})
+ return reverse("venue_detail", kwargs={"pk": self.pk})
class EventManager(models.Manager):
def current_events(self):
- events = self.filter(
- (models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q(
- status=Event.CANCELLED)) | # Starts after with no end
- (models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(
- status=Event.CANCELLED)) | # Ends after
- (models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(
- status=Event.CANCELLED)) | # Active dry hire
- (models.Q(dry_hire=True, checked_in_by__isnull=True) & (
- models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
- models.Q(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started
- ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
+ events = (
+ self.filter(
+ (
+ models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False)
+ & ~models.Q(status=Event.CANCELLED)
+ ) # Starts after with no end
+ | (
+ models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(status=Event.CANCELLED)
+ ) # Ends after
+ | (
+ models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(status=Event.CANCELLED)
+ ) # Active dry hire
+ | (
+ models.Q(dry_hire=True, checked_in_by__isnull=True)
+ & (models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))
+ ) # Active dry hire GT
+ | models.Q(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started
+ )
+ .order_by("start_date", "end_date", "start_time", "end_time", "meet_at")
+ .select_related("person", "organisation", "venue", "mic")
+ )
return events
def events_in_bounds(self, start, end):
- events = self.filter(
- (models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds
- (models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds
- (models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds
- (models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds
-
- (models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after
- (models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after
- (models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after
- (models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after
- (models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after
-
- ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
- 'organisation',
- 'venue', 'mic')
+ events = (
+ self.filter(
+ (models.Q(start_date__gte=start.date(), start_date__lte=end.date())) # Start date in bounds
+ | (models.Q(end_date__gte=start.date(), end_date__lte=end.date())) # End date in bounds
+ | (models.Q(access_at__gte=start, access_at__lte=end)) # Access at in bounds
+ | (models.Q(meet_at__gte=start, meet_at__lte=end)) # Meet at in bounds
+ | (models.Q(start_date__lte=start, end_date__gte=end)) # Start before, end after
+ | (models.Q(access_at__lte=start, start_date__gte=end)) # Access before, start after
+ | (models.Q(access_at__lte=start, end_date__gte=end)) # Access before, end after
+ | (models.Q(meet_at__lte=start, start_date__gte=end)) # Meet before, start after
+ | (models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after
+ )
+ .order_by("start_date", "end_date", "start_time", "end_time", "meet_at")
+ .select_related("person", "organisation", "venue", "mic")
+ )
return events
def rig_count(self):
event_count = self.filter(
- (models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False,
- is_rig=True) & ~models.Q(
- status=Event.CANCELLED)) | # Starts after with no end
- (models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q(
- status=Event.CANCELLED)) | # Ends after
- (models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q(
- status=Event.CANCELLED)) # Active dry hire
+ (
+ models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False, is_rig=True)
+ & ~models.Q(status=Event.CANCELLED)
+ ) # Starts after with no end
+ | (
+ models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q(status=Event.CANCELLED)
+ ) # Ends after
+ | (
+ models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q(status=Event.CANCELLED)
+ ) # Active dry hire
).count()
return event_count
def waiting_invoices(self):
- events = self.filter(
- (
- models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
- models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before
- ) & models.Q(invoice__isnull=True) & # Has not already been invoiced
- models.Q(is_rig=True) # Is a rig (not non-rig)
- ).order_by('start_date') \
- .select_related('person', 'organisation', 'venue', 'mic') \
- .prefetch_related('items')
+ events = (
+ self.filter(
+ (
+ models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) # Starts before with no end
+ | models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before
+ )
+ & models.Q(invoice__isnull=True) # Has not already been invoiced
+ & models.Q(is_rig=True) # Is a rig (not non-rig)
+ )
+ .order_by("start_date")
+ .select_related("person", "organisation", "venue", "mic")
+ .prefetch_related("items")
+ )
return events
@@ -313,11 +348,11 @@ def validate_forum_url(value):
if not value:
return # Required error is done the field
obj = urlparse(value)
- if obj.hostname not in ('forum.nottinghamtec.co.uk'):
- raise ValidationError('URL must point to a location on the TEC Forum')
+ if obj.hostname not in ("forum.nottinghamtec.co.uk"):
+ raise ValidationError("URL must point to a location on the TEC Forum")
-@reversion.register(follow=['items'])
+@reversion.register(follow=["items"])
class Event(models.Model, RevisionMixin):
# Done to make it much nicer on the database
PROVISIONAL = 0
@@ -325,23 +360,24 @@ class Event(models.Model, RevisionMixin):
BOOKED = 2
CANCELLED = 3
EVENT_STATUS_CHOICES = (
- (PROVISIONAL, 'Provisional'),
- (CONFIRMED, 'Confirmed'),
- (BOOKED, 'Booked'),
- (CANCELLED, 'Cancelled'),
+ (PROVISIONAL, "Provisional"),
+ (CONFIRMED, "Confirmed"),
+ (BOOKED, "Booked"),
+ (CANCELLED, "Cancelled"),
)
name = models.CharField(max_length=255)
- person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE)
- organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE)
- venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE)
- description = models.TextField(blank=True, default='')
- notes = models.TextField(blank=True, default='')
+ person = models.ForeignKey("Person", null=True, blank=True, on_delete=models.CASCADE)
+ organisation = models.ForeignKey("Organisation", blank=True, null=True, on_delete=models.CASCADE)
+ venue = models.ForeignKey("Venue", blank=True, null=True, on_delete=models.CASCADE)
+ description = models.TextField(blank=True, default="")
+ notes = models.TextField(blank=True, default="")
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
dry_hire = models.BooleanField(default=False)
is_rig = models.BooleanField(default=True)
- based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True,
- null=True)
+ based_on = models.ForeignKey(
+ "Event", on_delete=models.SET_NULL, related_name="future_events", blank=True, null=True
+ )
# Timing
start_date = models.DateField()
@@ -355,21 +391,28 @@ class Event(models.Model, RevisionMixin):
parking_and_access = models.BooleanField(default=False)
# Crew management
- checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True,
- on_delete=models.CASCADE)
- mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True,
- verbose_name="MIC", on_delete=models.CASCADE)
+ checked_in_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL, related_name="event_checked_in", blank=True, null=True, on_delete=models.CASCADE
+ )
+ mic = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ related_name="event_mic",
+ blank=True,
+ null=True,
+ verbose_name="MIC",
+ on_delete=models.CASCADE,
+ )
# Monies
- purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO')
- collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by')
+ purchase_order = models.CharField(max_length=255, blank=True, default="", verbose_name="PO")
+ collector = models.CharField(max_length=255, blank=True, default="", verbose_name="collected by")
# Authorisation request details
- auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE)
+ auth_request_by = models.ForeignKey("Profile", null=True, blank=True, on_delete=models.CASCADE)
auth_request_at = models.DateTimeField(null=True, blank=True)
- auth_request_to = models.EmailField(blank=True, default='')
+ auth_request_to = models.EmailField(blank=True, default="")
- forum_url = models.URLField(default='', blank=True, validators=[validate_forum_url])
+ forum_url = models.URLField(default="", blank=True, validators=[validate_forum_url])
@property
def display_id(self):
@@ -391,9 +434,11 @@ class Event(models.Model, RevisionMixin):
@property
def sum_total(self):
total = self.items.aggregate(
- sum_total=models.Sum(models.F('cost') * models.F('quantity'),
- output_field=models.DecimalField(max_digits=10, decimal_places=2))
- )['sum_total']
+ sum_total=models.Sum(
+ models.F("cost") * models.F("quantity"),
+ output_field=models.DecimalField(max_digits=10, decimal_places=2),
+ )
+ )["sum_total"]
if total:
return total
return Decimal("0.00")
@@ -407,7 +452,7 @@ class Event(models.Model, RevisionMixin):
# No VAT is owed on internal transfers
if self.internal:
return 0
- return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01'))
+ return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal(".01"))
"""
Inc VAT
@@ -415,15 +460,15 @@ class Event(models.Model, RevisionMixin):
@property
def total(self):
- return Decimal(self.sum_total + self.vat).quantize(Decimal('.01'))
+ return Decimal(self.sum_total + self.vat).quantize(Decimal(".01"))
@property
def cancelled(self):
- return (self.status == self.CANCELLED)
+ return self.status == self.CANCELLED
@property
def confirmed(self):
- return (self.status == self.BOOKED or self.status == self.CONFIRMED)
+ return self.status == self.BOOKED or self.status == self.CONFIRMED
@property
def hs_done(self):
@@ -520,7 +565,7 @@ class Event(models.Model, RevisionMixin):
objects = EventManager()
def get_absolute_url(self):
- return reverse('event_detail', kwargs={'pk': self.pk})
+ return reverse("event_detail", kwargs={"pk": self.pk})
def __str__(self):
return f"{self.display_id} | {self.name}"
@@ -528,18 +573,26 @@ class Event(models.Model, RevisionMixin):
def clean(self):
errdict = {}
if self.end_date and self.start_date > self.end_date:
- errdict['end_date'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
+ errdict["end_date"] = ["Unless you've invented time travel, the event can't finish before it has started."]
startEndSameDay = not self.end_date or self.end_date == self.start_date
hasStartAndEnd = self.has_start_time and self.has_end_time
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
- errdict['end_time'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.']
+ errdict["end_time"] = ["Unless you've invented time travel, the event can't finish before it has started."]
if self.access_at is not None:
if self.access_at.date() > self.start_date:
- errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
- elif self.start_time is not None and self.start_date == self.access_at.date() and self.access_at.time() > self.start_time:
- errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.']
+ errdict["access_at"] = [
+ "Regardless of what some clients might think, access time cannot be after the event has started."
+ ]
+ elif (
+ self.start_time is not None
+ and self.start_date == self.access_at.date()
+ and self.access_at.time() > self.start_time
+ ):
+ errdict["access_at"] = [
+ "Regardless of what some clients might think, access time cannot be after the event has started."
+ ]
if errdict != {}: # If there was an error when validation
raise ValidationError(errdict)
@@ -552,9 +605,9 @@ class Event(models.Model, RevisionMixin):
@reversion.register
class EventItem(models.Model, RevisionMixin):
- event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE)
+ event = models.ForeignKey("Event", related_name="items", blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
- description = models.TextField(blank=True, default='')
+ description = models.TextField(blank=True, default="")
quantity = models.IntegerField()
cost = models.DecimalField(max_digits=10, decimal_places=2)
order = models.IntegerField()
@@ -566,7 +619,7 @@ class EventItem(models.Model, RevisionMixin):
return self.cost * self.quantity
class Meta:
- ordering = ['order']
+ ordering = ["order"]
def __str__(self):
return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}"
@@ -578,16 +631,16 @@ class EventItem(models.Model, RevisionMixin):
@reversion.register
class EventAuthorisation(models.Model, RevisionMixin):
- event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE)
+ event = models.OneToOneField("Event", related_name="authorisation", on_delete=models.CASCADE)
email = models.EmailField()
name = models.CharField(max_length=255)
- uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID")
- account_code = models.CharField(max_length=50, default='', blank=True)
+ uni_id = models.CharField(max_length=10, blank=True, default="", verbose_name="University ID")
+ account_code = models.CharField(max_length=50, default="", blank=True)
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount")
- sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE)
+ sent_by = models.ForeignKey("Profile", on_delete=models.CASCADE)
def get_absolute_url(self):
- return reverse('event_detail', kwargs={'pk': self.event_id})
+ return reverse("event_detail", kwargs={"pk": self.event_id})
@property
def activity_feed_string(self):
@@ -597,15 +650,17 @@ class EventAuthorisation(models.Model, RevisionMixin):
class InvoiceManager(models.Manager):
def outstanding_invoices(self):
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
- sql = "SELECT * FROM " \
- "(SELECT " \
- "(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
- "(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
- "(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
- "\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
- "AS sub " \
- "WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
- "ORDER BY invoice_date"
+ sql = (
+ "SELECT * FROM "
+ "(SELECT "
+ '(SELECT COUNT(p.amount) FROM "RIGS_payment" AS p WHERE p.invoice_id="RIGS_invoice".id) AS "payment_count", '
+ '(SELECT SUM(ei.cost * ei.quantity) FROM "RIGS_eventitem" AS ei WHERE ei.event_id="RIGS_invoice".event_id) AS "cost", '
+ '(SELECT SUM(p.amount) FROM "RIGS_payment" AS p WHERE p.invoice_id="RIGS_invoice".id) AS "payments", '
+ '"RIGS_invoice"."id", "RIGS_invoice"."event_id", "RIGS_invoice"."invoice_date", "RIGS_invoice"."void" FROM "RIGS_invoice") '
+ "AS sub "
+ "WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'"
+ "ORDER BY invoice_date"
+ )
query = self.raw(sql)
return query
@@ -639,13 +694,13 @@ class InvoiceManager(models.Manager):
return qs
-@reversion.register(follow=['payment_set'])
+@reversion.register(follow=["payment_set"])
class Invoice(models.Model, RevisionMixin):
- event = models.OneToOneField('Event', on_delete=models.CASCADE)
+ event = models.OneToOneField("Event", on_delete=models.CASCADE)
invoice_date = models.DateField(auto_now_add=True)
void = models.BooleanField(default=False)
- reversion_perm = 'RIGS.view_invoice'
+ reversion_perm = "RIGS.view_invoice"
objects = InvoiceManager()
@@ -659,7 +714,7 @@ class Invoice(models.Model, RevisionMixin):
@property
def payment_total(self):
- total = self.payment_set.aggregate(total=models.Sum('amount'))['total']
+ total = self.payment_set.aggregate(total=models.Sum("amount"))["total"]
if total:
return total
return Decimal("0.00")
@@ -673,7 +728,7 @@ class Invoice(models.Model, RevisionMixin):
return self.balance == 0 or self.void
def get_absolute_url(self):
- return reverse('invoice_detail', kwargs={'pk': self.pk})
+ return reverse("invoice_detail", kwargs={"pk": self.pk})
@property
def activity_feed_string(self):
@@ -687,26 +742,26 @@ class Invoice(models.Model, RevisionMixin):
return f"#{self.pk:05d}"
class Meta:
- ordering = ['-invoice_date']
+ ordering = ["-invoice_date"]
@reversion.register
class Payment(models.Model, RevisionMixin):
- CASH = 'C'
- INTERNAL = 'I'
- EXTERNAL = 'E'
- ADJUSTMENT = 'T'
+ CASH = "C"
+ INTERNAL = "I"
+ EXTERNAL = "E"
+ ADJUSTMENT = "T"
METHODS = (
- (CASH, 'Cash'),
- (INTERNAL, 'Internal'),
- (EXTERNAL, 'External'),
- (ADJUSTMENT, 'TEC Adjustment'),
+ (CASH, "Cash"),
+ (INTERNAL, "Internal"),
+ (EXTERNAL, "External"),
+ (ADJUSTMENT, "TEC Adjustment"),
)
- invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE)
+ invoice = models.ForeignKey("Invoice", on_delete=models.CASCADE)
date = models.DateField()
- amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT')
- method = models.CharField(max_length=2, choices=METHODS, default='', blank=True)
+ amount = models.DecimalField(max_digits=10, decimal_places=2, help_text="Please use ex. VAT")
+ method = models.CharField(max_length=2, choices=METHODS, default="", blank=True)
reversion_hide = True
@@ -722,98 +777,164 @@ def validate_url(value):
if not value:
return # Required error is done the field
obj = urlparse(value)
- if obj.hostname not in ('nottinghamtec.sharepoint.com'):
- raise ValidationError('URL must point to a location on the TEC Sharepoint')
+ if obj.hostname not in ("nottinghamtec.sharepoint.com"):
+ raise ValidationError("URL must point to a location on the TEC Sharepoint")
class ReviewableModel(models.Model):
reviewed_at = models.DateTimeField(null=True, blank=True)
- reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
- verbose_name="Reviewer", on_delete=models.CASCADE)
+ reviewed_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL, blank=True, null=True, verbose_name="Reviewer", on_delete=models.CASCADE
+ )
class Meta:
abstract = True
@cached_property
def fieldz(self):
- return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created]
+ return [
+ n.name
+ for n in list(self._meta.get_fields())
+ if n.name != "reviewed_at" and n.name != "reviewed_by" and not n.is_relation and not n.auto_created
+ ]
@reversion.register
class RiskAssessment(ReviewableModel, RevisionMixin):
- SMALL = (0, 'Small')
- MEDIUM = (1, 'Medium')
- LARGE = (2, 'Large')
+ SMALL = (0, "Small")
+ MEDIUM = (1, "Medium")
+ LARGE = (2, "Large")
SIZES = (SMALL, MEDIUM, LARGE)
- event = models.OneToOneField('Event', on_delete=models.CASCADE)
+ event = models.OneToOneField("Event", on_delete=models.CASCADE)
# General
- nonstandard_equipment = models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by "
- "TEC's standard risk assessments and method statements?")
- nonstandard_use = models.BooleanField(help_text="Are TEC using their equipment in a way that is abnormal?
i.e. Not covered by TECs standard health and safety documentation")
- contractors = models.BooleanField(help_text="Are you using any external contractors?
i.e. Freelancers/Crewing Companies")
- other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site?
e.g. TEC is providing the lighting while another company does sound")
+ nonstandard_equipment = models.BooleanField(
+ help_text="Does the event require any hired in equipment or use of equipment that is not covered by "
+ "TEC's standard risk assessments and method statements?"
+ )
+ nonstandard_use = models.BooleanField(
+ help_text="Are TEC using their equipment in a way that is abnormal?
i.e. Not covered by TECs standard health and safety documentation"
+ )
+ contractors = models.BooleanField(
+ help_text="Are you using any external contractors?
i.e. Freelancers/Crewing Companies"
+ )
+ other_companies = models.BooleanField(
+ help_text="Are TEC working with any other companies on site?
e.g. TEC is providing the lighting while another company does sound"
+ )
crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?")
- general_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
+ general_notes = models.TextField(
+ blank=True,
+ default="",
+ help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?",
+ )
# Power
- big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?")
- power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True,
- verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person must be a Power Technician or Power Supervisor)")
+ big_power = models.BooleanField(
+ help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?"
+ )
+ power_mic = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ related_name="power_mic",
+ blank=True,
+ null=True,
+ verbose_name="Power MIC",
+ on_delete=models.CASCADE,
+ help_text="Who is the Power MIC? (if yes to the above question, this person must be a Power Technician or Power Supervisor)",
+ )
outside = models.BooleanField(help_text="Is the event outdoors?")
generators = models.BooleanField(help_text="Will generators be used?")
other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?")
- nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?")
- multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?")
- power_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
- power_plan = models.URLField(blank=True, default='', help_text="Upload your power plan to the Sharepoint and submit a link", validators=[validate_url])
+ nonstandard_equipment_power = models.BooleanField(
+ help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?"
+ )
+ multiple_electrical_environments = models.BooleanField(
+ help_text="Will the electrical installation occupy more than one electrical environment?"
+ )
+ power_notes = models.TextField(
+ blank=True,
+ default="",
+ help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?",
+ )
+ power_plan = models.URLField(
+ blank=True,
+ default="",
+ help_text="Upload your power plan to the Sharepoint and submit a link",
+ validators=[validate_url],
+ )
# Sound
- noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?")
- sound_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?")
+ noise_monitoring = models.BooleanField(
+ help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?"
+ )
+ sound_notes = models.TextField(
+ blank=True,
+ default="",
+ help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?",
+ )
# Site
known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?")
- safe_loading = models.BooleanField(help_text="Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)")
+ safe_loading = models.BooleanField(
+ help_text="Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)"
+ )
safe_storage = models.BooleanField(help_text="Are there any problems with safe and secure equipment storage?")
- area_outside_of_control = models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")
- barrier_required = models.BooleanField(help_text="Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?")
- nonstandard_emergency_procedure = models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")
+ area_outside_of_control = models.BooleanField(
+ help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?"
+ )
+ barrier_required = models.BooleanField(
+ help_text="Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?"
+ )
+ nonstandard_emergency_procedure = models.BooleanField(
+ help_text="Does the emergency procedure for the event differ from TEC's standard procedures?"
+ )
# Structures
- special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?")
- suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")
- persons_responsible_structures = models.TextField(blank=True, default='', help_text="Who are the persons on site responsible for their use?")
- rigging_plan = models.URLField(blank=True, default='', help_text="Upload your rigging plan to the Sharepoint and submit a link", validators=[validate_url])
+ special_structures = models.BooleanField(
+ help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?"
+ )
+ suspended_structures = models.BooleanField(
+ help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?"
+ )
+ persons_responsible_structures = models.TextField(
+ blank=True, default="", help_text="Who are the persons on site responsible for their use?"
+ )
+ rigging_plan = models.URLField(
+ blank=True,
+ default="",
+ help_text="Upload your rigging plan to the Sharepoint and submit a link",
+ validators=[validate_url],
+ )
# Venue Access
- parking_and_access = models.BooleanField(help_text="Are there additional requirements for parking and access to the venue? (i.e. campus parking permits, event access wristbands)")
+ parking_and_access = models.BooleanField(
+ help_text="Are there additional requirements for parking and access to the venue? (i.e. campus parking permits, event access wristbands)"
+ )
# Blimey that was a lot of options
supervisor_consulted = models.BooleanField(null=True)
expected_values = {
- 'nonstandard_equipment': False,
- 'nonstandard_use': False,
- 'contractors': False,
- 'other_companies': False,
- 'crew_fatigue': False,
+ "nonstandard_equipment": False,
+ "nonstandard_use": False,
+ "contractors": False,
+ "other_companies": False,
+ "crew_fatigue": False,
# 'big_power': False Doesn't require checking with a super either way
- 'generators': False,
- 'other_companies_power': False,
- 'nonstandard_equipment_power': False,
- 'multiple_electrical_environments': False,
- 'noise_monitoring': False,
- 'known_venue': False,
- 'safe_loading': False,
- 'safe_storage': False,
- 'area_outside_of_control': False,
- 'barrier_required': False,
- 'nonstandard_emergency_procedure': False,
- 'special_structures': False,
- 'suspended_structures': False,
- 'parking_and_access': False
+ "generators": False,
+ "other_companies_power": False,
+ "nonstandard_equipment_power": False,
+ "multiple_electrical_environments": False,
+ "noise_monitoring": False,
+ "known_venue": False,
+ "safe_loading": False,
+ "safe_storage": False,
+ "area_outside_of_control": False,
+ "barrier_required": False,
+ "nonstandard_emergency_procedure": False,
+ "special_structures": False,
+ "suspended_structures": False,
+ "parking_and_access": False,
}
inverted_fields = {key: value for (key, value) in expected_values.items() if not value}.keys()
@@ -823,15 +944,18 @@ class RiskAssessment(ReviewableModel, RevisionMixin):
raise forms.ValidationError("Engage brain, please. No generators indoors!(!)")
class Meta:
- ordering = ['event']
- permissions = [
- ('review_riskassessment', 'Can review Risk Assessments')
- ]
+ ordering = ["event"]
+ permissions = [("review_riskassessment", "Can review Risk Assessments")]
@property
def event_size(self):
# Confirm event size. Check all except generators, since generators entails outside
- if self.outside or self.other_companies_power or self.nonstandard_equipment_power or self.multiple_electrical_environments:
+ if (
+ self.outside
+ or self.other_companies_power
+ or self.nonstandard_equipment_power
+ or self.multiple_electrical_environments
+ ):
return self.LARGE[0]
elif self.big_power:
return self.MEDIUM[0]
@@ -845,7 +969,7 @@ class RiskAssessment(ReviewableModel, RevisionMixin):
return f"{self.pk} | {self.event}"
def get_absolute_url(self):
- return reverse('ra_detail', kwargs={'pk': self.pk})
+ return reverse("ra_detail", kwargs={"pk": self.pk})
@property
def activity_feed_string(self):
@@ -858,29 +982,37 @@ class RiskAssessment(ReviewableModel, RevisionMixin):
@reversion.register
class EventChecklist(ReviewableModel, RevisionMixin):
- event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE)
+ event = models.ForeignKey("Event", related_name="checklists", on_delete=models.CASCADE)
# General
- venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
+ venue = models.ForeignKey("Venue", on_delete=models.CASCADE)
date = models.DateField()
# Safety Checks
- safe_parking = models.BooleanField(blank=True, null=True, help_text="Vehicles parked safely?
(does not obstruct venue access)")
- safe_packing = models.BooleanField(blank=True, null=True, help_text="Equipment packed away safely?
(including flightcases)")
+ safe_parking = models.BooleanField(
+ blank=True, null=True, help_text="Vehicles parked safely?
(does not obstruct venue access)"
+ )
+ safe_packing = models.BooleanField(
+ blank=True, null=True, help_text="Equipment packed away safely?
(including flightcases)"
+ )
exits = models.BooleanField(blank=True, null=True, help_text="Emergency exits clear?")
- trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?")
- warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place?
(strobe, smoke, power etc.)")
+ trip_hazard = models.BooleanField(
+ blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?"
+ )
+ warning_signs = models.BooleanField(
+ blank=True, help_text="Warning signs in place?
(strobe, smoke, power etc.)"
+ )
ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?")
- hs_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of Safety Bag/Box")
- extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers")
+ hs_location = models.CharField(blank=True, default="", max_length=255, help_text="Location of Safety Bag/Box")
+ extinguishers_location = models.CharField(
+ blank=True, default="", max_length=255, help_text="Location of fire extinguishers"
+ )
inverted_fields = []
class Meta:
- ordering = ['event']
- permissions = [
- ('review_eventchecklist', 'Can review Event Checklists')
- ]
+ ordering = ["event"]
+ permissions = [("review_eventchecklist", "Can review Event Checklists")]
def __str__(self):
return f"{self.pk} - {self.event}"
@@ -890,7 +1022,7 @@ class EventChecklist(ReviewableModel, RevisionMixin):
return str(self.event)
def get_absolute_url(self):
- return reverse('ec_detail', kwargs={'pk': self.pk})
+ return reverse("ec_detail", kwargs={"pk": self.pk})
@reversion.register
@@ -898,57 +1030,114 @@ class PowerTestRecord(ReviewableModel, RevisionMixin):
earth_fault_text = "Earth Fault Loop Impedance (ZS) / Ω"
pssc_text = "Prospective Short Circuit Current / A"
- event = models.ForeignKey('Event', related_name='power_tests', on_delete=models.CASCADE)
- power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists',
- verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?")
- venue = models.ForeignKey('Venue', on_delete=models.CASCADE)
- notes = models.TextField(blank=True, default='')
+ event = models.ForeignKey("Event", related_name="power_tests", on_delete=models.CASCADE)
+ power_mic = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ blank=True,
+ null=True,
+ related_name="checklists",
+ verbose_name="Power MIC",
+ on_delete=models.CASCADE,
+ help_text="Who is the Power MIC?",
+ )
+ venue = models.ForeignKey("Venue", on_delete=models.CASCADE)
+ notes = models.TextField(blank=True, default="")
# Small Electrical Checks
rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?")
- supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested?
(using socket tester)")
+ supply_test = models.BooleanField(
+ blank=True, null=True, help_text="Electrical supplies tested?
(using socket tester)"
+ )
# Shared electrical checks
- earthing = models.BooleanField(blank=True, null=True, help_text="Equipment appropriately earthed?
(truss, stage, generators etc)")
+ earthing = models.BooleanField(
+ blank=True,
+ null=True,
+ help_text="Equipment appropriately earthed?
(truss, stage, generators etc)",
+ )
pat = models.BooleanField(blank=True, null=True, help_text="All equipment in PAT period?")
# Medium Electrical Checks
- source_rcd = models.BooleanField(blank=True, null=True, help_text="Source RCD protected?
(if cable is more than 3m long) ")
- labelling = models.BooleanField(blank=True, null=True, help_text="Appropriate and clear labelling on distribution and cabling?")
+ source_rcd = models.BooleanField(
+ blank=True, null=True, help_text="Source RCD protected?
(if cable is more than 3m long) "
+ )
+ labelling = models.BooleanField(
+ blank=True, null=True, help_text="Appropriate and clear labelling on distribution and cabling?"
+ )
# First Distro
- fd_voltage_l1 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L1-N", help_text="L1 - N")
- fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N")
- fd_voltage_l3 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N")
- fd_phase_rotation = models.BooleanField(blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation
(if required)")
- fd_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
+ fd_voltage_l1 = models.IntegerField(
+ blank=True, null=True, verbose_name="First Distro Voltage L1-N", help_text="L1 - N"
+ )
+ fd_voltage_l2 = models.IntegerField(
+ blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N"
+ )
+ fd_voltage_l3 = models.IntegerField(
+ blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N"
+ )
+ fd_phase_rotation = models.BooleanField(
+ blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation
(if required)"
+ )
+ fd_earth_fault = models.DecimalField(
+ blank=True,
+ null=True,
+ max_digits=6,
+ decimal_places=2,
+ verbose_name="Earth Fault Loop Impedance",
+ help_text=earth_fault_text,
+ )
fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text=pssc_text)
# Worst case points
- w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
+ w1_description = models.CharField(blank=True, default="", max_length=255, help_text="Description")
w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V")
- w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
- w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
+ w1_earth_fault = models.DecimalField(
+ blank=True,
+ null=True,
+ max_digits=6,
+ decimal_places=2,
+ verbose_name="Earth Fault Loop Impedance",
+ help_text=earth_fault_text,
+ )
+ w2_description = models.CharField(blank=True, default="", max_length=255, help_text="Description")
w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V")
- w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
- w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description")
+ w2_earth_fault = models.DecimalField(
+ blank=True,
+ null=True,
+ max_digits=6,
+ decimal_places=2,
+ verbose_name="Earth Fault Loop Impedance",
+ help_text=earth_fault_text,
+ )
+ w3_description = models.CharField(blank=True, default="", max_length=255, help_text="Description")
w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?")
w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V")
- w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text)
+ w3_earth_fault = models.DecimalField(
+ blank=True,
+ null=True,
+ max_digits=6,
+ decimal_places=2,
+ verbose_name="Earth Fault Loop Impedance",
+ help_text=earth_fault_text,
+ )
- all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?
(using test button)")
- public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?
(using socket tester)")
+ all_rcds_tested = models.BooleanField(
+ blank=True, null=True, help_text="All circuit RCDs tested?
(using test button)"
+ )
+ public_sockets_tested = models.BooleanField(
+ blank=True,
+ null=True,
+ help_text="Public/Performer accessible circuits tested?
(using socket tester)",
+ )
class Meta:
- ordering = ['event']
- permissions = [
- ('review_power', 'Can review Power Test Records')
- ]
+ ordering = ["event"]
+ permissions = [("review_power", "Can review Power Test Records")]
def __str__(self):
return f"{self.pk} - {self.event}"
def get_absolute_url(self):
- return reverse('pt_detail', kwargs={'pk': self.pk})
+ return reverse("pt_detail", kwargs={"pk": self.pk})
@property
def activity_feed_string(self):
@@ -960,8 +1149,8 @@ class PowerTestRecord(ReviewableModel, RevisionMixin):
class EventCheckIn(models.Model):
- event = models.ForeignKey('Event', related_name='crew', on_delete=models.CASCADE)
- person = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='checkins', on_delete=models.CASCADE)
+ event = models.ForeignKey("Event", related_name="crew", on_delete=models.CASCADE)
+ person = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="checkins", on_delete=models.CASCADE)
time = models.DateTimeField()
role = models.CharField(max_length=50, blank=True)
vehicle = models.CharField(max_length=100, blank=True)
@@ -978,7 +1167,7 @@ class EventCheckIn(models.Model):
raise ValidationError("May not check out before you've checked in." + sass)
def get_absolute_url(self):
- return reverse('event_detail', kwargs={'pk': self.event_id})
+ return reverse("event_detail", kwargs={"pk": self.event_id})
def active(self):
- return end_time is not None
+ return self.end_time is None
diff --git a/RIGS/signals.py b/RIGS/signals.py
index b17e751c..5c9b24c7 100644
--- a/RIGS/signals.py
+++ b/RIGS/signals.py
@@ -3,9 +3,8 @@ import urllib.error
import urllib.parse
import urllib.request
from io import BytesIO
-import datetime
-from PyPDF2 import PdfFileReader, PdfFileMerger
+from pypdf import PdfReader, PdfWriter
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.cache import cache
@@ -25,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 = PdfFileMerger()
+ template = get_template("event_print.xml")
+ merger = PdfWriter()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
- merger.append(PdfFileReader(buffer))
+ merger.append(PdfReader(buffer))
buffer.close()
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
@@ -47,13 +46,13 @@ 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"
@@ -64,17 +63,19 @@ def send_eventauthorisation_success_email(instance):
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
)
- css = finders.find('css/email.css')
- html = Premailer(get_template("email/eventauthorisation_client_success.html").render(context),
- external_styles=css).transform()
- client_email.attach_alternative(html, 'text/html')
+ 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")
- 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
@@ -82,9 +83,7 @@ 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("email/eventauthorisation_mic_success.txt").render(context), to=[mic_email_address]
)
# Now we have both emails successfully generated, send them out
@@ -110,10 +109,11 @@ 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")
+ + 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,
}
email = EmailMultiAlternatives(
@@ -122,10 +122,13 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
to=[admin.email],
reply_to=[user.email],
)
- css = finders.find('css/email.css')
- html = Premailer(get_template("email/admin_awaiting_approval.html").render(context),
- external_styles=css).transform()
- email.attach_alternative(html, 'text/html')
+ 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")
email.send()
# Update last sent
diff --git a/RIGS/static/js/marked.min.js b/RIGS/static/js/marked.min.js
index 555c1dc1..4d2eb081 100644
--- a/RIGS/static/js/marked.min.js
+++ b/RIGS/static/js/marked.min.js
@@ -3,4 +3,4 @@
* Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed)
* https://github.com/chjj/marked
*/
-(function(){var block={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:noop,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:noop,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:noop,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};block.bullet=/(?:[*+-]|\d+\.)/;block.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;block.item=replace(block.item,"gm")(/bull/g,block.bullet)();block.list=replace(block.list)(/bull/g,block.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+block.def.source+")")();block.blockquote=replace(block.blockquote)("def",block.def)();block._tag="(?!(?:"+"a|em|strong|small|s|cite|q|dfn|abbr|data|time|code"+"|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo"+"|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b";block.html=replace(block.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/
"}return'"+(escaped?code:escape(code,true))+"\n
\n"};Renderer.prototype.blockquote=function(quote){return"'+(escaped?code:escape(code,true))+"\n\n"+quote+"
\n"};Renderer.prototype.html=function(html){return html};Renderer.prototype.heading=function(text,level,raw){return"
\n":"
\n"};Renderer.prototype.list=function(body,ordered){var type=ordered?"ol":"ul";return"<"+type+">\n"+body+""+type+">\n"};Renderer.prototype.listitem=function(text){return"\n"+"\n"+header+"\n"+"\n"+body+"\n"+"
\n"};Renderer.prototype.tablerow=function(content){return"\n"+content+" \n"};Renderer.prototype.tablecell=function(content,flags){var type=flags.header?"th":"td";var tag=flags.align?"<"+type+' style="text-align:'+flags.align+'">':"<"+type+">";return tag+content+""+type+">\n"};Renderer.prototype.strong=function(text){return""+text+""};Renderer.prototype.em=function(text){return""+text+""};Renderer.prototype.codespan=function(text){return""+text+""};Renderer.prototype.br=function(){return this.options.xhtml?"
":"
"};Renderer.prototype.del=function(text){return""+text+""};Renderer.prototype.link=function(href,title,text){if(this.options.sanitize){try{var prot=decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return""}if(prot.indexOf("javascript:")===0||prot.indexOf("vbscript:")===0){return""}}var out='"+text+"";return out};Renderer.prototype.image=function(href,title,text){var out='":">";return out};Renderer.prototype.text=function(text){return text};function Parser(options){this.tokens=[];this.token=null;this.options=options||marked.defaults;this.options.renderer=this.options.renderer||new Renderer;this.renderer=this.options.renderer;this.renderer.options=this.options}Parser.parse=function(src,options,renderer){var parser=new Parser(options,renderer);return parser.parse(src)};Parser.prototype.parse=function(src){this.inline=new InlineLexer(src.links,this.options,this.renderer);this.tokens=src.reverse();var out="";while(this.next()){out+=this.tok()}return out};Parser.prototype.next=function(){return this.token=this.tokens.pop()};Parser.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0};Parser.prototype.parseText=function(){var body=this.token.text;while(this.peek().type==="text"){body+="\n"+this.next().text}return this.inline.output(body)};Parser.prototype.tok=function(){switch(this.token.type){case"space":{return""}case"hr":{return this.renderer.hr()}case"heading":{return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text)}case"code":{return this.renderer.code(this.token.text,this.token.lang,this.token.escaped)}case"table":{var header="",body="",i,row,cell,flags,j;cell="";for(i=0;i
"+escape(e.message+"",true)+""}throw e}}marked.options=marked.setOptions=function(opt){merge(marked.defaults,opt);return marked};marked.defaults={gfm:true,tables:true,breaks:false,pedantic:false,sanitize:false,sanitizer:null,mangle:true,smartLists:false,silent:false,highlight:null,langPrefix:"lang-",smartypants:false,headerPrefix:"",renderer:new Renderer,xhtml:false};marked.Parser=Parser;marked.parser=Parser.parse;marked.Renderer=Renderer;marked.Lexer=Lexer;marked.lexer=Lexer.lex;marked.InlineLexer=InlineLexer;marked.inlineLexer=InlineLexer.output;marked.parse=marked;if(typeof module!=="undefined"&&typeof exports==="object"){module.exports=marked}else if(typeof define==="function"&&define.amd){define(function(){return marked})}else{this.marked=marked}}).call(function(){return this||(typeof window!=="undefined"?window:global)}()); \ No newline at end of file +(function(){var block={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:noop,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:noop,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:noop,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};block.bullet=/(?:[*+-]|\d+\.)/;block.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;block.item=replace(block.item,"gm")(/bull/g,block.bullet)();block.list=replace(block.list)(/bull/g,block.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+block.def.source+")")();block.blockquote=replace(block.blockquote)("def",block.def)();block._tag="(?!(?:"+"a|em|strong|small|s|cite|q|dfn|abbr|data|time|code"+"|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo"+"|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b";block.html=replace(block.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/
"+(escaped?code:escape(code,true))+"\n"}return''+(escaped?code:escape(code,true))+"\n\n"};Renderer.prototype.blockquote=function(quote){return"\n"+quote+"\n"};Renderer.prototype.html=function(html){return html};Renderer.prototype.heading=function(text,level,raw){return"
"+text+"
\n"};Renderer.prototype.table=function(header,body){return""+text+""};Renderer.prototype.br=function(){return this.options.xhtml?""+escape(e.message+"",true)+""}throw e}}marked.options=marked.setOptions=function(opt){merge(marked.defaults,opt);return marked};marked.defaults={gfm:true,tables:true,breaks:false,pedantic:false,sanitize:false,sanitizer:null,mangle:true,smartLists:false,silent:false,highlight:null,langPrefix:"lang-",smartypants:false,headerPrefix:"",renderer:new Renderer,xhtml:false};marked.Parser=Parser;marked.parser=Parser.parse;marked.Renderer=Renderer;marked.Lexer=Lexer;marked.lexer=Lexer.lex;marked.InlineLexer=InlineLexer;marked.inlineLexer=InlineLexer.output;marked.parse=marked;if(typeof module!=="undefined"&&typeof exports==="object"){module.exports=marked}else if(typeof define==="function"&&define.amd){define(function(){return marked})}else{this.marked=marked}}).call(function(){return this||(typeof window!=="undefined"?window:global)}()); diff --git a/RIGS/templates/base_rigs.html b/RIGS/templates/base_rigs.html index f9a78c9d..d7556a4e 100644 --- a/RIGS/templates/base_rigs.html +++ b/RIGS/templates/base_rigs.html @@ -6,36 +6,8 @@ {% load total_invoices_todo from filters %} {% block titleheader %} - - -FrankenRIGS +RIGS {% endblock %} {% block titleelements %} diff --git a/RIGS/templates/email/eventauthorisation_mic_success.txt b/RIGS/templates/email/eventauthorisation_mic_success.txt index b4309dd4..43ddd5e1 100644 --- a/RIGS/templates/email/eventauthorisation_mic_success.txt +++ b/RIGS/templates/email/eventauthorisation_mic_success.txt @@ -1,5 +1,5 @@ Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}}, -Just to let you know your event N{{object.eventdisplay_id}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}. +Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}. The TEC Rig Information Gathering System diff --git a/RIGS/templates/event_detail.html b/RIGS/templates/event_detail.html index 816dee1e..4013df65 100644 --- a/RIGS/templates/event_detail.html +++ b/RIGS/templates/event_detail.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %} +{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %} {% load markdown_tags %} {% load static %} @@ -18,7 +18,7 @@ {% block content %}