mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-10-10 22:55:29 +00:00
Compare commits
1 Commits
dependabot
...
feature/su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee948381b1 |
@@ -1,5 +0,0 @@
|
||||
[run]
|
||||
omit = */migrations/*
|
||||
*/tests/*
|
||||
*/site-packages/*
|
||||
*/distutils/*
|
||||
@@ -1,30 +0,0 @@
|
||||
# Exclude everything by default, then explicitly include what the image needs.
|
||||
# This also keeps .env files, keys and local virtualenvs out of the image.
|
||||
*
|
||||
|
||||
# Build tooling
|
||||
!package.json
|
||||
!package-lock.json
|
||||
!gulpfile.js
|
||||
!pyproject.toml
|
||||
!uv.lock
|
||||
!.python-version
|
||||
!manage.py
|
||||
|
||||
# Frontend sources
|
||||
!pipeline/source_assets/**
|
||||
|
||||
# Django project and apps
|
||||
!PyRIGS/**
|
||||
!RIGS/**
|
||||
!assets/**
|
||||
!training/**
|
||||
!users/**
|
||||
!versioning/**
|
||||
!templates/**
|
||||
|
||||
# Never ship these, even from within included directories
|
||||
**/tests
|
||||
**/__pycache__
|
||||
**/*.pyc
|
||||
**/*.sqlite3
|
||||
27
.env.example
27
.env.example
@@ -1,27 +0,0 @@
|
||||
SECRET_KEY=mysecretkey
|
||||
DEBUG=true
|
||||
STAGING=false
|
||||
DJANGO_ALLOWED_HOSTS=localhost
|
||||
DOMAIN=
|
||||
SESSION_COOKIE_SECURE_ENABLED=true
|
||||
CSRF_COOKIE_SECURE_ENABLED=true
|
||||
FORUM_WEBHOOK_SECRET=
|
||||
|
||||
DATABASE_ENGINE=postgresql
|
||||
DATABASE_NAME=rigs
|
||||
DATABASE_USERNAME=rigs
|
||||
DATABASE_PASSWORD=rigs
|
||||
DATABASE_HOST=db
|
||||
DATABASE_PORT=5432
|
||||
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=587
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
EMAIL_USE_TLS=
|
||||
EMAIL_USE_SSL=
|
||||
EMAIL_FROM=
|
||||
|
||||
HCAPTCHA_SITEKEY=
|
||||
HCAPTCHA_SECRET=
|
||||
SENTRY_DSN=
|
||||
32
.github/dependabot.yml
vendored
32
.github/dependabot.yml
vendored
@@ -1,32 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "uv"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
python-minor-patch:
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
npm-minor-patch:
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "docker-compose"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
211
.github/workflows/ci.yml
vendored
211
.github/workflows/ci.yml
vendored
@@ -1,211 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libcairo2-dev
|
||||
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
enable-cache: true
|
||||
|
||||
- run: uv sync --locked
|
||||
|
||||
- name: Pre-commit hooks (ruff, formatting, file hygiene)
|
||||
run: uv run prek run --all-files --show-diff-on-failure
|
||||
|
||||
- name: Django system checks
|
||||
run: uv run python manage.py check
|
||||
|
||||
- name: Check for missing migrations
|
||||
run: uv run python manage.py makemigrations --check --dry-run
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DATABASE_ENGINE: sqlite3
|
||||
DATABASE_NAME: db.sqlite3
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libcairo2-dev
|
||||
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
enable-cache: true
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
|
||||
- run: uv sync --locked
|
||||
|
||||
- name: Build frontend assets
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- run: uv run python manage.py collectstatic --noinput
|
||||
|
||||
- name: Run tests
|
||||
run: uv run pytest -n auto --cov
|
||||
|
||||
- name: Coveralls
|
||||
run: uv run coveralls --service=github
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Each architecture builds on a native runner: emulating arm64 under QEMU (compiling pycairo, npm, uv) was the slow part
|
||||
docker-build:
|
||||
name: Docker build (${{ matrix.platform }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: [lint, test]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
|
||||
# Only authenticate when we are actually going to push (never for PRs, which may come from forks)
|
||||
- name: Log in to GHCR
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Registry references must be lowercase, but the repository name is not
|
||||
- name: Set image name
|
||||
run: echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
|
||||
|
||||
- id: build
|
||||
name: Build (and push by digest on master)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: GIT_SHA=${{ github.sha }}
|
||||
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=${{ github.event_name == 'push' }}
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
|
||||
- name: Export digest
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
DIGEST: ${{ steps.build.outputs.digest }}
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/digests"
|
||||
touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
if: github.event_name == 'push'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ strategy.job-index }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
docker:
|
||||
name: Docker image
|
||||
runs-on: ubuntu-latest
|
||||
needs: [docker-build]
|
||||
if: github.event_name == 'push'
|
||||
outputs:
|
||||
tag: ${{ steps.sha-tag.outputs.tag }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=sha
|
||||
|
||||
- id: sha-tag
|
||||
name: Pick the commit tag
|
||||
env:
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
ref="$(grep -m1 ':sha-' <<< "$TAGS")"
|
||||
test -n "$ref"
|
||||
echo "tag=${ref##*:}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create multi-arch manifest
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
IMAGE="ghcr.io/${GITHUB_REPOSITORY,,}"
|
||||
docker buildx imagetools create \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "$IMAGE@sha256:%s " *)
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
needs: [docker]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
environment: production
|
||||
concurrency: deploy-production
|
||||
steps:
|
||||
- name: Deploy the image built for this commit
|
||||
env:
|
||||
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
|
||||
TAG: ${{ needs.docker.outputs.tag }}
|
||||
run: |
|
||||
install -m 700 -d ~/.ssh
|
||||
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||
ssh deploy@rigs.nottinghamtec.co.uk "$TAG"
|
||||
151
.github/workflows/combine-prs.yml
vendored
151
.github/workflows/combine-prs.yml
vendored
@@ -1,151 +0,0 @@
|
||||
name: 'Combine PRs'
|
||||
|
||||
# Controls when the action will run - in this case triggered manually
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branchPrefix:
|
||||
description: 'Branch prefix to find combinable PRs based on'
|
||||
required: true
|
||||
default: 'dependabot'
|
||||
mustBeGreen:
|
||||
description: 'Only combine PRs that are green (status is success)'
|
||||
required: true
|
||||
default: true
|
||||
combineBranchName:
|
||||
description: 'Name of the branch to combine PRs into'
|
||||
required: true
|
||||
default: 'combine-prs-branch'
|
||||
ignoreLabel:
|
||||
description: 'Exclude PRs with this label'
|
||||
required: true
|
||||
default: 'nocombine'
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "combine-prs"
|
||||
combine-prs:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- uses: actions/github-script@v9
|
||||
id: create-combined-pr
|
||||
name: Create Combined PR
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
let branchesAndPRStrings = [];
|
||||
let baseBranch = null;
|
||||
let baseBranchSHA = null;
|
||||
for (const pull of pulls) {
|
||||
const branch = pull['head']['ref'];
|
||||
console.log('Pull for branch: ' + branch);
|
||||
if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) {
|
||||
console.log('Branch matched prefix: ' + branch);
|
||||
let statusOK = true;
|
||||
if(${{ github.event.inputs.mustBeGreen }}) {
|
||||
console.log('Checking green status: ' + branch);
|
||||
const stateQuery = `query($owner: String!, $repo: String!, $pull_number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number:$pull_number) {
|
||||
commits(last: 1) {
|
||||
nodes {
|
||||
commit {
|
||||
statusCheckRollup {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
const vars = {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pull['number']
|
||||
};
|
||||
const result = await github.graphql(stateQuery, vars);
|
||||
const [{ commit }] = result.repository.pullRequest.commits.nodes;
|
||||
const state = commit.statusCheckRollup.state
|
||||
console.log('Validating status: ' + state);
|
||||
if(state != 'SUCCESS') {
|
||||
console.log('Discarding ' + branch + ' with status ' + state);
|
||||
statusOK = false;
|
||||
}
|
||||
}
|
||||
console.log('Checking labels: ' + branch);
|
||||
const labels = pull['labels'];
|
||||
for(const label of labels) {
|
||||
const labelName = label['name'];
|
||||
console.log('Checking label: ' + labelName);
|
||||
if(labelName == '${{ github.event.inputs.ignoreLabel }}') {
|
||||
console.log('Discarding ' + branch + ' with label ' + labelName);
|
||||
statusOK = false;
|
||||
}
|
||||
}
|
||||
if (statusOK) {
|
||||
console.log('Adding branch to array: ' + branch);
|
||||
const prString = '#' + pull['number'] + ' ' + pull['title'];
|
||||
branchesAndPRStrings.push({ branch, prString });
|
||||
baseBranch = pull['base']['ref'];
|
||||
baseBranchSHA = pull['base']['sha'];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (branchesAndPRStrings.length == 0) {
|
||||
core.setFailed('No PRs/branches matched criteria');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await github.rest.git.createRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}',
|
||||
sha: baseBranchSHA
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?');
|
||||
return;
|
||||
}
|
||||
|
||||
let combinedPRs = [];
|
||||
let mergeFailedPRs = [];
|
||||
for(const { branch, prString } of branchesAndPRStrings) {
|
||||
try {
|
||||
await github.rest.repos.merge({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
base: '${{ github.event.inputs.combineBranchName }}',
|
||||
head: branch,
|
||||
});
|
||||
console.log('Merged branch ' + branch);
|
||||
combinedPRs.push(prString);
|
||||
} catch (error) {
|
||||
console.log('Failed to merge branch ' + branch);
|
||||
mergeFailedPRs.push(prString);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Creating combined PR');
|
||||
const combinedPRsString = combinedPRs.join('\n');
|
||||
let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString;
|
||||
if(mergeFailedPRs.length > 0) {
|
||||
const mergeFailedPRsString = mergeFailedPRs.join('\n');
|
||||
body += '\n\n⚠️ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString
|
||||
}
|
||||
await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: 'Combined PR',
|
||||
head: '${{ github.event.inputs.combineBranchName }}',
|
||||
base: baseBranch,
|
||||
body: body
|
||||
});
|
||||
39
.gitignore
vendored
39
.gitignore
vendored
@@ -1,6 +1,3 @@
|
||||
tmp/
|
||||
db.sqlite3
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
@@ -25,11 +22,12 @@ var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
node_modules/
|
||||
data/
|
||||
|
||||
# Continer extras
|
||||
.vagrant
|
||||
_builds
|
||||
_steps
|
||||
_projects
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
@@ -46,7 +44,6 @@ htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
.pytest_cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
@@ -69,9 +66,19 @@ target/
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
#Built dependencies
|
||||
pipeline/built_assets
|
||||
# User-specific stuff:
|
||||
# .idea/workspace.xml
|
||||
# .idea/tasks.xml
|
||||
# .idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
# .idea/dataSources.ids
|
||||
# .idea/dataSources.xml
|
||||
# .idea/sqlDataSources.xml
|
||||
# .idea/dynamic.xml
|
||||
# .idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
@@ -99,19 +106,3 @@ atlassian-ide-plugin.xml
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
.vscode/
|
||||
screenshots/
|
||||
|
||||
# Virutal Environments
|
||||
.venv/
|
||||
|
||||
# Environment variable files (keep .env.example)
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
nginx/certs/
|
||||
nginx/certbot/
|
||||
nginx/*.conf
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
PyRIGS
|
||||
5
.idea/encodings.xml
generated
Normal file
5
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
|
||||
</project>
|
||||
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/pyrigs.iml" filepath="$PROJECT_DIR$/.idea/pyrigs.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
5
.idea/scopes/scope_settings.xml
generated
Normal file
5
.idea/scopes/scope_settings.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<state>
|
||||
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
|
||||
</state>
|
||||
</component>
|
||||
7
.idea/vcs.xml
generated
Normal file
7
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
args: [--allow-multiple-documents]
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
args: [--fix=lf]
|
||||
- id: trailing-whitespace
|
||||
args: [--markdown-linebreak-ext=md]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: uv-check
|
||||
name: uv lock check
|
||||
description: Checks the validity of the uv.lock file.
|
||||
entry: uv lock --check
|
||||
language: system
|
||||
files: pyproject.toml
|
||||
pass_filenames: false
|
||||
|
||||
- id: ruff-lint
|
||||
name: ruff linting
|
||||
description: Run ruff linting
|
||||
entry: uv run ruff check --force-exclude
|
||||
language: system
|
||||
'types_or': [python, pyi]
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
- id: ruff-format
|
||||
name: ruff formatting
|
||||
description: Run ruff formatting
|
||||
entry: uv run ruff format --force-exclude
|
||||
language: system
|
||||
'types_or': [python, pyi]
|
||||
@@ -1 +0,0 @@
|
||||
3.14
|
||||
7
.slugignore
Normal file
7
.slugignore
Normal file
@@ -0,0 +1,7 @@
|
||||
*.sqlite3
|
||||
*.scss
|
||||
*.md
|
||||
*.rb
|
||||
Vagrantfile
|
||||
config/vagrant/*
|
||||
config/vagrant.yml
|
||||
106
Dockerfile
106
Dockerfile
@@ -1,106 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
#
|
||||
# Targets:
|
||||
# prod (default, last stage) gunicorn image with assets and static files baked in
|
||||
# dev dependencies only; the source is bind-mounted by compose.override.yml
|
||||
#
|
||||
# Stages are ordered so that editing application code only invalidates the cheap final layers:
|
||||
# apt packages, Python dependencies and node modules are all cached separately.
|
||||
|
||||
# ---- Frontend assets (multi-arch: official node image) ----
|
||||
FROM node:26-slim AS assets
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci
|
||||
|
||||
COPY gulpfile.js ./
|
||||
COPY pipeline/source_assets ./pipeline/source_assets
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# ---- Shared Python base: runtime system libraries only ----
|
||||
FROM python:3.14-slim-trixie AS python-base
|
||||
|
||||
# Keep downloaded packages between builds (the default Docker config for Debian images deletes them)
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libcairo2
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
UV_PROJECT_ENVIRONMENT=/opt/venv \
|
||||
UV_LINK_MODE=copy \
|
||||
PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
|
||||
# ---- Python dependencies (only rebuilt when pyproject.toml / uv.lock change) ----
|
||||
FROM python-base AS deps-base
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.12.21 /uv /uvx /bin/
|
||||
|
||||
# pycairo (via z3c.rml) has no wheels and is compiled against cairo
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential pkg-config libcairo2-dev
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
FROM deps-base AS deps-prod
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --no-install-project --no-dev
|
||||
|
||||
FROM deps-base AS deps-dev
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --no-install-project
|
||||
|
||||
|
||||
# ---- Development: dependencies only, source and static assets are mounted at runtime ----
|
||||
FROM python-base AS dev
|
||||
COPY --from=deps-dev /opt/venv /opt/venv
|
||||
WORKDIR /app
|
||||
EXPOSE 8000
|
||||
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||
|
||||
|
||||
# ---- Static files: depends on the source, so this is the first stage that reruns on a code change ----
|
||||
FROM python-base AS static
|
||||
COPY --from=deps-prod /opt/venv /opt/venv
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
COPY --from=assets /app/pipeline/built_assets /app/pipeline/built_assets
|
||||
# Placeholder values only satisfy settings that are mandatory when DEBUG is off; they are not kept in the image
|
||||
RUN DEBUG=false EMAIL_HOST=build EMAIL_HOST_USER=build EMAIL_HOST_PASSWORD=build EMAIL_FROM=build@example.com \
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
|
||||
# ---- Production image ----
|
||||
FROM python-base AS prod
|
||||
RUN addgroup --system app \
|
||||
&& adduser --system --group --home /home/app app
|
||||
|
||||
COPY --from=deps-prod /opt/venv /opt/venv
|
||||
WORKDIR /app
|
||||
COPY --chown=app:app . /app
|
||||
COPY --from=static --chown=app:app /app/static /app/static
|
||||
COPY --from=assets --chown=app:app /app/pipeline/built_assets /app/pipeline/built_assets
|
||||
|
||||
# DEBUG must never be on in a built image; enable it explicitly via the environment if needed
|
||||
ENV DEBUG=false
|
||||
|
||||
# Declared last so a new commit SHA does not invalidate any of the layers above
|
||||
ARG GIT_SHA=unknown
|
||||
ENV GIT_SHA=$GIT_SHA
|
||||
|
||||
USER app
|
||||
EXPOSE 8000
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "PyRIGS.wsgi"]
|
||||
2
LICENSE
2
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.
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -1,96 +1,54 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
def get_oembed(login_url, request, oembed_view, kwargs):
|
||||
context = {}
|
||||
context["oembed_url"] = f"{request.scheme}://{request.META['HTTP_HOST']}{reverse(oembed_view, kwargs=kwargs)}"
|
||||
context["login_url"] = f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}"
|
||||
resp = render(request, "login_redirect.html", context=context)
|
||||
return resp
|
||||
|
||||
|
||||
def has_oembed(oembed_view, login_url=settings.LOGIN_URL):
|
||||
def _dec(view_func):
|
||||
def _checklogin(request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
return view_func(request, *args, **kwargs)
|
||||
else:
|
||||
if oembed_view is not None:
|
||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||
else:
|
||||
return HttpResponseRedirect(f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}")
|
||||
|
||||
_checklogin.__doc__ = view_func.__doc__
|
||||
_checklogin.__dict__ = view_func.__dict__
|
||||
return _checklogin
|
||||
|
||||
return _dec
|
||||
|
||||
|
||||
def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
|
||||
def user_passes_test_with_403(test_func, login_url=None):
|
||||
"""
|
||||
Decorator for views that checks that the user passes the given test.
|
||||
|
||||
Anonymous users will be redirected to login_url, while users that fail
|
||||
the test will be given a 403 error.
|
||||
If embed_view is set, then a JS redirect will be used, and a application/json+oembed
|
||||
meta tag set with the url of oembed_view
|
||||
(oembed_view will be passed the kwargs from the main function)
|
||||
"""
|
||||
if not login_url:
|
||||
from django.conf import settings
|
||||
|
||||
login_url = settings.LOGIN_URL
|
||||
|
||||
def _dec(view_func):
|
||||
def _checklogin(request, *args, **kwargs):
|
||||
if test_func(request.user):
|
||||
return view_func(request, *args, **kwargs)
|
||||
elif not request.user.is_authenticated:
|
||||
if oembed_view is not None:
|
||||
return get_oembed(login_url, request, oembed_view, kwargs)
|
||||
else:
|
||||
return HttpResponseRedirect(f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}")
|
||||
elif not request.user.is_authenticated():
|
||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||
else:
|
||||
resp = render(request, "403.html")
|
||||
resp = render_to_response('403.html', context_instance=RequestContext(request))
|
||||
resp.status_code = 403
|
||||
return resp
|
||||
|
||||
_checklogin.__doc__ = view_func.__doc__
|
||||
_checklogin.__dict__ = view_func.__dict__
|
||||
return _checklogin
|
||||
|
||||
return _dec
|
||||
|
||||
|
||||
def permission_required_with_403(perm, login_url=None, oembed_view=None):
|
||||
def permission_required_with_403(perm, login_url=None):
|
||||
"""
|
||||
Decorator for views that checks whether a user has a particular permission
|
||||
enabled, redirecting to the log-in page or rendering a 403 as necessary.
|
||||
"""
|
||||
return user_passes_test_with_403(lambda u: u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)
|
||||
return user_passes_test_with_403(lambda u: u.has_perm(perm), login_url=login_url)
|
||||
|
||||
from RIGS import models
|
||||
|
||||
def api_key_required(function):
|
||||
"""
|
||||
Decorator for views that checks api_pk and api_key.
|
||||
Failed users will be given a 403 error.
|
||||
Should only be used for urls which include <api_pk> and <api_key> kwargs.
|
||||
|
||||
Will update the kwargs to include the user object if successful (under the key 'user').
|
||||
Should only be used for urls which include <api_pk> and <api_key> kwargs
|
||||
"""
|
||||
|
||||
def wrap(request, *args, **kwargs):
|
||||
|
||||
userid = kwargs.get("api_pk")
|
||||
key = kwargs.get("api_key")
|
||||
userid = kwargs.get('api_pk')
|
||||
key = kwargs.get('api_key')
|
||||
|
||||
error_resp = render(request, "403.html")
|
||||
error_resp = render_to_response('403.html', context_instance=RequestContext(request))
|
||||
error_resp.status_code = 403
|
||||
|
||||
if key is None:
|
||||
@@ -100,28 +58,10 @@ def api_key_required(function):
|
||||
|
||||
try:
|
||||
user_object = models.Profile.objects.get(pk=userid)
|
||||
kwargs = {**kwargs, "user": user_object}
|
||||
except models.Profile.DoesNotExist:
|
||||
return error_resp
|
||||
|
||||
if user_object.api_key != key:
|
||||
return error_resp
|
||||
return function(request, *args, **kwargs)
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
def nottinghamtec_address_required(function):
|
||||
"""
|
||||
Checks that the current user has an email address ending @nottinghamtec.co.uk
|
||||
"""
|
||||
|
||||
def wrap(request, *args, **kwargs):
|
||||
# Fail if current user's email address isn't @nottinghamtec.co.uk
|
||||
if not request.user.email.endswith("@nottinghamtec.co.uk"):
|
||||
error_resp = render(request, "eventauthorisation_request_error.html")
|
||||
return error_resp
|
||||
|
||||
return function(request, *args, **kwargs)
|
||||
|
||||
return wrap
|
||||
return wrap
|
||||
@@ -1,3 +1,5 @@
|
||||
DATETIME_FORMAT = "d/m/Y H:i"
|
||||
DATE_FORMAT = "d/m/Y"
|
||||
TIME_FORMAT = "H:i"
|
||||
from __future__ import unicode_literals
|
||||
|
||||
DATETIME_FORMAT = ('d/m/Y H:i')
|
||||
DATE_FORMAT = ('d/m/Y')
|
||||
TIME_FORMAT = ('H:i')
|
||||
@@ -8,263 +8,212 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.7/ref/settings/
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from envparse import env
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = env("SECRET_KEY", default="gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e")
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get('SECRET_KEY') else 'gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = env("DEBUG", cast=bool, default=True)
|
||||
STAGING = env("STAGING", cast=bool, default=False)
|
||||
CI = env("CI", cast=bool, default=False)
|
||||
# Commit the running image was built from (baked in at build time)
|
||||
GIT_SHA = env("GIT_SHA", default="unknown")
|
||||
DEBUG = bool(int(os.environ.get('DEBUG'))) if os.environ.get('DEBUG') else True
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
host.strip() for host in env("DJANGO_ALLOWED_HOSTS", default="rigs.nottinghamtec.co.uk").split(",") if host.strip()
|
||||
]
|
||||
TEMPLATE_DEBUG = True
|
||||
|
||||
if DEBUG:
|
||||
CSRF_TRUSTED_ORIGINS = [f"http://{host}" for host in ALLOWED_HOSTS]
|
||||
CSRF_TRUSTED_ORIGINS.append("http://localhost:8000")
|
||||
CSRF_TRUSTED_ORIGINS.append("http://localhost:8001")
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
else:
|
||||
# Django 4+ checks the Origin header on unsafe requests against this list, which
|
||||
# matters when TLS is terminated by a proxy in front of us.
|
||||
CSRF_TRUSTED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS]
|
||||
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
|
||||
|
||||
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)
|
||||
SECURE_SSL_REDIRECT = True # Redirect all http requests to https
|
||||
|
||||
INTERNAL_IPS = ["127.0.0.1"]
|
||||
INTERNAL_IPS = ['127.0.0.1']
|
||||
|
||||
DOMAIN = env("DOMAIN", default="example.com")
|
||||
ADMINS = (
|
||||
('Tom Price', 'tomtom5152@gmail.com')
|
||||
)
|
||||
|
||||
ADMINS = [("IT Manager", f"it@{DOMAIN}")]
|
||||
if DEBUG:
|
||||
ADMINS.append(("Testing Superuser", "superuser@example.com"))
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = (
|
||||
"whitenoise.runserver_nostatic",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.humanize",
|
||||
"versioning",
|
||||
"users",
|
||||
"RIGS",
|
||||
"assets",
|
||||
"training",
|
||||
"registration",
|
||||
"reversion",
|
||||
"widget_tweaks",
|
||||
"hcaptcha",
|
||||
"massadmin",
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'RIGS',
|
||||
'subhire',
|
||||
|
||||
'debug_toolbar',
|
||||
'registration',
|
||||
'reversion',
|
||||
'captcha',
|
||||
'widget_tweaks',
|
||||
'raven.contrib.django.raven_compat',
|
||||
)
|
||||
|
||||
MIDDLEWARE = (
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"reversion.middleware.RevisionMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"htmlmin.middleware.HtmlMinifyMiddleware",
|
||||
"htmlmin.middleware.MarkRequestMiddleware",
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'reversion.middleware.RevisionMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = "PyRIGS.urls"
|
||||
ROOT_URLCONF = 'PyRIGS.urls'
|
||||
|
||||
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
||||
|
||||
WSGI_APPLICATION = "PyRIGS.wsgi.application"
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.{}".format(env("DATABASE_ENGINE", default="sqlite3")),
|
||||
"NAME": env("DATABASE_NAME", default="rigs"),
|
||||
"USER": env("DATABASE_USERNAME", default="rigs"),
|
||||
"PASSWORD": env("DATABASE_PASSWORD", default="rigs"),
|
||||
"HOST": env("DATABASE_HOST", default="127.0.0.1"),
|
||||
"PORT": env("DATABASE_PORT", 5432),
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
# Logging
|
||||
if not DEBUG:
|
||||
import dj_database_url
|
||||
DATABASES['default'] = dj_database_url.config()
|
||||
|
||||
# Logging
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": (
|
||||
"%(asctime)s [%(process)d] [%(levelname)s] "
|
||||
+ "pathname=%(pathname)s lineno=%(lineno)s "
|
||||
+ "funcname=%(funcName)s %(message)s"
|
||||
),
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': ('%(asctime)s [%(process)d] [%(levelname)s] ' +
|
||||
'pathname=%(pathname)s lineno=%(lineno)s ' +
|
||||
'funcname=%(funcName)s %(message)s'),
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
||||
},
|
||||
"simple": {"format": "%(levelname)s %(message)s"},
|
||||
'simple': {
|
||||
'format': '%(levelname)s %(message)s'
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"null": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.NullHandler",
|
||||
'handlers': {
|
||||
'null': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.NullHandler',
|
||||
},
|
||||
"console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose"},
|
||||
"mail_admins": {
|
||||
"class": "django.utils.log.AdminEmailHandler",
|
||||
"level": "ERROR",
|
||||
# But the emails are plain text by default - HTML is nicer
|
||||
"include_html": True,
|
||||
'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,
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
# Again, default Django configuration to email unhandled exceptions
|
||||
"django.request": {
|
||||
"handlers": ["mail_admins"],
|
||||
"level": "ERROR",
|
||||
"propagate": True,
|
||||
'loggers': {
|
||||
# Again, default Django configuration to email unhandled exceptions
|
||||
'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"}}
|
||||
elif DEBUG:
|
||||
CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}}
|
||||
else:
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
|
||||
"LOCATION": "cache_table",
|
||||
}
|
||||
}
|
||||
import raven
|
||||
|
||||
# Error/performance monitoring
|
||||
sentry_sdk.init(
|
||||
dsn=env("SENTRY_DSN", default=""),
|
||||
integrations=[DjangoIntegration()],
|
||||
traces_sample_rate=1.0,
|
||||
)
|
||||
RAVEN_CONFIG = {
|
||||
'dsn': os.environ.get('RAVEN_DSN'),
|
||||
# If you are using git, you can also automatically configure the
|
||||
# release based on the git info.
|
||||
# 'release': raven.fetch_git_sha(os.path.dirname(os.path.dirname(__file__))),
|
||||
}
|
||||
|
||||
# User system
|
||||
AUTH_USER_MODEL = "RIGS.Profile"
|
||||
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")
|
||||
# reCAPTCHA settings
|
||||
RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', None)
|
||||
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', None)
|
||||
NOCAPTCHA = True
|
||||
|
||||
# Email
|
||||
EMAILER_TEST = False
|
||||
if not DEBUG or EMAILER_TEST:
|
||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||
EMAIL_HOST = env("EMAIL_HOST")
|
||||
EMAIL_PORT = env("EMAIL_PORT", cast=int, default=25)
|
||||
EMAIL_HOST_USER = env("EMAIL_HOST_USER")
|
||||
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
|
||||
EMAIL_USE_TLS = env("EMAIL_USE_TLS", cast=bool, default=False)
|
||||
EMAIL_USE_SSL = env("EMAIL_USE_SSL", cast=bool, default=False)
|
||||
DEFAULT_FROM_EMAIL = env("EMAIL_FROM")
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = os.environ.get('EMAIL_HOST')
|
||||
EMAIL_PORT = int(os.environ.get('EMAIL_PORT'))
|
||||
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
|
||||
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
|
||||
EMAIL_USE_TLS = bool(int(os.environ.get('EMAIL_USE_TLS', 0)))
|
||||
EMAIL_USE_SSL = bool(int(os.environ.get('EMAIL_USE_SSL', 0)))
|
||||
DEFAULT_FROM_EMAIL = os.environ.get('EMAIL_FROM')
|
||||
else:
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
EMAIL_COOLDOWN = datetime.timedelta(minutes=15)
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-gb"
|
||||
LANGUAGE_CODE = 'en-gb'
|
||||
|
||||
TIME_ZONE = "Europe/London"
|
||||
TIME_ZONE = 'Europe/London'
|
||||
|
||||
FORMAT_MODULE_PATH = "PyRIGS.formats"
|
||||
FORMAT_MODULE_PATH = 'PyRIGS.formats'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
USE_THOUSAND_SEPARATOR = False
|
||||
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M','%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.core.context_processors.debug",
|
||||
"django.core.context_processors.i18n",
|
||||
"django.core.context_processors.media",
|
||||
"django.core.context_processors.static",
|
||||
"django.core.context_processors.tz",
|
||||
"django.core.context_processors.request",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
)
|
||||
|
||||
# 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")
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
},
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
}
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = str(BASE_DIR / "static/")
|
||||
STATICFILES_DIRS = [
|
||||
str(BASE_DIR / "pipeline/built_assets"),
|
||||
]
|
||||
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
|
||||
STATIC_DIRS = (
|
||||
os.path.join(BASE_DIR, 'static/')
|
||||
)
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [BASE_DIR / "templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.i18n",
|
||||
"django.template.context_processors.media",
|
||||
"django.template.context_processors.static",
|
||||
"django.template.context_processors.tz",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
"PyRIGS.views.ajax_context",
|
||||
"PyRIGS.views.version_context",
|
||||
],
|
||||
"debug": DEBUG,
|
||||
},
|
||||
},
|
||||
]
|
||||
TEMPLATE_DIRS = (
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
)
|
||||
|
||||
USE_GRAVATAR = True
|
||||
USE_GRAVATAR=True
|
||||
|
||||
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
||||
AUTHORISATION_NOTIFICATION_ADDRESS = "productions@nottinghamtec.co.uk"
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from selenium import webdriver
|
||||
|
||||
from pytest_django.asserts import assertContains
|
||||
|
||||
|
||||
def create_datetime(year, month, day, hour, minute):
|
||||
tz = pytz.timezone(settings.TIME_ZONE)
|
||||
return tz.localize(datetime(year, month, day, hour, minute)).astimezone(tz)
|
||||
|
||||
|
||||
def create_browser():
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument("--window-size=1920,1080")
|
||||
options.add_argument("--lang=en-GB")
|
||||
options.add_argument("--headless")
|
||||
if settings.CI:
|
||||
options.add_argument("--no-sandbox")
|
||||
driver = webdriver.Chrome(options=options)
|
||||
return driver
|
||||
|
||||
|
||||
def assert_times_almost_equal(first_time, second_time):
|
||||
assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0)
|
||||
|
||||
|
||||
def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url):
|
||||
# Test the meta tag is in place
|
||||
response = client.get(event_url, follow=True, HTTP_HOST="example.com")
|
||||
assertContains(response, "application/json+oembed")
|
||||
assertContains(response, oembed_url)
|
||||
# Test that the JSON exists
|
||||
response = client.get(oembed_url, follow=True, HTTP_HOST="example.com")
|
||||
assert response.status_code == 200
|
||||
assertContains(response, event_embed_url)
|
||||
# Should also work for non-existant events
|
||||
response = client.get(alt_oembed_url, follow=True, HTTP_HOST="example.com")
|
||||
assert response.status_code == 200
|
||||
assertContains(response, alt_event_embed_url)
|
||||
|
||||
|
||||
def login(client, django_user_model):
|
||||
pwd = "testuser"
|
||||
usr = "TestUser"
|
||||
user = django_user_model.objects.create_user(
|
||||
username=usr, email="TestUser@test.com", password=pwd, is_superuser=True, is_active=True, is_staff=True
|
||||
)
|
||||
assert client.login(username=usr, password=pwd)
|
||||
return user
|
||||
@@ -1,152 +0,0 @@
|
||||
import pytest
|
||||
from django.core.management import call_command
|
||||
from django.urls import URLPattern, URLResolver
|
||||
from django.urls import reverse
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
from pytest_django.asserts import assertRedirects, assertContains, assertNotContains
|
||||
from pytest_django.asserts import assertTemplateUsed
|
||||
|
||||
from PyRIGS import urls
|
||||
from RIGS.models import Event
|
||||
from assets.models import Asset
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
|
||||
def find_urls_recursive(patterns):
|
||||
urls_to_check = []
|
||||
for url in patterns:
|
||||
if isinstance(url, URLResolver):
|
||||
urls_to_check += find_urls_recursive(url.url_patterns)
|
||||
elif isinstance(url, URLPattern):
|
||||
# Skip some things that actually don't need auth (mainly OEmbed JSONs that are essentially just a redirect)
|
||||
if url.name is not None and url.name != "closemodal" and "json" not in str(url):
|
||||
urls_to_check.append(url)
|
||||
return urls_to_check
|
||||
|
||||
|
||||
def get_request_url(url):
|
||||
pattern = str(url.pattern)
|
||||
try:
|
||||
kwargz = {}
|
||||
if ":pk>" in pattern:
|
||||
kwargz["pk"] = 1
|
||||
if ":model>" in pattern:
|
||||
kwargz["model"] = "event"
|
||||
return reverse(url.name, kwargs=kwargz)
|
||||
except NoReverseMatch:
|
||||
print("Couldn't test url " + pattern)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"command",
|
||||
[
|
||||
"generateSampleAssetsData",
|
||||
"generateSampleRIGSData",
|
||||
"generateSampleUserData",
|
||||
"deleteSampleData",
|
||||
"generateSampleTrainingData",
|
||||
"generate_sample_training_users",
|
||||
],
|
||||
)
|
||||
def test_production_exception(command):
|
||||
from django.core.management.base import CommandError
|
||||
|
||||
with pytest.raises(CommandError, match=".*production"):
|
||||
call_command(command)
|
||||
|
||||
|
||||
class TestSampleDataGenerator(TestCase):
|
||||
@override_settings(DEBUG=True)
|
||||
def test_sample_data(self):
|
||||
call_command("generateSampleData")
|
||||
assert Asset.objects.all().count() > 50
|
||||
assert Event.objects.all().count() > 100
|
||||
call_command("deleteSampleData")
|
||||
assert not Asset.objects.all().exists()
|
||||
assert not Event.objects.all().exists()
|
||||
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@pytest.mark.skip(reason="broken")
|
||||
def test_unauthenticated(client): # Nothing should be available to the unauthenticated
|
||||
call_command("generateSampleData")
|
||||
for url in find_urls_recursive(urls.urlpatterns):
|
||||
request_url = get_request_url(url)
|
||||
if request_url and "user" not in request_url: # User module is full of edge cases
|
||||
response = client.get(request_url, follow=True, HTTP_HOST="example.com")
|
||||
assertContains(response, "Login")
|
||||
if "application/json+oembed" in response.content.decode():
|
||||
assertTemplateUsed(response, "login_redirect.html")
|
||||
else:
|
||||
if "embed" in str(url):
|
||||
expected_url = f"{reverse('login_embed')}?next={request_url}"
|
||||
else:
|
||||
expected_url = f"{reverse('login')}?next={request_url}"
|
||||
assertRedirects(response, expected_url)
|
||||
call_command("deleteSampleData")
|
||||
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@pytest.mark.skip(reason="broken")
|
||||
def test_basic_access(client):
|
||||
call_command("generateSampleData")
|
||||
assert client.login(username="basic", password="basic")
|
||||
|
||||
url = reverse("asset_list")
|
||||
response = client.get(url)
|
||||
# Check edit and duplicate buttons NOT shown in list
|
||||
assertNotContains(response, "Edit")
|
||||
assertNotContains(
|
||||
response, "Duplicate"
|
||||
) # If this line is randomly failing, check the debug toolbar HTML hasn't crept in
|
||||
|
||||
url = reverse("asset_detail", kwargs={"pk": Asset.objects.first().asset_id})
|
||||
response = client.get(url)
|
||||
assertNotContains(response, "Purchase Details")
|
||||
assertNotContains(response, "View Revision History")
|
||||
|
||||
urlz = {"asset_history", "asset_update", "asset_duplicate"}
|
||||
for url_name in urlz:
|
||||
request_url = reverse(url_name, kwargs={"pk": Asset.objects.first().asset_id})
|
||||
response = client.get(request_url, follow=True)
|
||||
assert response.status_code == 403
|
||||
|
||||
request_url = reverse("supplier_create")
|
||||
response = client.get(request_url, follow=True)
|
||||
assert response.status_code == 403
|
||||
|
||||
request_url = reverse("supplier_update", kwargs={"pk": 1})
|
||||
response = client.get(request_url, follow=True)
|
||||
assert response.status_code == 403
|
||||
client.logout()
|
||||
call_command("deleteSampleData")
|
||||
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@pytest.mark.skip(reason="broken")
|
||||
def test_keyholder_access(client):
|
||||
call_command("generateSampleData")
|
||||
assert client.login(username="keyholder", password="keyholder")
|
||||
|
||||
url = reverse("asset_list")
|
||||
response = client.get(url)
|
||||
# Check edit and duplicate buttons shown in list
|
||||
assertContains(response, "Edit")
|
||||
assertContains(response, "Duplicate")
|
||||
|
||||
url = reverse("asset_detail", kwargs={"pk": Asset.objects.first().asset_id})
|
||||
response = client.get(url)
|
||||
assertContains(response, "Purchase Details")
|
||||
assertContains(response, "View Revision History")
|
||||
client.logout()
|
||||
call_command("deleteSampleData")
|
||||
|
||||
|
||||
def test_search(admin_client, admin_user):
|
||||
url = reverse("search")
|
||||
response = admin_client.get(url, {"q": "Definetelynothingfoundifwesearchthis"})
|
||||
assertContains(response, "No results found")
|
||||
response = admin_client.get(url, {"q": admin_user.first_name})
|
||||
assertContains(response, admin_user.first_name)
|
||||
@@ -1,29 +1,24 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls import patterns, include, url
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.urls import include, path
|
||||
from django.views.generic import TemplateView
|
||||
from django.conf import settings
|
||||
from registration.backends.default.views import RegistrationView
|
||||
import RIGS
|
||||
from RIGS import regbackend
|
||||
|
||||
from PyRIGS import views
|
||||
urlpatterns = patterns('',
|
||||
# Examples:
|
||||
# url(r'^$', 'PyRIGS.views.home', name='home'),
|
||||
# url(r'^blog/', include('blog.urls')),
|
||||
|
||||
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"),
|
||||
# API
|
||||
path("api/<str:model>/", login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
|
||||
path("api/<str:model>/<int:pk>/", login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
|
||||
path("closemodal/", views.CloseModal.as_view(), name="closemodal"),
|
||||
path("search/", login_required(views.Search.as_view()), name="search"),
|
||||
path("search_help/", login_required(views.SearchHelp.as_view()), name="search_help"),
|
||||
path("", include("users.urls")),
|
||||
path("admin/", include("massadmin.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
|
||||
]
|
||||
url(r'^', include('RIGS.urls')),
|
||||
url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail),
|
||||
name="registration_register"),
|
||||
url('^user/', include('django.contrib.auth.urls')),
|
||||
url('^user/', include('registration.backends.default.urls')),
|
||||
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
)
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
402
PyRIGS/views.py
402
PyRIGS/views.py
@@ -1,402 +0,0 @@
|
||||
import datetime
|
||||
import operator
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
from functools import reduce
|
||||
from itertools import chain
|
||||
from io import BytesIO
|
||||
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
from z3c.rml import rml2pdf
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core import serializers
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse_lazy, reverse, NoReverseMatch
|
||||
from django.views import generic
|
||||
from django.template.loader import get_template
|
||||
from django.utils import timezone
|
||||
|
||||
from RIGS import models
|
||||
from assets import models as asset_models
|
||||
from training import models as training_models
|
||||
|
||||
|
||||
def is_ajax(request):
|
||||
return request.headers.get("x-requested-with") == "XMLHttpRequest"
|
||||
|
||||
|
||||
def ajax_context(request):
|
||||
"""Template context processor exposing ``is_ajax`` to every template."""
|
||||
return {"is_ajax": is_ajax(request)}
|
||||
|
||||
|
||||
def version_context(request):
|
||||
"""Template context processor exposing the running commit to every template."""
|
||||
sha = settings.GIT_SHA
|
||||
return {"git_sha": sha, "git_sha_short": sha[:7]}
|
||||
|
||||
|
||||
def get_related(
|
||||
form, context
|
||||
): # Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
||||
for field, model in form.related_models.items():
|
||||
value = form[field].value()
|
||||
if value is not None and value != "":
|
||||
context[field] = model.objects.get(pk=value)
|
||||
|
||||
|
||||
class Index(generic.TemplateView): # Displays the current rig count along with a few other bits and pieces
|
||||
template_name = "index.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["rig_count"] = models.Event.objects.rig_count()
|
||||
context["now"] = (
|
||||
models.Event.objects.events_in_bounds(timezone.now(), timezone.now())
|
||||
.exclude(status=models.Event.CANCELLED)
|
||||
.filter(is_rig=True, dry_hire=False)
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class SecureAPIRequest(generic.View):
|
||||
models = {
|
||||
"venue": models.Venue,
|
||||
"person": models.Person,
|
||||
"organisation": models.Organisation,
|
||||
"profile": models.Profile,
|
||||
"event": models.Event,
|
||||
"asset": asset_models.Asset,
|
||||
"supplier": asset_models.Supplier,
|
||||
"training_item": training_models.TrainingItem,
|
||||
}
|
||||
|
||||
perms = {
|
||||
"venue": "RIGS.view_venue",
|
||||
"person": "RIGS.view_person",
|
||||
"organisation": "RIGS.view_organisation",
|
||||
"profile": "RIGS.view_profile",
|
||||
"event": None,
|
||||
"asset": None,
|
||||
"supplier": None,
|
||||
"training_item": None,
|
||||
}
|
||||
|
||||
"""
|
||||
Validate the request is allowed based on user permissions.
|
||||
Raises 403 if denied.
|
||||
Potential to add API key validation at a later date.
|
||||
"""
|
||||
|
||||
def __validate__(self, request, key, perm):
|
||||
if request.user.is_active:
|
||||
if request.user.is_superuser or perm is None:
|
||||
return True
|
||||
elif request.user.has_perm(perm):
|
||||
return True
|
||||
raise PermissionDenied()
|
||||
|
||||
def get(self, request, model, pk=None, param=None):
|
||||
# Request permission validation things
|
||||
key = request.GET.get("apikey", None)
|
||||
perm = self.perms[model]
|
||||
self.__validate__(request, key, perm)
|
||||
|
||||
# Response format where applicable
|
||||
format = request.GET.get("format", "json")
|
||||
fields = request.GET.get("fields", None)
|
||||
if fields:
|
||||
fields = fields.split(",")
|
||||
filters = request.GET.get("filters", [])
|
||||
if filters:
|
||||
filters = filters.split(",")
|
||||
|
||||
# Supply data for one record
|
||||
if pk:
|
||||
object = get_object_or_404(self.models[model], pk=pk)
|
||||
data = serializers.serialize(format, [object], fields=fields)
|
||||
return HttpResponse(data, content_type="application/" + format)
|
||||
|
||||
# Supply data for autocomplete ajax request in json form
|
||||
term = request.GET.get("q", None)
|
||||
if term:
|
||||
if fields is None: # Default to just name
|
||||
fields = ["name"]
|
||||
|
||||
# Build a list of Q objects for use later
|
||||
queries = []
|
||||
for part in term.split(" "):
|
||||
qs = []
|
||||
for field in fields:
|
||||
q = Q(**{field + "__icontains": part})
|
||||
qs.append(q)
|
||||
|
||||
queries.append(reduce(operator.or_, qs))
|
||||
|
||||
for f in filters:
|
||||
q = Q(**{f: True})
|
||||
queries.append(q)
|
||||
|
||||
# Build the data response list
|
||||
results = []
|
||||
query = reduce(operator.and_, queries)
|
||||
objects = self.models[model].objects.filter(query)
|
||||
# Returning unactivated or unapproved users when they are elsewhere filtered out of the default queryset leads to some *very* unexpected results
|
||||
if model == "profile":
|
||||
objects = objects.filter(is_active=True, is_approved=True)
|
||||
for o in objects:
|
||||
name = o.display_name if hasattr(o, "display_name") else o.name
|
||||
data = {
|
||||
"pk": o.pk,
|
||||
"value": o.pk,
|
||||
"text": name,
|
||||
}
|
||||
try: # See if there is a valid update URL
|
||||
data["update"] = reverse(f"{model}_update", kwargs={"pk": o.pk})
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
results.append(data)
|
||||
|
||||
# return a data response
|
||||
return JsonResponse(results, safe=False)
|
||||
|
||||
start = request.GET.get("start", None)
|
||||
end = request.GET.get("end", None)
|
||||
|
||||
if model == "event" and start and end:
|
||||
# Probably a calendar request
|
||||
start_datetime = datetime.datetime.strptime(start, "%Y-%m-%dT%H:%M:%S")
|
||||
end_datetime = datetime.datetime.strptime(end, "%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
objects = self.models[model].objects.events_in_bounds(start_datetime, end_datetime)
|
||||
|
||||
results = []
|
||||
for item in objects:
|
||||
data = {
|
||||
"pk": item.pk,
|
||||
"title": item.name,
|
||||
"is_rig": item.is_rig,
|
||||
"status": str(item.get_status_display()),
|
||||
"earliest": item.earliest_time.isoformat(),
|
||||
"latest": item.latest_time.isoformat(),
|
||||
"url": str(item.get_absolute_url()),
|
||||
}
|
||||
|
||||
results.append(data)
|
||||
return JsonResponse(results, safe=False)
|
||||
|
||||
return HttpResponse(model)
|
||||
|
||||
|
||||
class ModalURLMixin:
|
||||
def get_close_url(self, update, detail):
|
||||
if is_ajax(self.request):
|
||||
url = reverse_lazy("closemodal")
|
||||
update_url = str(reverse_lazy(update, kwargs={"pk": self.object.pk}))
|
||||
messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object]))
|
||||
messages.info(self.request, f"modalobject[0]['update_url']='{update_url}'")
|
||||
else:
|
||||
url = reverse_lazy(
|
||||
detail,
|
||||
kwargs={
|
||||
"pk": self.object.pk,
|
||||
},
|
||||
)
|
||||
return url
|
||||
|
||||
|
||||
class GenericListView(generic.ListView):
|
||||
template_name = "generic_list.html"
|
||||
paginate_by = 20
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_title"] = self.model.__name__ + "s"
|
||||
if is_ajax(self.request):
|
||||
context["override"] = "base_ajax.html"
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
object_list = self.model.objects.search(query=self.request.GET.get("q", ""))
|
||||
|
||||
orderBy = self.request.GET.get("orderBy", "name")
|
||||
if orderBy != "":
|
||||
object_list = object_list.order_by(orderBy)
|
||||
return object_list
|
||||
|
||||
|
||||
class GenericDetailView(generic.DetailView):
|
||||
template_name = "generic_detail.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_title"] = f"{self.model.__name__} | {self.object.name}"
|
||||
if is_ajax(self.request):
|
||||
context["override"] = "base_ajax.html"
|
||||
return context
|
||||
|
||||
|
||||
class GenericUpdateView(generic.UpdateView):
|
||||
template_name = "generic_form.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_title"] = f"Edit {self.model.__name__}"
|
||||
if is_ajax(self.request):
|
||||
context["override"] = "base_ajax.html"
|
||||
return context
|
||||
|
||||
|
||||
class GenericCreateView(generic.CreateView):
|
||||
template_name = "generic_form.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_title"] = f"Create {self.model.__name__}"
|
||||
if is_ajax(self.request):
|
||||
context["override"] = "base_ajax.html"
|
||||
return context
|
||||
|
||||
|
||||
class Search(generic.ListView):
|
||||
template_name = "search_results.html"
|
||||
paginate_by = 20
|
||||
count = 0
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context["count"] = self.count or 0
|
||||
context["query"] = self.request.GET.get("q")
|
||||
context["page_title"] = f"{context['count']} search results for <b>{context['query']}</b>"
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
request = self.request
|
||||
query = request.GET.get("q", None)
|
||||
|
||||
if query is not None:
|
||||
event_results = models.Event.objects.search(query)
|
||||
person_results = models.Person.objects.search(query)
|
||||
organisation_results = models.Organisation.objects.search(query)
|
||||
venue_results = models.Venue.objects.search(query)
|
||||
invoice_results = models.Invoice.objects.search(query)
|
||||
asset_results = asset_models.Asset.objects.search(query)
|
||||
supplier_results = asset_models.Supplier.objects.search(query)
|
||||
trainee_results = training_models.Trainee.objects.search(query)
|
||||
training_item_results = training_models.TrainingItem.objects.search(query)
|
||||
|
||||
# combine querysets
|
||||
queryset_chain = chain(
|
||||
event_results,
|
||||
person_results,
|
||||
organisation_results,
|
||||
venue_results,
|
||||
invoice_results,
|
||||
asset_results,
|
||||
supplier_results,
|
||||
trainee_results,
|
||||
training_item_results,
|
||||
)
|
||||
qs = sorted(queryset_chain, key=lambda instance: instance.pk, reverse=True)
|
||||
self.count = len(qs) # since qs is actually a list
|
||||
return qs
|
||||
return models.Event.objects.none() # just an empty queryset as default
|
||||
|
||||
|
||||
class SearchHelp(generic.TemplateView):
|
||||
template_name = "search_help.html"
|
||||
|
||||
|
||||
class CloseModal(generic.TemplateView):
|
||||
"""
|
||||
Called from a modal window (e.g. when an item is submitted to an event/invoice).
|
||||
May optionally also include some javascript in a success message to cause a load of
|
||||
the new information onto the page.
|
||||
"""
|
||||
|
||||
template_name = "closemodal.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return {"messages": messages.get_messages(self.request)}
|
||||
|
||||
|
||||
class OEmbedView(generic.View):
|
||||
def get(self, request, pk=None):
|
||||
embed_url = reverse(self.url_name, args=[pk])
|
||||
full_url = f"{request.scheme}://{request.META['HTTP_HOST']}{embed_url}"
|
||||
|
||||
data = {
|
||||
"html": f'<iframe src="{full_url}" frameborder="0" width="100%" height="250"></iframe>',
|
||||
"version": "1.0",
|
||||
"type": "rich",
|
||||
"height": "250",
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
def get_info_string(user):
|
||||
user_str = f"by {user.name} " if user else ""
|
||||
time = timezone.now().strftime("%d/%m/%Y %H:%I")
|
||||
return f"[Paperwork generated {user_str}on {time}"
|
||||
|
||||
|
||||
def render_pdf_response(template, context, append_terms):
|
||||
merger = PdfWriter()
|
||||
rml = template.render(context)
|
||||
buffer = rml2pdf.parseString(rml)
|
||||
merger.append(PdfReader(buffer))
|
||||
buffer.close()
|
||||
|
||||
if append_terms:
|
||||
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
|
||||
merger.append(BytesIO(terms.read()))
|
||||
|
||||
merged = BytesIO()
|
||||
merger.write(merged)
|
||||
|
||||
response = HttpResponse(content_type="application/pdf")
|
||||
f = context["filename"]
|
||||
response["Content-Disposition"] = f'filename="{f}"'
|
||||
response.write(merged.getvalue())
|
||||
return response
|
||||
|
||||
|
||||
class PrintView(generic.View):
|
||||
append_terms = False
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
obj = get_object_or_404(self.model, pk=self.kwargs["pk"])
|
||||
object_name = re.sub(r"[^a-zA-Z0-9 \n\.]", "", obj.name)
|
||||
|
||||
context = {
|
||||
"object": obj,
|
||||
"current_user": self.request.user,
|
||||
"object_name": object_name,
|
||||
"info_string": get_info_string(self.request.user) + f"- {obj.current_version_id}]",
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
def get(self, request, pk):
|
||||
return render_pdf_response(get_template(self.template_name), self.get_context_data(), self.append_terms)
|
||||
|
||||
|
||||
class PrintListView(generic.ListView):
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context["current_user"] = self.request.user
|
||||
context["info_string"] = get_info_string(self.request.user) + "]"
|
||||
return context
|
||||
|
||||
def get(self, request):
|
||||
self.object_list = self.get_queryset()
|
||||
return render_pdf_response(get_template(self.template_name), self.get_context_data(), False)
|
||||
@@ -6,11 +6,10 @@ It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "PyRIGS.settings")
|
||||
|
||||
from django.core.wsgi import get_wsgi_application # noqa
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
from dj_static import Cling
|
||||
|
||||
application = get_wsgi_application()
|
||||
application = Cling(get_wsgi_application())
|
||||
|
||||
151
README.md
151
README.md
@@ -1,73 +1,78 @@
|
||||
# TEC PA & Lighting - PyRIGS #
|
||||

|
||||
[](https://coveralls.io/github/nottinghamtec/PyRIGS)
|
||||
[](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability)
|
||||
|
||||
Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets.
|
||||
|
||||
For setup information and other such helpful stuff check the [Wiki](https://github.com/nottinghamtec/PyRIGS/wiki)
|
||||
|
||||
# Apps
|
||||
- PyRIGS: Base app, stores 'global' information
|
||||
- RIGS: Rigboard stuff - event calendar etc
|
||||
- assets: Database of our kit, testing data etc
|
||||
- training: Logs in-house training within various "departments" (sound, lighting etc).
|
||||
- versioning: Our custom logic built on top of django-reversion. Semi-modular.
|
||||
- users: Our custom logic for registration and profiles. Semi-modular.
|
||||
|
||||
# Running locally
|
||||
> [!WARNING]
|
||||
> `compose.yml` is for **development only** and must not be run in production. It mounts the source into the container, runs Django's development server with `DEBUG` on by default, and uses placeholder captcha keys. Production is deployed from the image built by the `prod` target of the `Dockerfile`.
|
||||
|
||||
The compose stack runs Postgres, the Django development server and a gulp watcher that rebuilds the CSS/JS. Changes to Python code and templates reload the app automatically, and changes to `pipeline/source_assets` are rebuilt into `pipeline/built_assets` (refresh the browser to pick them up).
|
||||
|
||||
1. Copy `.env.example` to `.env` and fill it in. For local use set at least:
|
||||
```
|
||||
DEBUG=true
|
||||
DJANGO_ALLOWED_HOSTS=localhost
|
||||
```
|
||||
The sample data commands refuse to run unless `DEBUG` (or `STAGING`) is true.
|
||||
2. Start the stack (migrations run automatically on start):
|
||||
```
|
||||
docker compose up -d --build
|
||||
```
|
||||
The first start installs the node modules, so give the assets a minute to appear. After changing `pyproject.toml`, `uv.lock` or the `Dockerfile`, rebuild with `docker compose up -d --build` (or run `docker compose watch`).
|
||||
3. Open <http://localhost:8000/>.
|
||||
|
||||
## Creating a user
|
||||
```
|
||||
docker compose exec pyrigs python manage.py createsuperuser
|
||||
```
|
||||
|
||||
## Sample data
|
||||
Load sample users, events, assets and training data (run once on a fresh database):
|
||||
```
|
||||
docker compose exec pyrigs python manage.py generateSampleData
|
||||
```
|
||||
The individual commands (`generateSampleUserData`, `generateSampleRIGSData`, `generateSampleAssetsData`, `generateSampleTrainingData`) can be run separately, but run the user one first. To remove the sample data again (requires `DEBUG=true`):
|
||||
```
|
||||
docker compose exec pyrigs python manage.py deleteSampleData
|
||||
```
|
||||
|
||||
The sample data creates these logins (the password is the same as the username):
|
||||
|
||||
| Username | Password | Access |
|
||||
|---|---|---|
|
||||
| `superuser` | `superuser` | Superuser |
|
||||
| `finance` | `finance` | Finance and Keyholders groups |
|
||||
| `hs` | `hs` | H&S and Keyholders groups |
|
||||
| `keyholder` | `keyholder` | Keyholders group |
|
||||
| `basic` | `basic` | Approved user, no groups |
|
||||
|
||||
It also creates a number of generic profiles (e.g. `AmyPond`, `RoryWilliams`) which have no password set, so they can't be logged into.
|
||||
|
||||
|
||||
# Development
|
||||
Linting and formatting use [ruff](https://docs.astral.sh/ruff/), run through [prek](https://github.com/j178/prek) (a pre-commit compatible hook runner) using `.pre-commit-config.yaml`:
|
||||
```
|
||||
uv sync
|
||||
uv run prek install # run the hooks on every commit
|
||||
uv run prek run --all-files
|
||||
```
|
||||
|
||||
[](https://forthebadge.com) [](https://forthebadge.com)
|
||||
# TEC PA & Lighting - PyRIGS #
|
||||
[](https://app.wercker.com/project/bykey/2dbe0517c3d83859c985ffc5a55a2802)
|
||||
|
||||
Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails.
|
||||
|
||||
The purpose of this project is to make the system more compatible and easier to understand such that should future changes be needed they can be made without having to understand the intricacies of Rails.
|
||||
|
||||
At this stage the project is very early on, and the main focus has been on getting a working system that can be tested and put into use ASAP due to the imminent failure of the existing system. Because of this, the documentation is still quite weak, but this should be fixed as time goes on.
|
||||
|
||||
This document is intended to get you up and running, but if don't care about what I have to say, just clone the sodding repository and have a poke around with what's in it, but for GODS SAKE DO NOT PUSH WITHOUT TESTING.
|
||||
|
||||
### What is this repository for? ###
|
||||
For the rapid development of the application for medium term deployment, the main branch is being used.
|
||||
Once the application is deployed in a production environment, other branches should be used to properly stage edits and pushes of new features. When a significant feature is developed on a branch, raise a pull request and it can be reviewed before being put into production.
|
||||
|
||||
Most of the documents here assume a basic knowledge of how Python and Django work (hint, if I don't say something, Google it, you will find 10000's of answers). The documentation is purely to be specific to TEC's application of the framework.
|
||||
|
||||
### Editing ###
|
||||
It is recommended that you use the PyCharm IDE by JetBrains. Whilst other editors are available, this is the best for integration with Django as it can automatically manage all the pesky admin commands that frequently need running, as well as nice integration with git.
|
||||
|
||||
For the more experienced developer/somebody who doesn't want a full IDE and wants it to open in less than the age of the universe, I can strongly recommend [Sublime Text](http://www.sublimetext.com/). It has a bit of a steeper learning curve, and won't manage anything Django/git related out of the box, but once you get the hang of it is by far the fastest and most powerful editor I have used (for any type of project).
|
||||
|
||||
Please contact TJP for details on how to acquire these.
|
||||
|
||||
### Python Environment ###
|
||||
Whilst the Python version used is not critical to the running of the application, using the same version usually helps avoid a lot of issues. Mainly the C implementation of Python 2 (CPython 2) has been used (specifically the Python 2.7 standard). Most of the application has been written with Python 3 in mind however, and should run without issue. Some level of testing on Python 3 has been done, but there is no guarantee it will work (for more information on this please see [[Python Version]] on the wiki)
|
||||
|
||||
Once you have your Python distribution installed, go ahead an follow the steps to set up a virtualenv, which will isolate the project from the system environment.
|
||||
|
||||
#### PyCharm ####
|
||||
If you are using the prefered PyCharm IDE, then this should be quite easy.
|
||||
|
||||
1. Select "File/Settings" -> "Project Interpreter"
|
||||
2. Click the small cog in the top right
|
||||
3. Select "Create VirtualEnv"
|
||||
4. Enter a name and a location. This doesn't matter where, just make sure it makes sense and you remember it incase you need it later (I recommend calling it "pyrigs" in "~/.virtualenvs/pyrigs")
|
||||
5. Select the base interpreter to your Python 3 base interpreter (Python 2 will work, just be careful)
|
||||
6. Click OK, you *don't* want to inherit global packages or make it available to all projects.
|
||||
7. Open a file such as manage.py. PyCharm should winge that dependances aren't installed. This might take a while to register, but give it change. When it does, click the button to install them and let it do it's thing. If for some reason PyCharm should decide that it doesn't want to help you here, see below for the console instructions on how to do this manually.
|
||||
|
||||
To run the Django application follow these steps
|
||||
|
||||
1. Select "Run/Edit Configurations"
|
||||
2. Create a new "Django server", give it a sensible name for when you need it later.
|
||||
3. You might need to set the interpreter to be your virtualenv.
|
||||
4. Click "OK"
|
||||
5. Run the application
|
||||
|
||||
#### Console Based ####
|
||||
If you aren't using PyCharm, or want to use a console for some reason, this is really easy, there is even [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) to help things along. Simply run
|
||||
```
|
||||
virtualenv <dir>
|
||||
```
|
||||
Where dir is the directory you wish to create the virtualenv in.
|
||||
|
||||
Next activate the virtualenv.
|
||||
```
|
||||
Windows
|
||||
<virtualenv_dir>/Scripts/activate.bat
|
||||
|
||||
Unix
|
||||
source <virtualenv_dir>/bin/activate
|
||||
```
|
||||
Finally install the requirements using pip
|
||||
```
|
||||
cd <pyrigs project directory>
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
This might take a while, but be patient and you should then be ready to go.
|
||||
|
||||
To run the server under normal conditions when you are already in the virtualenv (see above)
|
||||
```
|
||||
python manage.py runserver
|
||||
```
|
||||
Please refer to Django documentation for a full list of options available here.
|
||||
|
||||
### Committing, pushing and testing ###
|
||||
Feel free to commit as you wish, on your own branch. On my branch (master for development) do not commit code that you either know doesn't work or don't know works. If you must commit this code, please make sure you say in the commit message that it isn't working, and if you can why it isn't working. If and only if you absolutely must push, then please don't leave it as the HEAD for too long, it's not much to ask but when you are done just make sure you haven't broken the HEAD for the next person.
|
||||
|
||||
240
RIGS/admin.py
240
RIGS/admin.py
@@ -1,133 +1,81 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib import messages
|
||||
from django.contrib.admin import helpers
|
||||
from RIGS import models, forms
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import reversion
|
||||
|
||||
from django.contrib.admin import helpers
|
||||
from django.template.response import TemplateResponse
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Count
|
||||
from django.forms import ModelForm
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db import IntegrityError
|
||||
from reversion import revisions as reversion
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from RIGS import models
|
||||
from users import forms as user_forms
|
||||
# Register your models here.
|
||||
admin.site.register(models.VatRate, reversion.VersionAdmin)
|
||||
admin.site.register(models.Event, reversion.VersionAdmin)
|
||||
admin.site.register(models.EventItem, reversion.VersionAdmin)
|
||||
admin.site.register(models.Invoice)
|
||||
admin.site.register(models.Payment)
|
||||
|
||||
|
||||
admin.site.register(models.VatRate, VersionAdmin)
|
||||
admin.site.register(models.Event, VersionAdmin)
|
||||
admin.site.register(models.EventItem, VersionAdmin)
|
||||
admin.site.register(models.Invoice, VersionAdmin)
|
||||
admin.site.register(models.EventCheckIn)
|
||||
@admin.register(models.Profile)
|
||||
class ProfileAdmin(UserAdmin):
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password')}),
|
||||
(_('Personal info'), {
|
||||
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
|
||||
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
|
||||
'groups', 'user_permissions')}),
|
||||
(_('Important dates'), {
|
||||
'fields': ('last_login', 'date_joined')}),
|
||||
)
|
||||
add_fieldsets = (
|
||||
(None, {
|
||||
'classes': ('wide',),
|
||||
'fields': ('username', 'password1', 'password2'),
|
||||
}),
|
||||
)
|
||||
form = forms.ProfileChangeForm
|
||||
add_form = forms.ProfileCreationForm
|
||||
|
||||
|
||||
@transaction.atomic() # Copied from django-extensions. GenericForeignKey support removed as unnecessary.
|
||||
def merge_model_instances(primary_object, alias_objects):
|
||||
"""
|
||||
Merge several model instances into one, the `primary_object`.
|
||||
Use this function to merge model objects and migrate all of the related
|
||||
fields from the alias objects the primary object.
|
||||
"""
|
||||
class AssociateAdmin(reversion.VersionAdmin):
|
||||
list_display = ('id', 'name', 'number_of_events')
|
||||
search_fields = ['id', 'name']
|
||||
list_display_links = ['id', 'name']
|
||||
actions = ['merge']
|
||||
|
||||
# get related fields
|
||||
related_fields = list(filter(lambda x: x.is_relation is True, primary_object._meta.get_fields()))
|
||||
|
||||
many_to_many_fields = list(filter(lambda x: x.many_to_many is True, related_fields))
|
||||
|
||||
related_fields = list(filter(lambda x: x.many_to_many is False, related_fields))
|
||||
|
||||
# Loop through all alias objects and migrate their references to the
|
||||
# primary object
|
||||
deleted_objects = []
|
||||
deleted_objects_count = 0
|
||||
for alias_object in alias_objects:
|
||||
# Migrate all foreign key references from alias object to primary
|
||||
# object.
|
||||
for many_to_many_field in many_to_many_fields:
|
||||
alias_varname = many_to_many_field.name
|
||||
related_objects = getattr(alias_object, alias_varname)
|
||||
for obj in related_objects.all():
|
||||
try:
|
||||
# Handle regular M2M relationships.
|
||||
getattr(alias_object, alias_varname).remove(obj)
|
||||
getattr(primary_object, alias_varname).add(obj)
|
||||
except AttributeError:
|
||||
# Handle M2M relationships with a 'through' model.
|
||||
# This does not delete the 'through model.
|
||||
# TODO: Allow the user to delete a duplicate 'through' model.
|
||||
through_model = getattr(alias_object, alias_varname).through
|
||||
kwargs = {
|
||||
many_to_many_field.m2m_reverse_field_name(): obj,
|
||||
many_to_many_field.m2m_field_name(): alias_object,
|
||||
}
|
||||
through_model_instances = through_model.objects.filter(**kwargs)
|
||||
for instance in through_model_instances:
|
||||
# Re-attach the through model to the primary_object
|
||||
setattr(instance, many_to_many_field.m2m_field_name(), primary_object)
|
||||
instance.save()
|
||||
# TODO: Here, try to delete duplicate instances that are
|
||||
# disallowed by a unique_together constraint
|
||||
|
||||
for related_field in related_fields:
|
||||
if related_field.one_to_many:
|
||||
with transaction.atomic():
|
||||
try:
|
||||
alias_varname = related_field.get_accessor_name()
|
||||
related_objects = getattr(alias_object, alias_varname)
|
||||
for obj in related_objects.all():
|
||||
field_name = related_field.field.name
|
||||
setattr(obj, field_name, primary_object)
|
||||
obj.save()
|
||||
except IntegrityError:
|
||||
pass # Skip to avoid integrity error from unique_together
|
||||
elif related_field.one_to_one or related_field.many_to_one:
|
||||
alias_varname = related_field.name
|
||||
if hasattr(alias_object, alias_varname):
|
||||
related_object = getattr(alias_object, alias_varname)
|
||||
primary_related_object = getattr(primary_object, alias_varname)
|
||||
if primary_related_object is None:
|
||||
setattr(primary_object, alias_varname, related_object)
|
||||
primary_object.save()
|
||||
elif related_field.one_to_one:
|
||||
related_object.delete()
|
||||
|
||||
if alias_object.id:
|
||||
deleted_objects += [alias_object]
|
||||
alias_object.delete()
|
||||
deleted_objects_count += 1
|
||||
|
||||
return primary_object, deleted_objects, deleted_objects_count
|
||||
|
||||
|
||||
class AssociateAdmin(VersionAdmin):
|
||||
search_fields = ["id", "name"]
|
||||
list_display_links = ["id", "name"]
|
||||
actions = ["merge"]
|
||||
merge_fields = ['name']
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).annotate(event_count=Count("event"))
|
||||
return super(AssociateAdmin, self).get_queryset(request).annotate(event_count=Count('event'))
|
||||
|
||||
def number_of_events(self, obj):
|
||||
return obj.latest_events.count()
|
||||
|
||||
number_of_events.admin_order_field = "event_count"
|
||||
number_of_events.admin_order_field = 'event_count'
|
||||
|
||||
def merge(self, request, queryset):
|
||||
if request.POST.get("post"): # Has the user confirmed which is the master record?
|
||||
if request.POST.get('post'): # Has the user confirmed which is the master record?
|
||||
try:
|
||||
master_object_pk = request.POST.get("master")
|
||||
master_object = queryset.get(pk=master_object_pk)
|
||||
masterObjectPk = request.POST.get('master')
|
||||
masterObject = queryset.get(pk=masterObjectPk)
|
||||
except ObjectDoesNotExist:
|
||||
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
|
||||
return
|
||||
|
||||
primary_object, deleted_objects, deleted_objects_count = merge_model_instances(
|
||||
master_object, queryset.exclude(pk=master_object_pk).all()
|
||||
)
|
||||
reversion.set_comment("Merging Objects")
|
||||
self.message_user(request, f"Objects successfully merged. {deleted_objects_count} old objects deleted.")
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
for obj in queryset.exclude(pk=masterObjectPk):
|
||||
events = obj.event_set.all()
|
||||
for event in events:
|
||||
masterObject.event_set.add(event)
|
||||
obj.delete()
|
||||
reversion.set_comment('Merging Objects')
|
||||
|
||||
self.message_user(request, "Objects successfully merged.")
|
||||
return
|
||||
else: # Present the confirmation screen
|
||||
|
||||
class TempForm(ModelForm):
|
||||
@@ -140,84 +88,28 @@ class AssociateAdmin(VersionAdmin):
|
||||
forms.append(TempForm(instance=obj))
|
||||
|
||||
context = {
|
||||
"title": _("Are you sure?"),
|
||||
"queryset": queryset,
|
||||
"action_checkbox_name": helpers.ACTION_CHECKBOX_NAME,
|
||||
"forms": forms,
|
||||
'title': _("Are you sure?"),
|
||||
'queryset': queryset,
|
||||
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
||||
'forms': forms
|
||||
}
|
||||
return TemplateResponse(request, "admin_associate_merge.html", context)
|
||||
|
||||
|
||||
@admin.register(models.Profile)
|
||||
class ProfileAdmin(UserAdmin, AssociateAdmin):
|
||||
list_display = (
|
||||
"username",
|
||||
"name",
|
||||
"is_approved",
|
||||
"is_superuser",
|
||||
"is_supervisor",
|
||||
"number_of_events",
|
||||
"last_login",
|
||||
"date_joined",
|
||||
)
|
||||
list_display_links = ["username"]
|
||||
list_filter = UserAdmin.list_filter + ("is_approved", "date_joined")
|
||||
fieldsets = (
|
||||
(None, {"fields": ("username", "password")}),
|
||||
(_("Personal info"), {"fields": ("first_name", "last_name", "email", "initials", "phone")}),
|
||||
(
|
||||
_("Permissions"),
|
||||
{"fields": ("is_approved", "is_active", "is_staff", "is_superuser", "groups", "user_permissions")},
|
||||
),
|
||||
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||
)
|
||||
add_fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"classes": ("wide",),
|
||||
"fields": ("username", "password1", "password2"),
|
||||
},
|
||||
),
|
||||
)
|
||||
form = user_forms.ProfileChangeForm
|
||||
add_form = user_forms.ProfileCreationForm
|
||||
actions = ["approve_user", "merge"]
|
||||
|
||||
merge_fields = ["username", "first_name", "last_name", "initials", "email", "phone", "is_supervisor"]
|
||||
|
||||
def approve_user(modeladmin, request, queryset):
|
||||
queryset.update(is_approved=True)
|
||||
return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context,
|
||||
current_app=self.admin_site.name)
|
||||
|
||||
|
||||
@admin.register(models.Person)
|
||||
class PersonAdmin(AssociateAdmin):
|
||||
list_display = ("id", "name", "phone", "email", "number_of_events")
|
||||
merge_fields = ["name", "phone", "email", "address", "notes"]
|
||||
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
|
||||
merge_fields = ['name', 'phone', 'email', 'address', 'notes']
|
||||
|
||||
|
||||
@admin.register(models.Venue)
|
||||
class VenueAdmin(AssociateAdmin):
|
||||
list_display = ("id", "name", "phone", "email", "number_of_events")
|
||||
merge_fields = ["name", "phone", "email", "address", "notes", "three_phase_available"]
|
||||
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
|
||||
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available']
|
||||
|
||||
|
||||
@admin.register(models.Organisation)
|
||||
class OrganisationAdmin(AssociateAdmin):
|
||||
list_display = ("id", "name", "phone", "email", "number_of_events")
|
||||
merge_fields = ["name", "phone", "email", "address", "notes", "union_account"]
|
||||
|
||||
|
||||
@admin.register(models.RiskAssessment)
|
||||
class RiskAssessmentAdmin(VersionAdmin):
|
||||
list_display = ("id", "event", "reviewed_at", "reviewed_by")
|
||||
|
||||
|
||||
@admin.register(models.EventChecklist)
|
||||
class EventChecklistAdmin(VersionAdmin):
|
||||
list_display = ("id", "event", "reviewed_at", "reviewed_by")
|
||||
|
||||
|
||||
@admin.register(models.PowerTestRecord)
|
||||
class PowerTestRecordAdmin(VersionAdmin):
|
||||
list_display = ("id", "event", "reviewed_at", "reviewed_by")
|
||||
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
|
||||
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account']
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RIGSAppConfig(AppConfig):
|
||||
name = "RIGS"
|
||||
|
||||
def ready(self):
|
||||
import RIGS.signals # noqa: F401 (registers signal handlers)
|
||||
173
RIGS/finance.py
Normal file
173
RIGS/finance.py
Normal file
@@ -0,0 +1,173 @@
|
||||
import cStringIO as StringIO
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.template.loader import get_template
|
||||
from django.views import generic
|
||||
from django.db.models import Q
|
||||
from z3c.rml import rml2pdf
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
class InvoiceIndex(generic.ListView):
|
||||
model = models.Invoice
|
||||
template_name = 'RIGS/invoice_list_active.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(InvoiceIndex, self).get_context_data(**kwargs)
|
||||
total = 0
|
||||
for i in context['object_list']:
|
||||
total += i.balance
|
||||
context['total'] = total
|
||||
context['count'] = len(list(context['object_list']))
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||
sql = "SELECT * FROM " \
|
||||
"(SELECT " \
|
||||
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
||||
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
||||
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
||||
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
||||
"AS sub " \
|
||||
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
||||
"ORDER BY invoice_date"
|
||||
|
||||
query = self.model.objects.raw(sql)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
class InvoiceDetail(generic.DetailView):
|
||||
model = models.Invoice
|
||||
|
||||
|
||||
class InvoicePrint(generic.View):
|
||||
def get(self, request, pk):
|
||||
invoice = get_object_or_404(models.Invoice, pk=pk)
|
||||
object = invoice.event
|
||||
template = get_template('RIGS/event_print.xml')
|
||||
copies = ('TEC', 'Client')
|
||||
context = RequestContext(request, {
|
||||
'object': object,
|
||||
'fonts': {
|
||||
'opensans': {
|
||||
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
|
||||
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
|
||||
}
|
||||
},
|
||||
'invoice': invoice,
|
||||
'current_user': request.user,
|
||||
})
|
||||
|
||||
rml = template.render(context)
|
||||
buffer = StringIO.StringIO()
|
||||
|
||||
buffer = rml2pdf.parseString(rml)
|
||||
|
||||
pdfData = buffer.read()
|
||||
|
||||
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
|
||||
|
||||
response = HttpResponse(content_type='application/pdf')
|
||||
response['Content-Disposition'] = "filename=Invoice %05d | %s.pdf" % (invoice.pk, escapedEventName)
|
||||
response.write(pdfData)
|
||||
return response
|
||||
|
||||
|
||||
class InvoiceVoid(generic.View):
|
||||
def get(self, *args, **kwargs):
|
||||
pk = kwargs.get('pk')
|
||||
object = get_object_or_404(models.Invoice, pk=pk)
|
||||
object.void = not object.void
|
||||
object.save()
|
||||
|
||||
if object.void:
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_list'))
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
|
||||
|
||||
|
||||
class InvoiceArchive(generic.ListView):
|
||||
model = models.Invoice
|
||||
template_name = 'RIGS/invoice_list_archive.html'
|
||||
paginate_by = 25
|
||||
|
||||
|
||||
class InvoiceWaiting(generic.ListView):
|
||||
model = models.Event
|
||||
paginate_by = 25
|
||||
template_name = 'RIGS/event_invoice.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(InvoiceWaiting, self).get_context_data(**kwargs)
|
||||
total = 0
|
||||
for obj in self.get_objects():
|
||||
total += obj.sum_total
|
||||
context['total'] = total
|
||||
context['count'] = len(self.get_objects())
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_objects()
|
||||
|
||||
def get_objects(self):
|
||||
# @todo find a way to select items
|
||||
events = self.model.objects.filter(
|
||||
(
|
||||
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
||||
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
|
||||
) & Q(invoice__isnull=True) # Has not already been invoiced
|
||||
& 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
|
||||
|
||||
|
||||
class InvoiceEvent(generic.View):
|
||||
def get(self, *args, **kwargs):
|
||||
epk = kwargs.get('pk')
|
||||
event = models.Event.objects.get(pk=epk)
|
||||
invoice, created = models.Invoice.objects.get_or_create(event=event)
|
||||
|
||||
if created:
|
||||
invoice.invoice_date = datetime.date.today()
|
||||
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))
|
||||
|
||||
|
||||
class PaymentCreate(generic.CreateView):
|
||||
model = models.Payment
|
||||
fields = ['invoice', 'date', 'amount', 'method']
|
||||
|
||||
def get_initial(self):
|
||||
initial = super(generic.CreateView, self).get_initial()
|
||||
invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None))
|
||||
if invoicepk == None:
|
||||
raise Http404()
|
||||
invoice = get_object_or_404(models.Invoice, pk=invoicepk)
|
||||
initial.update({'invoice': invoice})
|
||||
return initial
|
||||
|
||||
def get_success_url(self):
|
||||
messages.info(self.request, "location.reload()")
|
||||
return reverse_lazy('closemodal')
|
||||
|
||||
|
||||
class PaymentDelete(generic.DeleteView):
|
||||
model = models.Payment
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.POST.get('next')
|
||||
278
RIGS/forms.py
278
RIGS/forms.py
@@ -1,73 +1,95 @@
|
||||
from datetime import timedelta
|
||||
|
||||
import simplejson
|
||||
__author__ = 'Ghost'
|
||||
from django import forms
|
||||
from django.utils import formats
|
||||
from django.conf import settings
|
||||
from django.core import serializers
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
||||
from registration.forms import RegistrationFormUniqueEmail
|
||||
from captcha.fields import ReCaptchaField
|
||||
import simplejson
|
||||
|
||||
from RIGS import models
|
||||
from training.models import TrainingLevel
|
||||
|
||||
# Override the django form defaults to use the HTML date/time/datetime UI elements
|
||||
forms.DateField.widget = forms.DateInput(attrs={"type": "date"})
|
||||
forms.TimeField.widget = forms.TimeInput(attrs={"type": "time"}, format="%H:%M")
|
||||
forms.DateTimeField.widget = forms.DateTimeInput(attrs={"type": "datetime-local"}, format="%Y-%m-%d %H:%M")
|
||||
|
||||
# Registration
|
||||
class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
|
||||
captcha = ReCaptchaField()
|
||||
|
||||
class Meta:
|
||||
model = models.Profile
|
||||
fields = ('username', 'email', 'first_name', 'last_name', 'initials', 'phone')
|
||||
|
||||
def clean_initials(self):
|
||||
"""
|
||||
Validate that the supplied initials are unique.
|
||||
"""
|
||||
if models.Profile.objects.filter(initials__iexact=self.cleaned_data['initials']):
|
||||
raise forms.ValidationError("These initials are already in use. Please supply different initials.")
|
||||
return self.cleaned_data['initials']
|
||||
|
||||
|
||||
# Login form
|
||||
class PasswordReset(PasswordResetForm):
|
||||
captcha = ReCaptchaField(label='Captcha')
|
||||
|
||||
|
||||
class ProfileCreationForm(UserCreationForm):
|
||||
class Meta(UserCreationForm.Meta):
|
||||
model = models.Profile
|
||||
|
||||
|
||||
class ProfileChangeForm(UserChangeForm):
|
||||
class Meta(UserChangeForm.Meta):
|
||||
model = models.Profile
|
||||
|
||||
|
||||
# Events Shit
|
||||
class EventForm(forms.ModelForm):
|
||||
datetime_input_formats = list(settings.DATETIME_INPUT_FORMATS)
|
||||
datetime_input_formats = formats.get_format_lazy("DATETIME_INPUT_FORMATS") + settings.DATETIME_INPUT_FORMATS
|
||||
meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
|
||||
access_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False)
|
||||
parking_and_access = forms.BooleanField(
|
||||
label="Additional parking or access requirements (i.e. campus parking permits, wristbands)?", required=False
|
||||
)
|
||||
|
||||
items_json = forms.CharField()
|
||||
|
||||
items = {}
|
||||
|
||||
related_models = {
|
||||
"person": models.Person,
|
||||
"organisation": models.Organisation,
|
||||
"venue": models.Venue,
|
||||
"mic": models.Profile,
|
||||
"checked_in_by": models.Profile,
|
||||
'person': models.Person,
|
||||
'organisation': models.Organisation,
|
||||
'venue': models.Venue,
|
||||
'mic': models.Profile,
|
||||
'checked_in_by': models.Profile,
|
||||
}
|
||||
|
||||
@property
|
||||
def _get_items_json(self):
|
||||
items = {}
|
||||
if self.instance.pk is None:
|
||||
return items
|
||||
for item in self.instance.items.all():
|
||||
data = serializers.serialize("json", [item])
|
||||
data = serializers.serialize('json', [item])
|
||||
struct = simplejson.loads(data)
|
||||
items[item.pk] = simplejson.dumps(struct[0])
|
||||
return simplejson.dumps(items)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
super(EventForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields["items_json"].initial = self._get_items_json
|
||||
self.fields["start_date"].widget.format = "%Y-%m-%d"
|
||||
self.fields["end_date"].widget.format = "%Y-%m-%d"
|
||||
self.fields['items_json'].initial = self._get_items_json
|
||||
self.fields['start_date'].widget.format = '%Y-%m-%d'
|
||||
self.fields['end_date'].widget.format = '%Y-%m-%d'
|
||||
|
||||
self.fields["access_at"].widget.format = "%Y-%m-%dT%H:%M:%S"
|
||||
self.fields["meet_at"].widget.format = "%Y-%m-%dT%H:%M:%S"
|
||||
self.fields['access_at'].widget.format = '%Y-%m-%dT%H:%M:%S'
|
||||
self.fields['meet_at'].widget.format = '%Y-%m-%dT%H:%M:%S'
|
||||
|
||||
def init_items(self):
|
||||
self.items = self.process_items_json()
|
||||
return self.items
|
||||
|
||||
def process_items_json(self, event=None):
|
||||
data = simplejson.loads(self.cleaned_data["items_json"])
|
||||
data = simplejson.loads(self.cleaned_data['items_json'])
|
||||
items = {}
|
||||
for key in data:
|
||||
pk = int(key)
|
||||
items[pk] = self._get_or_initialise_item(pk, data[key]["fields"], event)
|
||||
items[pk] = self._get_or_initialise_item(pk, data[key]['fields'], event)
|
||||
|
||||
return items
|
||||
|
||||
@@ -82,45 +104,24 @@ class EventForm(forms.ModelForm):
|
||||
item = models.EventItem()
|
||||
|
||||
# Take the data from the form and update the item object
|
||||
item.name = data["name"]
|
||||
item.description = data["description"]
|
||||
item.quantity = data["quantity"]
|
||||
item.cost = data["cost"]
|
||||
item.order = data["order"]
|
||||
item.name = data['name']
|
||||
item.description = data['description']
|
||||
item.quantity = data['quantity']
|
||||
item.cost = data['cost']
|
||||
item.order = data['order']
|
||||
|
||||
if event:
|
||||
if (event):
|
||||
item.event = event
|
||||
item.full_clean()
|
||||
else:
|
||||
item.full_clean("event")
|
||||
item.full_clean('event')
|
||||
|
||||
return item
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data.get("is_rig") and not (
|
||||
self.cleaned_data.get("person") or self.cleaned_data.get("organisation")
|
||||
):
|
||||
raise forms.ValidationError(
|
||||
"You haven't provided any client contact details. Please add a person or organisation.", code="contact"
|
||||
)
|
||||
access = self.cleaned_data.get("access_at")
|
||||
if (
|
||||
"warn-access" not in self.data
|
||||
and access is not None
|
||||
and access.date() < (self.cleaned_data.get("start_date") - timedelta(days=7))
|
||||
):
|
||||
raise forms.ValidationError(
|
||||
format_html(
|
||||
"Are you sure about that? Your access time seems a bit optimistic. If you're sure, save again. <input type='hidden' id='warn-access' name='warn-access' value='0'/>"
|
||||
),
|
||||
code="access_sanity",
|
||||
)
|
||||
return super().clean()
|
||||
|
||||
def save(self, commit=True):
|
||||
m = super().save(commit=False)
|
||||
m = super(EventForm, self).save(commit=False)
|
||||
|
||||
if commit:
|
||||
if (commit):
|
||||
m.save()
|
||||
cur_items = m.items.all()
|
||||
items = self.process_items_json(m)
|
||||
@@ -136,156 +137,7 @@ class EventForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = models.Event
|
||||
fields = [
|
||||
"is_rig",
|
||||
"name",
|
||||
"venue",
|
||||
"start_time",
|
||||
"end_date",
|
||||
"start_date",
|
||||
"end_time",
|
||||
"meet_at",
|
||||
"access_at",
|
||||
"description",
|
||||
"notes",
|
||||
"mic",
|
||||
"person",
|
||||
"organisation",
|
||||
"dry_hire",
|
||||
"checked_in_by",
|
||||
"status",
|
||||
"purchase_order",
|
||||
"collector",
|
||||
"forum_url",
|
||||
"parking_and_access",
|
||||
]
|
||||
|
||||
|
||||
class BaseClientEventAuthorisationForm(forms.ModelForm):
|
||||
tos = forms.BooleanField(required=True, label="Terms of hire")
|
||||
name = forms.CharField(label="Your Name")
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data.get("amount") != self.instance.event.total:
|
||||
self.add_error("amount", "The amount authorised must equal the total for the event (inc VAT).")
|
||||
return super().clean()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.fields["uni_id"].required = True
|
||||
self.fields["account_code"].required = True
|
||||
|
||||
class Meta:
|
||||
model = models.EventAuthorisation
|
||||
fields = ("tos", "name", "amount", "uni_id", "account_code")
|
||||
|
||||
|
||||
class EventAuthorisationRequestForm(forms.Form):
|
||||
email = forms.EmailField(required=True, label="Authoriser Email")
|
||||
|
||||
|
||||
class EventRiskAssessmentForm(forms.ModelForm):
|
||||
related_models = {
|
||||
"power_mic": models.Profile,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for name, field in self.fields.items():
|
||||
if str(name) == "supervisor_consulted":
|
||||
field.widget = forms.CheckboxInput()
|
||||
elif field.__class__ == forms.BooleanField:
|
||||
field.widget = forms.RadioSelect(
|
||||
choices=[(True, "Yes"), (False, "No")], attrs={"class": "form-check-input", "required": "true"}
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data.get("big_power"):
|
||||
power_mic = self.cleaned_data.get("power_mic")
|
||||
if power_mic is None:
|
||||
self.add_error(
|
||||
"power_mic",
|
||||
forms.ValidationError("A Power MIC is required for big power.", code="power_mic_required"),
|
||||
)
|
||||
elif not power_mic.level_qualifications.filter(level__department=TrainingLevel.POWER).exists():
|
||||
self.add_error(
|
||||
"power_mic",
|
||||
forms.ValidationError("Your Power MIC must be a Power Technician.", code="power_tech_required"),
|
||||
)
|
||||
# Check expected values
|
||||
unexpected_values = []
|
||||
for field, value in models.RiskAssessment.expected_values.items():
|
||||
if self.cleaned_data.get(field) != value:
|
||||
unexpected_values.append(f"<li>{self._meta.model._meta.get_field(field).help_text}</li>")
|
||||
if len(unexpected_values) > 0 and not self.cleaned_data.get("supervisor_consulted"):
|
||||
raise forms.ValidationError(
|
||||
f"Your answers to these questions: <ul>{''.join([str(elem) for elem in unexpected_values])}</ul> require consulting with a supervisor.",
|
||||
code="unusual_answers",
|
||||
)
|
||||
return super().clean()
|
||||
|
||||
class Meta:
|
||||
model = models.RiskAssessment
|
||||
fields = "__all__"
|
||||
exclude = ["reviewed_at", "reviewed_by"]
|
||||
|
||||
|
||||
class EventChecklistForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["date"].widget.format = "%Y-%m-%d"
|
||||
for name, field in self.fields.items():
|
||||
if field.__class__ == forms.NullBooleanField:
|
||||
# Only display yes/no to user, the 'none' is only ever set in the background
|
||||
field.widget = forms.CheckboxInput()
|
||||
|
||||
related_models = {
|
||||
"venue": models.Venue,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = models.EventChecklist
|
||||
fields = "__all__"
|
||||
exclude = ["reviewed_at", "reviewed_by"]
|
||||
|
||||
|
||||
class PowerTestRecordForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for name, field in self.fields.items():
|
||||
if field.__class__ == forms.NullBooleanField:
|
||||
# Only display yes/no to user, the 'none' is only ever set in the background
|
||||
field.widget = forms.CheckboxInput()
|
||||
|
||||
related_models = {
|
||||
"venue": models.Venue,
|
||||
"power_mic": models.Profile,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = models.PowerTestRecord
|
||||
fields = "__all__"
|
||||
exclude = ["reviewed_at", "reviewed_by"]
|
||||
|
||||
|
||||
class EventCheckInForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["time"].initial = timezone.now()
|
||||
self.fields["role"].initial = "Crew"
|
||||
|
||||
class Meta:
|
||||
model = models.EventCheckIn
|
||||
fields = "__all__"
|
||||
exclude = ["end_time"]
|
||||
|
||||
|
||||
class EditCheckInForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.EventCheckIn
|
||||
fields = "__all__"
|
||||
fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date',
|
||||
'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic',
|
||||
'person', 'organisation', 'dry_hire', 'checked_in_by', 'status',
|
||||
'collector', 'purchase_order']
|
||||
|
||||
156
RIGS/ical.py
Normal file
156
RIGS/ical.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from RIGS import models, forms
|
||||
from django_ical.views import ICalFeed
|
||||
from django.db.models import Q
|
||||
from django.core.urlresolvers import reverse_lazy, reverse, NoReverseMatch
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
import datetime, pytz
|
||||
|
||||
class CalendarICS(ICalFeed):
|
||||
"""
|
||||
A simple event calender
|
||||
"""
|
||||
#Metadata which is passed on to clients
|
||||
product_id = 'RIGS'
|
||||
title = 'RIGS Calendar'
|
||||
timezone = settings.TIME_ZONE
|
||||
file_name = "rigs.ics"
|
||||
|
||||
# Cancelled = 'cancelled' = False
|
||||
# Dry Hire = 'dry-hire' = True
|
||||
# Non Rig = 'non-rig' = True
|
||||
# Rig = 'rig' = True
|
||||
# Provisional = 'provisional' = True
|
||||
# Confirmed/Booked = 'confirmed' = True
|
||||
|
||||
def get_object(self, request, *args, **kwargs):
|
||||
params = {}
|
||||
|
||||
params['dry-hire'] = request.GET.get('dry-hire','true') == 'true'
|
||||
params['non-rig'] = request.GET.get('non-rig','true') == 'true'
|
||||
params['rig'] = request.GET.get('rig','true') == 'true'
|
||||
|
||||
params['cancelled'] = request.GET.get('cancelled','false') == 'true'
|
||||
params['provisional'] = request.GET.get('provisional','true') == 'true'
|
||||
params['confirmed'] = request.GET.get('confirmed','true') == 'true'
|
||||
|
||||
return params
|
||||
|
||||
def description(self,params):
|
||||
desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + ('Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n'
|
||||
desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + ('Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '')
|
||||
|
||||
return desc
|
||||
|
||||
def items(self, params):
|
||||
#include events from up to 1 year ago
|
||||
start = datetime.datetime.now() - datetime.timedelta(days=365)
|
||||
filter = Q(start_date__gte=start)
|
||||
|
||||
typeFilters = Q(pk=None) #Need something that is false for every entry
|
||||
|
||||
if params['dry-hire']:
|
||||
typeFilters = typeFilters | Q(dry_hire=True, is_rig=True)
|
||||
|
||||
if params['non-rig']:
|
||||
typeFilters = typeFilters | Q(is_rig=False)
|
||||
|
||||
if params['rig']:
|
||||
typeFilters = typeFilters | Q(is_rig=True, dry_hire=False)
|
||||
|
||||
statusFilters = Q(pk=None) #Need something that is false for every entry
|
||||
|
||||
if params['cancelled']:
|
||||
statusFilters = statusFilters | Q(status=models.Event.CANCELLED)
|
||||
if params['provisional']:
|
||||
statusFilters = statusFilters | Q(status=models.Event.PROVISIONAL)
|
||||
if params['confirmed']:
|
||||
statusFilters = statusFilters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED)
|
||||
|
||||
filter = filter & typeFilters & statusFilters
|
||||
|
||||
return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
def item_title(self, item):
|
||||
title = ''
|
||||
|
||||
# Prefix title with status (if it's a critical status)
|
||||
if item.cancelled:
|
||||
title += 'CANCELLED: '
|
||||
|
||||
if not item.is_rig:
|
||||
title += 'NON-RIG: '
|
||||
|
||||
if item.dry_hire:
|
||||
title += 'DRY HIRE: '
|
||||
|
||||
# Add the rig name
|
||||
title += item.name
|
||||
|
||||
# Add the status
|
||||
title += ' ('+str(item.get_status_display())+')'
|
||||
|
||||
return title
|
||||
|
||||
def item_start_datetime(self, item):
|
||||
return item.earliest_time
|
||||
|
||||
def item_end_datetime(self, item):
|
||||
if type(item.latest_time) is datetime.date: # Ical end_datetime is non-inclusive, so add a day
|
||||
return item.latest_time + datetime.timedelta(days=1)
|
||||
|
||||
return item.latest_time
|
||||
|
||||
def item_location(self,item):
|
||||
return item.venue
|
||||
|
||||
def item_description(self, item):
|
||||
# Create a nice information-rich description
|
||||
# note: only making use of information available to "non-keyholders"
|
||||
|
||||
tz = pytz.timezone(self.timezone)
|
||||
|
||||
desc = 'Rig ID = '+str(item.pk)+'\n'
|
||||
desc += 'Event = ' + item.name + '\n'
|
||||
desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n'
|
||||
if item.is_rig and item.person:
|
||||
desc += 'Client = ' + item.person.name + ( (' for '+item.organisation.name) if item.organisation else '') + '\n'
|
||||
desc += 'Status = ' + str(item.get_status_display()) + '\n'
|
||||
desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n'
|
||||
|
||||
|
||||
desc += '\n'
|
||||
if item.meet_at:
|
||||
desc += 'Crew Meet = ' + (item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n'
|
||||
if item.access_at:
|
||||
desc += 'Access At = ' + (item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n'
|
||||
if item.start_date:
|
||||
desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + ((' '+item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n'
|
||||
if item.end_date:
|
||||
desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + ((' '+item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n'
|
||||
|
||||
desc += '\n'
|
||||
if item.description:
|
||||
desc += 'Event Description:\n'+item.description+'\n\n'
|
||||
# if item.notes: // Need to add proper keyholder checks before this gets put back
|
||||
# desc += 'Notes:\n'+item.notes+'\n\n'
|
||||
|
||||
base_url = "http://rigs.nottinghamtec.co.uk"
|
||||
desc += 'URL = '+base_url+str(item.get_absolute_url())
|
||||
|
||||
return desc
|
||||
|
||||
def item_link(self, item):
|
||||
# Make a link to the event in the web interface
|
||||
# base_url = "https://pyrigs.nottinghamtec.co.uk"
|
||||
return item.get_absolute_url()
|
||||
|
||||
# def item_created(self, item): #TODO - Implement created date-time (using django-reversion?) - not really necessary though
|
||||
# return ''
|
||||
|
||||
def item_updated(self, item): # some ical clients will display this
|
||||
return item.last_edited_at
|
||||
|
||||
def item_guid(self, item): # use the rig-id as the ical unique event identifier
|
||||
return item.pk
|
||||
28
RIGS/importer_tests.py
Normal file
28
RIGS/importer_tests.py
Normal file
@@ -0,0 +1,28 @@
|
||||
__author__ = 'ghost'
|
||||
|
||||
import unittest
|
||||
from importer import fix_email
|
||||
|
||||
|
||||
class EmailFixerTest(unittest.TestCase):
|
||||
def test_correct(self):
|
||||
e = fix_email("tom@ghost.uk.net")
|
||||
self.assertEqual(e, "tom@ghost.uk.net")
|
||||
|
||||
def test_partial(self):
|
||||
e = fix_email("psytp")
|
||||
self.assertEqual(e, "psytp@nottingham.ac.uk")
|
||||
|
||||
def test_none(self):
|
||||
old = None
|
||||
new = fix_email(old)
|
||||
self.assertEqual(old, new)
|
||||
|
||||
def test_empty(self):
|
||||
old = ""
|
||||
new = fix_email(old)
|
||||
self.assertEqual(old, new)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,43 +0,0 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from assets import models
|
||||
from RIGS import models as rigsmodels
|
||||
from training import models as tmodels
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Deletes testing sample data"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
from django.conf import settings
|
||||
|
||||
if not settings.DEBUG:
|
||||
raise CommandError("You cannot run this command in production")
|
||||
|
||||
self.delete_objects(models.AssetCategory)
|
||||
self.delete_objects(models.AssetStatus)
|
||||
self.delete_objects(models.Supplier)
|
||||
self.delete_objects(models.Connector)
|
||||
self.delete_objects(models.Asset)
|
||||
self.delete_objects(rigsmodels.VatRate)
|
||||
self.delete_objects(rigsmodels.Profile)
|
||||
self.delete_objects(rigsmodels.Person)
|
||||
self.delete_objects(rigsmodels.Organisation)
|
||||
self.delete_objects(rigsmodels.Venue)
|
||||
self.delete_objects(Group)
|
||||
self.delete_objects(rigsmodels.Event)
|
||||
self.delete_objects(rigsmodels.EventItem)
|
||||
self.delete_objects(rigsmodels.Invoice)
|
||||
self.delete_objects(rigsmodels.Payment)
|
||||
self.delete_objects(rigsmodels.RiskAssessment)
|
||||
self.delete_objects(rigsmodels.EventChecklist)
|
||||
self.delete_objects(tmodels.TrainingCategory)
|
||||
self.delete_objects(tmodels.TrainingItem)
|
||||
self.delete_objects(tmodels.TrainingLevel)
|
||||
self.delete_objects(tmodels.TrainingItemQualification)
|
||||
self.delete_objects(tmodels.TrainingLevelRequirement)
|
||||
|
||||
def delete_objects(self, model):
|
||||
for obj in model.objects.all():
|
||||
obj.delete()
|
||||
@@ -1,13 +0,0 @@
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Adds sample data to use for testing"
|
||||
can_import_settings = True
|
||||
|
||||
def handle(self, *args, **options):
|
||||
call_command("generateSampleUserData")
|
||||
call_command("generateSampleRIGSData")
|
||||
call_command("generateSampleAssetsData")
|
||||
call_command("generateSampleTrainingData")
|
||||
@@ -1,564 +0,0 @@
|
||||
import datetime
|
||||
import random
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Adds sample data to use for testing"
|
||||
can_import_settings = True
|
||||
|
||||
people = []
|
||||
organisations = []
|
||||
venues = []
|
||||
events = []
|
||||
profiles = models.Profile.objects.all()
|
||||
|
||||
def handle(self, *args, **options):
|
||||
print("Generating rigboard data")
|
||||
from django.conf import settings
|
||||
|
||||
if not (settings.DEBUG or settings.STAGING):
|
||||
raise CommandError("You cannot run this command in production")
|
||||
|
||||
random.seed(
|
||||
"Some object to seed the random number generator"
|
||||
) # otherwise it is done by time, which could lead to inconsistant tests
|
||||
|
||||
with transaction.atomic():
|
||||
models.VatRate.objects.create(start_at="2014-03-05", rate=0.20, comment="test1")
|
||||
self.setup_people()
|
||||
self.setup_organisations()
|
||||
self.setup_venues()
|
||||
self.setup_events()
|
||||
print("Done generating rigboard data")
|
||||
|
||||
def setup_people(self):
|
||||
names = [
|
||||
"Regulus Black",
|
||||
"Sirius Black",
|
||||
"Lavender Brown",
|
||||
"Cho Chang",
|
||||
"Vincent Crabbe",
|
||||
"Vincent Crabbe",
|
||||
"Bartemius Crouch",
|
||||
"Fleur Delacour",
|
||||
"Cedric Diggory",
|
||||
"Alberforth Dumbledore",
|
||||
"Albus Dumbledore",
|
||||
"Dudley Dursley",
|
||||
"Petunia Dursley",
|
||||
"Vernon Dursley",
|
||||
"Argus Filch",
|
||||
"Seamus Finnigan",
|
||||
"Nicolas Flamel",
|
||||
"Cornelius Fudge",
|
||||
"Goyle",
|
||||
"Gregory Goyle",
|
||||
"Hermione Granger",
|
||||
"Rubeus Hagrid",
|
||||
"Igor Karkaroff",
|
||||
"Viktor Krum",
|
||||
"Bellatrix Lestrange",
|
||||
"Alice Longbottom",
|
||||
"Frank Longbottom",
|
||||
"Neville Longbottom",
|
||||
"Luna Lovegood",
|
||||
"Xenophilius Lovegood", # noqa
|
||||
"Remus Lupin",
|
||||
"Draco Malfoy",
|
||||
"Lucius Malfoy",
|
||||
"Narcissa Malfoy",
|
||||
"Olympe Maxime",
|
||||
"Minerva McGonagall",
|
||||
"Mad-Eye Moody",
|
||||
"Peter Pettigrew",
|
||||
"Harry Potter",
|
||||
"James Potter",
|
||||
"Lily Potter",
|
||||
"Quirinus Quirrell",
|
||||
"Tom Riddle",
|
||||
"Mary Riddle",
|
||||
"Lord Voldemort",
|
||||
"Rita Skeeter",
|
||||
"Severus Snape",
|
||||
"Nymphadora Tonks",
|
||||
"Dolores Janes Umbridge",
|
||||
"Arthur Weasley",
|
||||
"Bill Weasley",
|
||||
"Charlie Weasley",
|
||||
"Fred Weasley",
|
||||
"George Weasley",
|
||||
"Ginny Weasley",
|
||||
"Molly Weasley",
|
||||
"Percy Weasley",
|
||||
"Ron Weasley",
|
||||
"Dobby",
|
||||
"Fluffy",
|
||||
"Hedwig",
|
||||
"Moaning Myrtle",
|
||||
"Aragog",
|
||||
"Grawp",
|
||||
] # noqa
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(models.Profile.objects.all()))
|
||||
person = models.Person.objects.create(name=name)
|
||||
|
||||
if i % 3 == 0:
|
||||
person.email = "address@person.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
person.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
person.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
person.phone = "01234 567894"
|
||||
|
||||
person.save()
|
||||
self.people.append(person)
|
||||
|
||||
def setup_organisations(self):
|
||||
names = [
|
||||
"Acme, inc.",
|
||||
"Widget Corp",
|
||||
"123 Warehousing",
|
||||
"Demo Company",
|
||||
"Smith and Co.",
|
||||
"Foo Bars",
|
||||
"ABC Telecom",
|
||||
"Fake Brothers",
|
||||
"QWERTY Logistics",
|
||||
"Demo, inc.",
|
||||
"Sample Company",
|
||||
"Sample, inc",
|
||||
"Acme Corp",
|
||||
"Allied Biscuit",
|
||||
"Ankh-Sto Associates",
|
||||
"Extensive Enterprise",
|
||||
"Galaxy Corp",
|
||||
"Globo-Chem",
|
||||
"Mr. Sparkle",
|
||||
"Globex Corporation",
|
||||
"LexCorp",
|
||||
"LuthorCorp",
|
||||
"North Central Positronics",
|
||||
"Omni Consimer Products",
|
||||
"Praxis Corporation",
|
||||
"Sombra Corporation",
|
||||
"Sto Plains Holdings",
|
||||
"Tessier-Ashpool",
|
||||
"Wayne Enterprises",
|
||||
"Wentworth Industries",
|
||||
"ZiffCorp",
|
||||
"Bluth Company",
|
||||
"Strickland Propane",
|
||||
"Thatherton Fuels",
|
||||
"Three Waters",
|
||||
"Water and Power",
|
||||
"Western Gas & Electric",
|
||||
"Mammoth Pictures",
|
||||
"Mooby Corp",
|
||||
"Gringotts",
|
||||
"Thrift Bank",
|
||||
"Flowers By Irene",
|
||||
"The Legitimate Businessmens Club",
|
||||
"Osato Chemicals",
|
||||
"Transworld Consortium",
|
||||
"Universal Export",
|
||||
"United Fried Chicken",
|
||||
"Virtucon",
|
||||
"Kumatsu Motors",
|
||||
"Keedsler Motors",
|
||||
"Powell Motors",
|
||||
"Industrial Automation",
|
||||
"Sirius Cybernetics Corporation",
|
||||
"U.S. Robotics and Mechanical Men",
|
||||
"Colonial Movers",
|
||||
"Corellian Engineering Corporation",
|
||||
"Incom Corporation",
|
||||
"General Products",
|
||||
"Leeding Engines Ltd.",
|
||||
"Blammo", # noqa
|
||||
"Input, Inc.",
|
||||
"Mainway Toys",
|
||||
"Videlectrix",
|
||||
"Zevo Toys",
|
||||
"Ajax",
|
||||
"Axis Chemical Co.",
|
||||
"Barrytron",
|
||||
"Carrys Candles",
|
||||
"Cogswell Cogs",
|
||||
"Spacely Sprockets",
|
||||
"General Forge and Foundry",
|
||||
"Duff Brewing Company",
|
||||
"Dunder Mifflin",
|
||||
"General Services Corporation",
|
||||
"Monarch Playing Card Co.",
|
||||
"Krustyco",
|
||||
"Initech",
|
||||
"Roboto Industries",
|
||||
"Primatech",
|
||||
"Sonky Rubber Goods",
|
||||
"St. Anky Beer",
|
||||
"Stay Puft Corporation",
|
||||
"Vandelay Industries",
|
||||
"Wernham Hogg",
|
||||
"Gadgetron",
|
||||
"Burleigh and Stronginthearm",
|
||||
"BLAND Corporation",
|
||||
"Nordyne Defense Dynamics",
|
||||
"Petrox Oil Company",
|
||||
"Roxxon",
|
||||
"McMahon and Tate",
|
||||
"Sixty Second Avenue",
|
||||
"Charles Townsend Agency",
|
||||
"Spade and Archer",
|
||||
"Megadodo Publications",
|
||||
"Rouster and Sideways",
|
||||
"C.H. Lavatory and Sons",
|
||||
"Globo Gym American Corp",
|
||||
"The New Firm",
|
||||
"SpringShield",
|
||||
"Compuglobalhypermeganet",
|
||||
"Data Systems",
|
||||
"Gizmonic Institute",
|
||||
"Initrode",
|
||||
"Taggart Transcontinental",
|
||||
"Atlantic Northern",
|
||||
"Niagular",
|
||||
"Plow King",
|
||||
"Big Kahuna Burger",
|
||||
"Big T Burgers and Fries",
|
||||
"Chez Quis",
|
||||
"Chotchkies",
|
||||
"The Frying Dutchman",
|
||||
"Klimpys",
|
||||
"The Krusty Krab",
|
||||
"Monks Diner",
|
||||
"Milliways",
|
||||
"Minuteman Cafe",
|
||||
"Taco Grande",
|
||||
"Tip Top Cafe",
|
||||
"Moes Tavern",
|
||||
"Central Perk",
|
||||
"Chasers",
|
||||
] # noqa
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(models.Profile.objects.all()))
|
||||
new_organisation = models.Organisation.objects.create(name=name)
|
||||
|
||||
if i % 2 == 0:
|
||||
new_organisation.has_su_account = True
|
||||
|
||||
if i % 3 == 0:
|
||||
new_organisation.email = "address@organisation.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
new_organisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
new_organisation.address = (
|
||||
"1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
)
|
||||
|
||||
if i % 9 == 0:
|
||||
new_organisation.phone = "01234 567894"
|
||||
|
||||
new_organisation.save()
|
||||
self.organisations.append(new_organisation)
|
||||
|
||||
def setup_venues(self):
|
||||
names = [
|
||||
"Bear Island",
|
||||
"Crossroads Inn",
|
||||
"Deepwood Motte",
|
||||
"The Dreadfort",
|
||||
"The Eyrie",
|
||||
"Greywater Watch",
|
||||
"The Iron Islands",
|
||||
"Karhold",
|
||||
"Moat Cailin",
|
||||
"Oldstones",
|
||||
"Raventree Hall",
|
||||
"Riverlands",
|
||||
"The Ruby Ford",
|
||||
"Saltpans",
|
||||
"Seagard",
|
||||
"Torrhen's Square",
|
||||
"The Trident",
|
||||
"The Twins",
|
||||
"The Vale of Arryn",
|
||||
"The Whispering Wood",
|
||||
"White Harbor",
|
||||
"Winterfell",
|
||||
"The Arbor",
|
||||
"Ashemark",
|
||||
"Brightwater Keep",
|
||||
"Casterly Rock",
|
||||
"Clegane's Keep",
|
||||
"Dragonstone",
|
||||
"Dorne",
|
||||
"God's Eye",
|
||||
"The Golden Tooth", # noqa
|
||||
"Harrenhal",
|
||||
"Highgarden",
|
||||
"Horn Hill",
|
||||
"Fingers",
|
||||
"King's Landing",
|
||||
"Lannisport",
|
||||
"Oldtown",
|
||||
"Rainswood",
|
||||
"Storm's End",
|
||||
"Summerhall",
|
||||
"Sunspear",
|
||||
"Tarth",
|
||||
"Castle Black",
|
||||
"Craster's Keep",
|
||||
"Fist of the First Men",
|
||||
"The Frostfangs",
|
||||
"The Gift",
|
||||
"The Skirling Pass",
|
||||
"The Wall",
|
||||
"Asshai",
|
||||
"Astapor",
|
||||
"Braavos",
|
||||
"The Dothraki Sea",
|
||||
"Lys",
|
||||
"Meereen",
|
||||
"Myr",
|
||||
"Norvos",
|
||||
"Pentos",
|
||||
"Qarth",
|
||||
"Qohor",
|
||||
"The Red Waste",
|
||||
"Tyrosh",
|
||||
"Vaes Dothrak",
|
||||
"Valyria",
|
||||
"Village of the Lhazareen",
|
||||
"Volantis",
|
||||
"Yunkai",
|
||||
] # noqa
|
||||
for i, name in enumerate(names):
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
new_venue = models.Venue.objects.create(name=name)
|
||||
|
||||
if i % 2 == 0:
|
||||
new_venue.three_phase_available = True
|
||||
|
||||
if i % 3 == 0:
|
||||
new_venue.email = "address@venue.com"
|
||||
|
||||
if i % 5 == 0:
|
||||
new_venue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
|
||||
if i % 7 == 0:
|
||||
new_venue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
|
||||
|
||||
if i % 9 == 0:
|
||||
new_venue.phone = "01234 567894"
|
||||
|
||||
new_venue.save()
|
||||
self.venues.append(new_venue)
|
||||
|
||||
def setup_events(self):
|
||||
names = [
|
||||
"Outdoor Concert",
|
||||
"Hall Open Mic Night",
|
||||
"Festival",
|
||||
"Weekend Event",
|
||||
"Magic Show",
|
||||
"Society Ball",
|
||||
"Evening Show",
|
||||
"Talent Show",
|
||||
"Acoustic Evening",
|
||||
"Hire of Things",
|
||||
"SU Event",
|
||||
"End of Term Show",
|
||||
"Theatre Show",
|
||||
"Outdoor Fun Day",
|
||||
"Summer Carnival",
|
||||
"Open Days",
|
||||
"Magic Show",
|
||||
"Awards Ceremony",
|
||||
"Debating Event",
|
||||
"Club Night",
|
||||
"DJ Evening",
|
||||
"Building Projection",
|
||||
"Choir Concert",
|
||||
]
|
||||
descriptions = [
|
||||
"A brief description of the event",
|
||||
"This event is boring",
|
||||
"Probably wont happen",
|
||||
"Warning: this has lots of kit",
|
||||
]
|
||||
notes = [
|
||||
"The client came into the office at some point",
|
||||
"Who knows if this will happen",
|
||||
"Probably should check this event",
|
||||
"Maybe not happening",
|
||||
"Run away!",
|
||||
]
|
||||
|
||||
item_options = [
|
||||
{
|
||||
"name": "Speakers",
|
||||
"description": "Some really really big speakers \n these are very loud",
|
||||
"quantity": 2,
|
||||
"cost": 200.00,
|
||||
},
|
||||
{
|
||||
"name": "Projector",
|
||||
"description": "Some kind of video thinamejig, probably with unnecessary processing for free",
|
||||
"quantity": 1,
|
||||
"cost": 500.00,
|
||||
},
|
||||
{
|
||||
"name": "Lighting Desk",
|
||||
"description": "Cannot provide guarentee that it will work",
|
||||
"quantity": 1,
|
||||
"cost": 200.52,
|
||||
},
|
||||
{"name": "Moving lights", "description": "Flashy lights, with the copper", "quantity": 8, "cost": 50.00},
|
||||
{
|
||||
"name": "Microphones",
|
||||
"description": "Make loud noise \n you will want speakers with this",
|
||||
"quantity": 5,
|
||||
"cost": 0.50,
|
||||
},
|
||||
{
|
||||
"name": "Sound Mixer Thing",
|
||||
"description": "Might be analogue, might be digital",
|
||||
"quantity": 1,
|
||||
"cost": 100.00,
|
||||
},
|
||||
{"name": "Electricity", "description": "You need this", "quantity": 1, "cost": 200.00},
|
||||
{"name": "Crew", "description": "Costs nothing, because reasons", "quantity": 1, "cost": 0.00},
|
||||
{"name": "Loyalty Discount", "description": "Have some negative moneys", "quantity": 1, "cost": -50.00},
|
||||
]
|
||||
|
||||
day_delta = -120 # start adding events from 4 months ago
|
||||
|
||||
for i in range(150): # Let's add 100 events
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
|
||||
name = names[i % len(names)]
|
||||
|
||||
start_date = datetime.date.today() + datetime.timedelta(days=day_delta)
|
||||
day_delta = day_delta + random.randint(0, 3)
|
||||
|
||||
new_event = models.Event.objects.create(name=name, start_date=start_date)
|
||||
|
||||
if random.randint(0, 2) > 1: # 1 in 3 have a start time
|
||||
new_event.start_time = datetime.time(random.randint(15, 20))
|
||||
if random.randint(0, 2) > 1: # of those, 1 in 3 have an end time on the same day
|
||||
new_event.end_time = datetime.time(random.randint(21, 23))
|
||||
elif random.randint(0, 1) > 0: # half of the others finish early the next day
|
||||
new_event.end_date = new_event.start_date + datetime.timedelta(days=1)
|
||||
new_event.end_time = datetime.time(random.randint(0, 5))
|
||||
elif random.randint(0, 2) > 1: # 1 in 3 of the others finish a few days ahead
|
||||
new_event.end_date = new_event.start_date + datetime.timedelta(days=random.randint(1, 4))
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have MIC
|
||||
new_event.mic = random.choice(self.profiles)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have organisation
|
||||
new_event.organisation = random.choice(self.organisations)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have person
|
||||
new_event.person = random.choice(self.people)
|
||||
|
||||
if random.randint(0, 6) > 0: # 5 in 6 have venue
|
||||
new_event.venue = random.choice(self.venues)
|
||||
|
||||
# Could have any status, equally weighted
|
||||
new_event.status = random.choice(
|
||||
[models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED]
|
||||
)
|
||||
|
||||
new_event.dry_hire = random.randint(0, 7) == 0 # 1 in 7 are dry hire
|
||||
|
||||
if random.randint(0, 1) > 0: # 1 in 2 have description
|
||||
new_event.description = random.choice(descriptions)
|
||||
|
||||
if random.randint(0, 1) > 0: # 1 in 2 have notes
|
||||
new_event.notes = random.choice(notes)
|
||||
|
||||
new_event.save()
|
||||
|
||||
# Now add some items
|
||||
for j in range(random.randint(1, 5)):
|
||||
item_data = item_options[random.randint(0, len(item_options) - 1)]
|
||||
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
|
||||
new_item.save()
|
||||
|
||||
while new_event.sum_total < 0:
|
||||
item_data = item_options[random.randint(0, len(item_options) - 1)]
|
||||
new_item = models.EventItem.objects.create(event=new_event, order=j, **item_data)
|
||||
new_item.save()
|
||||
|
||||
with reversion.create_revision():
|
||||
reversion.set_user(random.choice(self.profiles))
|
||||
if new_event.start_date < datetime.date.today(): # think about adding an invoice
|
||||
if random.randint(0, 2) > 0: # 2 in 3 have had paperwork sent to treasury
|
||||
new_invoice = models.Invoice.objects.create(event=new_event)
|
||||
if new_event.status is models.Event.CANCELLED: # void cancelled events
|
||||
new_invoice.void = True
|
||||
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
|
||||
models.Payment.objects.create(
|
||||
invoice=new_invoice,
|
||||
amount=new_invoice.balance,
|
||||
date=datetime.date.today(),
|
||||
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)),
|
||||
)
|
||||
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),
|
||||
)
|
||||
@@ -1,52 +0,0 @@
|
||||
import premailer
|
||||
import datetime
|
||||
|
||||
from django.template.loader import get_template
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Sends email reminders as required. Triggered daily through sys cron in production."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
events = models.Event.objects.current_events().select_related("riskassessment")
|
||||
for event in events:
|
||||
earliest_time = (
|
||||
event.earliest_time
|
||||
if isinstance(event.earliest_time, datetime.datetime)
|
||||
else timezone.make_aware(datetime.datetime.combine(event.earliest_time, datetime.time(00, 00)))
|
||||
)
|
||||
# 48 hours = 172800 seconds
|
||||
if (
|
||||
event.is_rig
|
||||
and not event.cancelled
|
||||
and not event.dry_hire
|
||||
and (earliest_time - timezone.now()).total_seconds() <= 172800
|
||||
and not hasattr(event, "riskassessment")
|
||||
):
|
||||
context = {
|
||||
"event": event,
|
||||
"url": "https://" + settings.DOMAIN + reverse("event_ra", kwargs={"pk": event.pk}),
|
||||
}
|
||||
target = event.mic.email if event.mic else f"productions@{settings.DOMAIN}"
|
||||
msg = EmailMultiAlternatives(
|
||||
f"{event} - Risk Assessment Incomplete",
|
||||
get_template("email/ra_reminder.txt").render(context),
|
||||
to=[target],
|
||||
reply_to=[f"h.s.manager@{settings.DOMAIN}"],
|
||||
)
|
||||
css = finders.find("css/email.css")
|
||||
html = premailer.Premailer(
|
||||
get_template("email/ra_reminder.html").render(context),
|
||||
external_styles=css,
|
||||
allow_loading_external_files=True,
|
||||
).transform()
|
||||
msg.attach_alternative(html, "text/html")
|
||||
msg.send()
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.core.validators
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('postedAt', models.DateTimeField(auto_now=True)),
|
||||
('message', models.TextField()),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import RIGS.models
|
||||
import versioning
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -26,6 +25,6 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import RIGS.models
|
||||
import versioning
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -22,6 +21,6 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
import RIGS.models
|
||||
import versioning
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -34,15 +33,15 @@ class Migration(migrations.Migration):
|
||||
('payment_method', models.CharField(blank=True, null=True, max_length=255)),
|
||||
('payment_received', models.CharField(blank=True, null=True, max_length=255)),
|
||||
('purchase_order', models.CharField(blank=True, null=True, max_length=255)),
|
||||
('based_on', models.ForeignKey(to='RIGS.Event', related_name='future_events', on_delete=models.CASCADE)),
|
||||
('checked_in_by', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in', on_delete=models.CASCADE)),
|
||||
('mic', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', on_delete=models.CASCADE)),
|
||||
('organisation', models.ForeignKey(to='RIGS.Organisation', on_delete=models.CASCADE)),
|
||||
('person', models.ForeignKey(to='RIGS.Person', on_delete=models.CASCADE)),
|
||||
('based_on', models.ForeignKey(to='RIGS.Event', related_name='future_events')),
|
||||
('checked_in_by', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in')),
|
||||
('mic', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic')),
|
||||
('organisation', models.ForeignKey(to='RIGS.Organisation')),
|
||||
('person', models.ForeignKey(to='RIGS.Person')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventItem',
|
||||
@@ -53,7 +52,7 @@ class Migration(migrations.Migration):
|
||||
('quantity', models.IntegerField()),
|
||||
('cost', models.DecimalField(max_digits=10, decimal_places=2)),
|
||||
('order', models.IntegerField()),
|
||||
('event', models.ForeignKey(to='RIGS.Event', related_name='item', on_delete=models.CASCADE)),
|
||||
('event', models.ForeignKey(to='RIGS.Event', related_name='item')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
@@ -71,12 +70,12 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
bases=(models.Model, RIGS.models.RevisionMixin),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='venue',
|
||||
field=models.ForeignKey(to='RIGS.Venue', on_delete=models.CASCADE),
|
||||
field=models.ForeignKey(to='RIGS.Venue'),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
@@ -14,26 +14,26 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='based_on',
|
||||
field=models.ForeignKey(to='RIGS.Event', related_name='future_events', blank=True, null=True, on_delete=models.CASCADE),
|
||||
field=models.ForeignKey(to='RIGS.Event', related_name='future_events', blank=True, null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='checked_in_by',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True,
|
||||
null=True, on_delete=models.CASCADE),
|
||||
null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='mic',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True, on_delete=models.CASCADE),
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='organisation',
|
||||
field=models.ForeignKey(to='RIGS.Organisation', blank=True, null=True, on_delete=models.CASCADE),
|
||||
field=models.ForeignKey(to='RIGS.Organisation', blank=True, null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
@@ -19,8 +19,8 @@ class Migration(migrations.Migration):
|
||||
('run', models.BooleanField(default=False)),
|
||||
('derig', models.BooleanField(default=False)),
|
||||
('notes', models.TextField(blank=True, null=True)),
|
||||
('event', models.ForeignKey(related_name='crew', to='RIGS.Event', on_delete=models.CASCADE)),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
('event', models.ForeignKey(related_name='crew', to='RIGS.Event')),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='eventitem',
|
||||
name='event',
|
||||
field=models.ForeignKey(related_name='items', to='RIGS.Event', on_delete=models.CASCADE),
|
||||
field=models.ForeignKey(related_name='items', to='RIGS.Event'),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
@@ -14,7 +14,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='person',
|
||||
field=models.ForeignKey(blank=True, null=True, to='RIGS.Person', on_delete=models.CASCADE),
|
||||
field=models.ForeignKey(blank=True, null=True, to='RIGS.Person'),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
@@ -14,13 +14,13 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='venue',
|
||||
field=models.ForeignKey(blank=True, to='RIGS.Venue', null=True, on_delete=models.CASCADE),
|
||||
field=models.ForeignKey(blank=True, to='RIGS.Venue', null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventitem',
|
||||
name='event',
|
||||
field=models.ForeignKey(related_name='items', blank=True, to='RIGS.Event', on_delete=models.CASCADE),
|
||||
field=models.ForeignKey(related_name='items', blank=True, to='RIGS.Event'),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('invoice_date', models.DateField(auto_now_add=True)),
|
||||
('void', models.BooleanField()),
|
||||
('event', models.OneToOneField(to='RIGS.Event', on_delete=models.CASCADE)),
|
||||
('event', models.OneToOneField(to='RIGS.Event')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
@@ -31,7 +31,7 @@ class Migration(migrations.Migration):
|
||||
('date', models.DateField()),
|
||||
('amount', models.DecimalField(help_text=b'Please use ex. VAT', max_digits=10, decimal_places=2)),
|
||||
('method', models.CharField(max_length=2, choices=[(b'C', b'Cash'), (b'I', b'Internal'), (b'E', b'External'), (b'SU', b'SU Core'), (b'M', b'Members')])),
|
||||
('invoice', models.ForeignKey(to='RIGS.Invoice', on_delete=models.CASCADE)),
|
||||
('invoice', models.ForeignKey(to='RIGS.Invoice')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
@@ -40,7 +40,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='mic',
|
||||
field=models.ForeignKey(related_name='event_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE),
|
||||
field=models.ForeignKey(related_name='event_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.core.validators
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.db.models.deletion
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2016-03-31 12:02
|
||||
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0024_auto_20160229_2042'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='username',
|
||||
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vatrate',
|
||||
name='start_at',
|
||||
field=models.DateField(),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0024_auto_20160229_2042'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EventAuthorisation',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('uni_id', models.CharField(max_length=10, null=True, verbose_name=b'University ID', blank=True)),
|
||||
('account_code', models.CharField(max_length=50, null=True, blank=True)),
|
||||
('amount', models.DecimalField(verbose_name=b'authorisation amount', max_digits=10, decimal_places=2)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('event', models.ForeignKey(related_name='authroisations', to='RIGS.Event', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-05-10 17:46
|
||||
|
||||
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0025_auto_20160331_1302'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='username',
|
||||
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator()], verbose_name='username'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0025_eventauthorisation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='eventauthorisation',
|
||||
name='created_at',
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0026_remove_eventauthorisation_created_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='eventauthorisation',
|
||||
name='event',
|
||||
field=models.OneToOneField(related_name='authorisation', to='RIGS.Event', on_delete=models.CASCADE),
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0027_eventauthorisation_event_singular'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='eventauthorisation',
|
||||
name='sent_by',
|
||||
field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0029_eventauthorisation_sent_by'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='auth_request_at',
|
||||
field=models.DateTimeField(null=True, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='auth_request_by',
|
||||
field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='auth_request_to',
|
||||
field=models.EmailField(max_length=254, null=True, blank=True),
|
||||
),
|
||||
]
|
||||
@@ -1,16 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-05-12 20:02
|
||||
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0030_auth_request_sending'),
|
||||
('RIGS', '0026_auto_20170510_1846'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
@@ -1,69 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-09-04 22:55
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('reversion', '0001_squashed_0004_auto_20160611_1202'),
|
||||
('RIGS', '0031_merge_20170512_2102'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RIGSVersion',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'indexes': [],
|
||||
'proxy': True,
|
||||
},
|
||||
bases=('reversion.version',),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='collector',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='collected by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='mic',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='event_mic', to=settings.AUTH_USER_MODEL, verbose_name='MIC'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='purchase_order',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='PO'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventauthorisation',
|
||||
name='amount',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, verbose_name='authorisation amount'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventauthorisation',
|
||||
name='uni_id',
|
||||
field=models.CharField(blank=True, max_length=10, null=True, verbose_name='University ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='amount',
|
||||
field=models.DecimalField(decimal_places=2, help_text='Please use ex. VAT', max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='method',
|
||||
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('SU', 'SU Core'), ('T', 'TEC Adjustment')], max_length=2, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='username',
|
||||
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.0.3 on 2018-03-25 00:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0032_auto_20170904_2355'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='last_name',
|
||||
field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.0.5 on 2019-07-28 21:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0033_auto_20180325_0016'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='risk_assessment_edit_url',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.0.13 on 2019-11-24 13:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0034_event_risk_assessment_edit_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='risk_assessment_edit_url',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='risk assessment'),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 2.0.13 on 2020-01-10 14:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0035_auto_20191124_1319'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='is_approved',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='last_emailed',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 2.0.13 on 2020-01-11 18:29
|
||||
# This migration ensures that legacy Profiles from before approvals were implemented are automatically approved
|
||||
from django.db import migrations
|
||||
|
||||
def approve_legacy(apps, schema_editor):
|
||||
Profile = apps.get_model('RIGS', 'Profile')
|
||||
for person in Profile.objects.all():
|
||||
person.is_approved = True
|
||||
person.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0036_profile_is_approved'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(approve_legacy, migrations.RunPython.noop)
|
||||
]
|
||||
@@ -1,37 +0,0 @@
|
||||
# Generated by Django 2.0.13 on 2020-03-06 20:00
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0037_approve_legacy'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='event',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='invoice',
|
||||
options={'ordering': ['-invoice_date']},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='organisation',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='person',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='profile',
|
||||
options={'verbose_name': 'user', 'verbose_name_plural': 'users'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='venue',
|
||||
options={},
|
||||
),
|
||||
]
|
||||
@@ -1,191 +0,0 @@
|
||||
# Generated by Django 3.1.2 on 2021-01-23 19:10
|
||||
|
||||
import RIGS.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import versioning
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0038_auto_20200306_2000'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EventChecklist',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField()),
|
||||
('safe_parking', models.BooleanField(blank=True, help_text='Vehicles parked safely?<br><small>(does not obstruct venue access)</small>', null=True)),
|
||||
('safe_packing', models.BooleanField(blank=True, help_text='Equipment packed away safely?<br><small>(including flightcases)</small>', null=True)),
|
||||
('exits', models.BooleanField(blank=True, help_text='Emergency exits clear?', null=True)),
|
||||
('trip_hazard', models.BooleanField(blank=True, help_text='Appropriate barriers around kit and cabling secured?', null=True)),
|
||||
('warning_signs', models.BooleanField(blank=True, help_text='Warning signs in place?<br><small>(strobe, smoke, power etc.)</small>')),
|
||||
('ear_plugs', models.BooleanField(blank=True, help_text='Ear plugs issued to crew where needed?', null=True)),
|
||||
('hs_location', models.CharField(blank=True, help_text='Location of Safety Bag/Box', max_length=255, null=True)),
|
||||
('extinguishers_location', models.CharField(blank=True, help_text='Location of fire extinguishers', max_length=255, null=True)),
|
||||
('rcds', models.BooleanField(blank=True, help_text='RCDs installed where needed and tested?', null=True)),
|
||||
('supply_test', models.BooleanField(blank=True, help_text='Electrical supplies tested?<br><small>(using socket tester)</small>', null=True)),
|
||||
('earthing', models.BooleanField(blank=True, help_text='Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>', null=True)),
|
||||
('pat', models.BooleanField(blank=True, help_text='All equipment in PAT period?', null=True)),
|
||||
('source_rcd', models.BooleanField(blank=True, help_text='Source RCD protected?<br><small>(if cable is more than 3m long) </small>', null=True)),
|
||||
('labelling', models.BooleanField(blank=True, help_text='Appropriate and clear labelling on distribution and cabling?', null=True)),
|
||||
('fd_voltage_l1', models.IntegerField(blank=True, help_text='L1 - N', null=True, verbose_name='First Distro Voltage L1-N')),
|
||||
('fd_voltage_l2', models.IntegerField(blank=True, help_text='L2 - N', null=True, verbose_name='First Distro Voltage L2-N')),
|
||||
('fd_voltage_l3', models.IntegerField(blank=True, help_text='L3 - N', null=True, verbose_name='First Distro Voltage L3-N')),
|
||||
('fd_phase_rotation', models.BooleanField(blank=True, help_text='Phase Rotation<br><small>(if required)</small>', null=True, verbose_name='Phase Rotation')),
|
||||
('fd_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True, verbose_name='Earth Fault Loop Impedance')),
|
||||
('fd_pssc', models.IntegerField(blank=True, help_text='Prospective Short Circuit Current', null=True, verbose_name='PSCC')),
|
||||
('w1_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
|
||||
('w1_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
||||
('w1_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
||||
('w1_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
|
||||
('w2_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
|
||||
('w2_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
||||
('w2_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
||||
('w2_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
|
||||
('w3_description', models.CharField(blank=True, help_text='Description', max_length=255, null=True)),
|
||||
('w3_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
||||
('w3_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
||||
('w3_earth_fault', models.IntegerField(blank=True, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', null=True)),
|
||||
('all_rcds_tested', models.BooleanField(blank=True, help_text='All circuit RCDs tested?<br><small>(using test button)</small>', null=True)),
|
||||
('public_sockets_tested', models.BooleanField(blank=True, help_text='Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>', null=True)),
|
||||
('reviewed_at', models.DateTimeField(null=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['event'],
|
||||
'permissions': [('review_eventchecklist', 'Can review Event Checklists')],
|
||||
},
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventChecklistCrew',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(max_length=255)),
|
||||
('start', models.DateTimeField()),
|
||||
('end', models.DateTimeField()),
|
||||
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.eventchecklist')),
|
||||
],
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventChecklistVehicle',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('vehicle', models.CharField(max_length=255)),
|
||||
('checklist', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to='RIGS.eventchecklist')),
|
||||
],
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RiskAssessment',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nonstandard_equipment', models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by <a href='https://nottinghamtec.sharepoint.com/:f:/g/HealthAndSafety/Eo4xED_DrqFFsfYIjKzMZIIB6Gm_ZfR-a8l84RnzxtBjrA?e=Bf0Haw'>TEC's standard risk assessments and method statements?</a>")),
|
||||
('nonstandard_use', models.BooleanField(help_text='Are TEC using their equipment in a way that is abnormal?<br><small>i.e. Not covered by TECs standard health and safety documentation</small>')),
|
||||
('contractors', models.BooleanField(help_text='Are you using any external contractors?<br><small>i.e. Freelancers/Crewing Companies</small>')),
|
||||
('other_companies', models.BooleanField(help_text='Are TEC working with any other companies on site?<br><small>e.g. TEC is providing the lighting while another company does sound</small>')),
|
||||
('crew_fatigue', models.BooleanField(help_text='Is crew fatigue likely to be a risk at any point during this event?')),
|
||||
('general_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
|
||||
('big_power', models.BooleanField(help_text='Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?')),
|
||||
('outside', models.BooleanField(help_text='Is the event outdoors?')),
|
||||
('generators', models.BooleanField(help_text='Will generators be used?')),
|
||||
('other_companies_power', models.BooleanField(help_text='Will TEC be supplying power to any other companies?')),
|
||||
('nonstandard_equipment_power', models.BooleanField(help_text='Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?')),
|
||||
('multiple_electrical_environments', models.BooleanField(help_text='Will the electrical installation occupy more than one electrical environment?')),
|
||||
('power_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
|
||||
('power_plan', models.URLField(blank=True, help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", null=True, validators=[RIGS.models.validate_url])),
|
||||
('noise_monitoring', models.BooleanField(help_text='Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?')),
|
||||
('sound_notes', models.TextField(blank=True, help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?', null=True)),
|
||||
('known_venue', models.BooleanField(help_text='Is this venue new to you (the MIC) or new to TEC?')),
|
||||
('safe_loading', models.BooleanField(help_text='Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)')),
|
||||
('safe_storage', models.BooleanField(help_text='Are there any problems with safe and secure equipment storage?')),
|
||||
('area_outside_of_control', models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?")),
|
||||
('barrier_required', models.BooleanField(help_text='Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?')),
|
||||
('nonstandard_emergency_procedure', models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?")),
|
||||
('special_structures', models.BooleanField(help_text='Does the event require use of winch stands, motors, MPT Towers, or staging?')),
|
||||
('suspended_structures', models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?")),
|
||||
('persons_responsible_structures', models.TextField(blank=True, help_text='Who are the persons on site responsible for their use?', null=True)),
|
||||
('rigging_plan', models.URLField(blank=True, help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", null=True, validators=[RIGS.models.validate_url])),
|
||||
('reviewed_at', models.DateTimeField(null=True)),
|
||||
('supervisor_consulted', models.BooleanField(null=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['event'],
|
||||
'permissions': [('review_riskassessment', 'Can review Risk Assessments')],
|
||||
},
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventcrew',
|
||||
name='event',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventcrew',
|
||||
name='user',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='RIGSVersion',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='risk_assessment_edit_url',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='first_name',
|
||||
field=models.CharField(blank=True, max_length=150, verbose_name='first name'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='EventCrew',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='riskassessment',
|
||||
name='event',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='RIGS.event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='riskassessment',
|
||||
name='power_mic',
|
||||
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC? (if yes to the above question, this person <em>must</em> be a Power Technician or Power Supervisor)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='power_mic', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='riskassessment',
|
||||
name='reviewed_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventchecklistvehicle',
|
||||
name='driver',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vehicles', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventchecklistcrew',
|
||||
name='crewmember',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crewed', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventchecklist',
|
||||
name='event',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to='RIGS.event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventchecklist',
|
||||
name='power_mic',
|
||||
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC?', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventchecklist',
|
||||
name='reviewed_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventchecklist',
|
||||
name='venue',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.venue'),
|
||||
),
|
||||
]
|
||||
@@ -1,67 +0,0 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-02 11:48
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def postgres_migration_prep(apps, schema_editor):
|
||||
model = apps.get_model("RIGS", "Event")
|
||||
for field in ["auth_request_to", "collector", "description", "notes", "purchase_order"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "EventAuthorisation")
|
||||
for field in ["account_code", "uni_id"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "EventChecklist")
|
||||
for field in ["extinguishers_location", "hs_location", "w1_description", "w2_description", "w3_description"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "EventItem")
|
||||
for field in ["description"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Organisation")
|
||||
for field in ["address", "email", "notes", "phone"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Payment")
|
||||
for field in ["method"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Person")
|
||||
for field in ["address", "email", "notes", "phone"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Profile")
|
||||
for field in ["phone"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "RiskAssessment")
|
||||
for field in ["general_notes", "persons_responsible_structures", "power_notes", "rigging_plan", "sound_notes"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
model = apps.get_model("RIGS", "Venue")
|
||||
for field in ["address", "email", "notes", "phone"]:
|
||||
filter_param = {"{}__isnull".format(field): True}
|
||||
update_param = {field: ""}
|
||||
model.objects.filter(**filter_param).update(**update_param)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0039_auto_20210123_1910'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(postgres_migration_prep, migrations.RunPython.noop)
|
||||
]
|
||||
@@ -1,201 +0,0 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-02 12:04
|
||||
|
||||
import RIGS.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0040_auto_20210302_1148'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='meet_info',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='payment_method',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='payment_received',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='dark_theme',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='auth_request_to',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='collector',
|
||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='collected by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='purchase_order',
|
||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='PO'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventauthorisation',
|
||||
name='account_code',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventauthorisation',
|
||||
name='uni_id',
|
||||
field=models.CharField(blank=True, default='', max_length=10, verbose_name='University ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='extinguishers_location',
|
||||
field=models.CharField(blank=True, default='', help_text='Location of fire extinguishers', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='hs_location',
|
||||
field=models.CharField(blank=True, default='', help_text='Location of Safety Bag/Box', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w1_description',
|
||||
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w2_description',
|
||||
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w3_description',
|
||||
field=models.CharField(blank=True, default='', help_text='Description', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventitem',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organisation',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=15),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='method',
|
||||
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('SU', 'SU Core'), ('T', 'TEC Adjustment')], default='', max_length=2),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=15),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='api_key',
|
||||
field=models.CharField(blank=True, default='', editable=False, max_length=40),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='general_notes',
|
||||
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='persons_responsible_structures',
|
||||
field=models.TextField(blank=True, default='', help_text='Who are the persons on site responsible for their use?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='power_notes',
|
||||
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='power_plan',
|
||||
field=models.URLField(blank=True, default='', help_text="Upload your power plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[RIGS.models.validate_url]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='rigging_plan',
|
||||
field=models.URLField(blank=True, default='', help_text="Upload your rigging plan to the <a href='https://nottinghamtec.sharepoint.com/'>Sharepoint</a> and submit a link", validators=[RIGS.models.validate_url]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='sound_notes',
|
||||
field=models.TextField(blank=True, default='', help_text='Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='address',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, default='', max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, default='', max_length=15),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 3.1.13 on 2021-10-07 22:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0041_auto_20210302_1204'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='fd_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w1_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w2_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='w3_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.1.13 on 2021-10-27 14:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0042_auto_20211007_2338'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='initials',
|
||||
field=models.CharField(max_length=5, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.2.11 on 2022-01-09 14:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0043_auto_20211027_1519'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='is_supervisor',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.2.12 on 2022-10-20 23:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0044_profile_is_supervisor'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='is_approved',
|
||||
field=models.BooleanField(default=False, help_text='Designates whether a staff member has approved this user.', verbose_name='Approval Status'),
|
||||
),
|
||||
]
|
||||
@@ -1,71 +0,0 @@
|
||||
# Generated by Django 3.2.16 on 2023-05-08 15:58
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import versioning.versioning
|
||||
|
||||
def migrate_old_data(apps, schema_editor):
|
||||
EventChecklist = apps.get_model('RIGS', 'EventChecklist')
|
||||
PowerTestRecord = apps.get_model('RIGS', 'PowerTestRecord')
|
||||
for ec in EventChecklist.objects.all():
|
||||
# New highscore for the most pythonic BS I've ever written.
|
||||
PowerTestRecord.objects.create(event=ec.event, venue=ec.venue, reviewed_by=ec.reviewed_by, **{i.name:getattr(ec, i.attname) for i in PowerTestRecord._meta.get_fields() if not (i.is_relation or i.auto_created or i.name == "notes")})
|
||||
|
||||
|
||||
def revert(apps, schema_editor):
|
||||
apps.get_model('RIGS', 'PowerTestRecord').objects.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0045_alter_profile_is_approved'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PowerTestRecord',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_tests', to='RIGS.event')),
|
||||
('notes', models.TextField(blank=True, default='')),
|
||||
('venue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='RIGS.venue')),
|
||||
('reviewed_at', models.DateTimeField(null=True)),
|
||||
('rcds', models.BooleanField(blank=True, help_text='RCDs installed where needed and tested?', null=True)),
|
||||
('supply_test', models.BooleanField(blank=True, help_text='Electrical supplies tested?<br><small>(using socket tester)</small>', null=True)),
|
||||
('earthing', models.BooleanField(blank=True, help_text='Equipment appropriately earthed?<br><small>(truss, stage, generators etc)</small>', null=True)),
|
||||
('pat', models.BooleanField(blank=True, help_text='All equipment in PAT period?', null=True)),
|
||||
('source_rcd', models.BooleanField(blank=True, help_text='Source RCD protected?<br><small>(if cable is more than 3m long) </small>', null=True)),
|
||||
('labelling', models.BooleanField(blank=True, help_text='Appropriate and clear labelling on distribution and cabling?', null=True)),
|
||||
('fd_voltage_l1', models.IntegerField(blank=True, help_text='L1 - N', null=True, verbose_name='First Distro Voltage L1-N')),
|
||||
('fd_voltage_l2', models.IntegerField(blank=True, help_text='L2 - N', null=True, verbose_name='First Distro Voltage L2-N')),
|
||||
('fd_voltage_l3', models.IntegerField(blank=True, help_text='L3 - N', null=True, verbose_name='First Distro Voltage L3-N')),
|
||||
('fd_phase_rotation', models.BooleanField(blank=True, help_text='Phase Rotation<br><small>(if required)</small>', null=True, verbose_name='Phase Rotation')),
|
||||
('fd_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
|
||||
('fd_pssc', models.IntegerField(blank=True, help_text='Prospective Short Circuit Current', null=True, verbose_name='PSCC')),
|
||||
('w1_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
|
||||
('w1_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
||||
('w1_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
||||
('w1_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
|
||||
('w2_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
|
||||
('w2_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
||||
('w2_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
||||
('w2_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
|
||||
('w3_description', models.CharField(blank=True, default='', help_text='Description', max_length=255)),
|
||||
('w3_polarity', models.BooleanField(blank=True, help_text='Polarity Checked?', null=True)),
|
||||
('w3_voltage', models.IntegerField(blank=True, help_text='Voltage', null=True)),
|
||||
('w3_earth_fault', models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance')),
|
||||
('all_rcds_tested', models.BooleanField(blank=True, help_text='All circuit RCDs tested?<br><small>(using test button)</small>', null=True)),
|
||||
('public_sockets_tested', models.BooleanField(blank=True, help_text='Public/Performer accessible circuits tested?<br><small>(using socket tester)</small>', null=True)),
|
||||
('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Reviewer')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
'ordering': ['event'],
|
||||
'permissions': [('review_power', 'Can review Power Test Records')],
|
||||
},
|
||||
bases=(models.Model, versioning.versioning.RevisionMixin),
|
||||
),
|
||||
migrations.RunPython(migrate_old_data, reverse_code=revert),
|
||||
]
|
||||
@@ -1,44 +0,0 @@
|
||||
# Generated by Django 3.2.19 on 2023-05-17 08:44
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
|
||||
def migrate_old_data(apps, schema_editor):
|
||||
EventChecklist = apps.get_model('RIGS', 'EventChecklist')
|
||||
EventCheckIn = apps.get_model('RIGS', 'EventCheckIn')
|
||||
for ec in EventChecklist.objects.all():
|
||||
for crew in ec.crew.all():
|
||||
try:
|
||||
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end, vehicle=ec.vehicles.get(driver=crew.crewmember).vehicle)
|
||||
except ObjectDoesNotExist:
|
||||
EventCheckIn.objects.create(event=ec.event, person=crew.crewmember, role=crew.role, time=crew.start, end_time=crew.end)
|
||||
|
||||
|
||||
def revert(apps, schema_editor):
|
||||
apps.get_model('RIGS', 'EventCheckIn').objects.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0046_create_powertests'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EventCheckIn',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('time', models.DateTimeField()),
|
||||
('role', models.CharField(blank=True, max_length=50)),
|
||||
('vehicle', models.CharField(blank=True, max_length=100)),
|
||||
('end_time', models.DateTimeField(blank=True, null=True)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crew', to='RIGS.event')),
|
||||
('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkins', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(migrate_old_data, reverse_code=revert),
|
||||
]
|
||||
@@ -1,156 +0,0 @@
|
||||
# Generated by Django 3.2.19 on 2023-05-18 11:56
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0047_auto_20230517_0944'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklistvehicle',
|
||||
name='checklist',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklistvehicle',
|
||||
name='driver',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='all_rcds_tested',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='earthing',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='fd_earth_fault',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='fd_phase_rotation',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='fd_pssc',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='fd_voltage_l1',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='fd_voltage_l2',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='fd_voltage_l3',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='labelling',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='pat',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='power_mic',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='public_sockets_tested',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='rcds',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='source_rcd',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='supply_test',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='w1_description',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='w1_earth_fault',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='w1_polarity',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='w1_voltage',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='w2_description',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='w2_earth_fault',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='w2_polarity',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='w2_voltage',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='w3_description',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='w3_earth_fault',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='w3_polarity',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventchecklist',
|
||||
name='w3_voltage',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powertestrecord',
|
||||
name='power_mic',
|
||||
field=models.ForeignKey(blank=True, help_text='Who is the Power MIC?', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='checklists', to=settings.AUTH_USER_MODEL, verbose_name='Power MIC'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventchecklist',
|
||||
name='reviewed_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='reviewed_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='riskassessment',
|
||||
name='reviewed_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='EventChecklistCrew',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='EventChecklistVehicle',
|
||||
),
|
||||
]
|
||||
@@ -1,53 +0,0 @@
|
||||
# Generated by Django 3.2.19 on 2023-05-29 10:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0048_auto_20230518_1256'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='fd_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='fd_pssc',
|
||||
field=models.IntegerField(blank=True, help_text='Prospective Short Circuit Current / A', null=True, verbose_name='PSCC'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='w1_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='w1_voltage',
|
||||
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='w2_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='w2_voltage',
|
||||
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='w3_earth_fault',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (Z<small>S</small>) / Ω', max_digits=6, null=True, verbose_name='Earth Fault Loop Impedance'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powertestrecord',
|
||||
name='w3_voltage',
|
||||
field=models.IntegerField(blank=True, help_text='Voltage / V', null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 3.2.19 on 2023-06-27 11:28
|
||||
|
||||
import RIGS.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0049_auto_20230529_1123'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='forum_url',
|
||||
field=models.URLField(blank=True, default='', validators=[RIGS.models.validate_forum_url]),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.2.19 on 2023-07-09 21:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0050_event_forum_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='method',
|
||||
field=models.CharField(blank=True, choices=[('C', 'Cash'), ('I', 'Internal'), ('E', 'External'), ('T', 'TEC Adjustment')], default='', max_length=2),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.2.25 on 2024-11-20 20:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0051_alter_payment_method'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='parking_and_access',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 3.2.25 on 2024-11-20 21:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0052_event_parking_and_access'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='riskassessment',
|
||||
name='parking_and_access',
|
||||
field=models.BooleanField(default=False, help_text='Are there additional requirements for parking and access to the venue? (i.e. campus parking permits, event access wristbands)'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
1053
RIGS/models.py
1053
RIGS/models.py
File diff suppressed because it is too large
Load Diff
13
RIGS/regbackend.py
Normal file
13
RIGS/regbackend.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from RIGS.models import Profile
|
||||
from RIGS.forms import ProfileRegistrationFormUniqueEmail
|
||||
|
||||
def user_created(sender, user, request, **kwargs):
|
||||
form = ProfileRegistrationFormUniqueEmail(request.POST)
|
||||
user.first_name = form.data['first_name']
|
||||
user.last_name = form.data['last_name']
|
||||
user.initials = form.data['initials']
|
||||
user.phone = form.data['phone']
|
||||
user.save()
|
||||
|
||||
from registration.signals import user_registered
|
||||
user_registered.connect(user_created)
|
||||
195
RIGS/rigboard.py
Normal file
195
RIGS/rigboard.py
Normal file
@@ -0,0 +1,195 @@
|
||||
import os
|
||||
import cStringIO as StringIO
|
||||
from io import BytesIO
|
||||
import urllib2
|
||||
|
||||
from django.views import generic
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.template.loader import get_template
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.db.models import Q
|
||||
from django.contrib import messages
|
||||
from z3c.rml import rml2pdf
|
||||
from PyPDF2 import PdfFileMerger, PdfFileReader
|
||||
|
||||
from RIGS import models, forms
|
||||
import datetime
|
||||
import re
|
||||
import copy
|
||||
|
||||
__author__ = 'ghost'
|
||||
|
||||
|
||||
class RigboardIndex(generic.TemplateView):
|
||||
template_name = 'RIGS/rigboard.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# get super context
|
||||
context = super(RigboardIndex, self).get_context_data(**kwargs)
|
||||
|
||||
# call out method to get current events
|
||||
context['events'] = models.Event.objects.current_events()
|
||||
return context
|
||||
|
||||
class WebCalendar(generic.TemplateView):
|
||||
template_name = 'RIGS/calendar.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(WebCalendar, self).get_context_data(**kwargs)
|
||||
context['view'] = kwargs.get('view','')
|
||||
context['date'] = kwargs.get('date','')
|
||||
return context
|
||||
|
||||
class EventDetail(generic.DetailView):
|
||||
model = models.Event
|
||||
|
||||
|
||||
class EventCreate(generic.CreateView):
|
||||
model = models.Event
|
||||
form_class = forms.EventForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventCreate, self).get_context_data(**kwargs)
|
||||
context['edit'] = True
|
||||
context['currentVAT'] = models.VatRate.objects.current_rate()
|
||||
|
||||
form = context['form']
|
||||
if re.search('"-\d+"', form['items_json'].value()):
|
||||
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
|
||||
|
||||
|
||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
||||
for field, model in form.related_models.iteritems():
|
||||
value = form[field].value()
|
||||
if value is not None and value != '':
|
||||
context[field] = model.objects.get(pk=value)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
class EventUpdate(generic.UpdateView):
|
||||
model = models.Event
|
||||
form_class = forms.EventForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventUpdate, self).get_context_data(**kwargs)
|
||||
context['edit'] = True
|
||||
|
||||
form = context['form']
|
||||
|
||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
||||
for field, model in form.related_models.iteritems():
|
||||
value = form[field].value()
|
||||
if value is not None and value != '':
|
||||
context[field] = model.objects.get(pk=value)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('event_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
class EventDuplicate(EventUpdate):
|
||||
def get_object(self, queryset=None):
|
||||
old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
|
||||
new = copy.copy(old) # Make a copy of the object in memory
|
||||
new.based_on = old # Make the new event based on the old event
|
||||
|
||||
if self.request.method in ('POST', 'PUT'): # This only happens on save (otherwise items won't display in editor)
|
||||
new.pk = None # This means a new event will be created on save, and all items will be re-created
|
||||
|
||||
messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.')
|
||||
|
||||
return new
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EventDuplicate, self).get_context_data(**kwargs)
|
||||
context["duplicate"] = True
|
||||
return context
|
||||
|
||||
class EventPrint(generic.View):
|
||||
def get(self, request, pk):
|
||||
object = get_object_or_404(models.Event, pk=pk)
|
||||
template = get_template('RIGS/event_print.xml')
|
||||
copies = ('TEC', 'Client')
|
||||
|
||||
merger = PdfFileMerger()
|
||||
|
||||
for copy in copies:
|
||||
|
||||
context = RequestContext(request, { # this should be outside the loop, but bug in 1.8.2 prevents this
|
||||
'object': object,
|
||||
'fonts': {
|
||||
'opensans': {
|
||||
'regular': 'RIGS/static/fonts/OPENSANS-REGULAR.TTF',
|
||||
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
|
||||
}
|
||||
},
|
||||
'copy':copy,
|
||||
'current_user':request.user,
|
||||
})
|
||||
|
||||
# context['copy'] = copy # this is the way to do it once we upgrade to Django 1.8.3
|
||||
|
||||
rml = template.render(context)
|
||||
buffer = StringIO.StringIO()
|
||||
|
||||
buffer = rml2pdf.parseString(rml)
|
||||
|
||||
merger.append(PdfFileReader(buffer))
|
||||
|
||||
buffer.close()
|
||||
|
||||
terms = urllib2.urlopen(settings.TERMS_OF_HIRE_URL)
|
||||
merger.append(StringIO.StringIO(terms.read()))
|
||||
|
||||
merged = BytesIO()
|
||||
merger.write(merged)
|
||||
|
||||
response = HttpResponse(content_type='application/pdf')
|
||||
|
||||
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
|
||||
|
||||
response['Content-Disposition'] = "filename=N%05d | %s.pdf" % (object.pk, escapedEventName)
|
||||
response.write(merged.getvalue())
|
||||
return response
|
||||
|
||||
class EventArchive(generic.ArchiveIndexView):
|
||||
model = models.Event
|
||||
date_field = "start_date"
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
start = self.request.GET.get('start', None)
|
||||
end = self.request.GET.get('end', datetime.date.today())
|
||||
|
||||
# Assume idiots, always check
|
||||
if start and start > end:
|
||||
messages.add_message(self.request, messages.INFO,
|
||||
"Muppet! Check the dates, it has been fixed for you.")
|
||||
start, end = end, start # Stop the impending fail
|
||||
|
||||
filter = False
|
||||
if end != "":
|
||||
filter = Q(start_date__lte=end)
|
||||
if start:
|
||||
if filter:
|
||||
filter = filter & Q(start_date__gte=start)
|
||||
else:
|
||||
filter = Q(start_date__gte=start)
|
||||
|
||||
if filter:
|
||||
qs = self.model.objects.filter(filter).order_by('-start_date')
|
||||
else:
|
||||
qs = self.model.objects.all().order_by('-start_date')
|
||||
|
||||
# Preselect related for efficiency
|
||||
qs.select_related('person', 'organisation', 'venue', 'mic')
|
||||
|
||||
if len(qs) == 0:
|
||||
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
|
||||
|
||||
return qs
|
||||
147
RIGS/signals.py
147
RIGS/signals.py
@@ -1,147 +0,0 @@
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from io import BytesIO
|
||||
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.cache import cache
|
||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
||||
from django.db.models.signals import post_save
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from premailer import Premailer
|
||||
from registration.signals import user_activated
|
||||
from reversion import revisions as reversion
|
||||
from z3c.rml import rml2pdf
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
def send_eventauthorisation_success_email(instance):
|
||||
# Generate PDF first to prevent context conflicts
|
||||
context = {
|
||||
"object": instance.event,
|
||||
"receipt": True,
|
||||
"current_user": False,
|
||||
}
|
||||
|
||||
template = get_template("event_print.xml")
|
||||
merger = PdfWriter()
|
||||
|
||||
rml = template.render(context)
|
||||
|
||||
buffer = rml2pdf.parseString(rml)
|
||||
merger.append(PdfReader(buffer))
|
||||
buffer.close()
|
||||
|
||||
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
|
||||
merger.append(BytesIO(terms.read()))
|
||||
|
||||
merged = BytesIO()
|
||||
merger.write(merged)
|
||||
|
||||
# Produce email content
|
||||
context = {
|
||||
"object": instance,
|
||||
}
|
||||
|
||||
if instance.event.person is not None and instance.email == instance.event.person.email:
|
||||
context["to_name"] = instance.event.person.name
|
||||
elif instance.event.organisation is not None and instance.email == instance.event.organisation.email:
|
||||
context["to_name"] = instance.event.organisation.name
|
||||
|
||||
subject = f"{instance.event.display_id} | {instance.event.name} - Event Authorised"
|
||||
|
||||
client_email = EmailMultiAlternatives(
|
||||
subject,
|
||||
get_template("email/eventauthorisation_client_success.txt").render(context),
|
||||
to=[instance.email],
|
||||
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
|
||||
)
|
||||
|
||||
css = finders.find("css/email.css")
|
||||
html = Premailer(
|
||||
get_template("email/eventauthorisation_client_success.html").render(context),
|
||||
external_styles=css,
|
||||
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)
|
||||
|
||||
client_email.attach(
|
||||
f"{instance.event.display_id} - {escapedEventName} - CONFIRMATION.pdf", merged.getvalue(), "application/pdf"
|
||||
)
|
||||
|
||||
if instance.event.mic:
|
||||
mic_email_address = instance.event.mic.email
|
||||
else:
|
||||
mic_email_address = settings.AUTHORISATION_NOTIFICATION_ADDRESS
|
||||
|
||||
mic_email = EmailMessage(
|
||||
subject, get_template("email/eventauthorisation_mic_success.txt").render(context), to=[mic_email_address]
|
||||
)
|
||||
|
||||
# Now we have both emails successfully generated, send them out
|
||||
client_email.send(fail_silently=True)
|
||||
mic_email.send(fail_silently=True)
|
||||
|
||||
# Set event to booked now that it's authorised
|
||||
instance.event.status = models.Event.BOOKED
|
||||
instance.event.save()
|
||||
|
||||
|
||||
def on_revision_commit(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
send_eventauthorisation_success_email(instance)
|
||||
|
||||
|
||||
post_save.connect(on_revision_commit, sender=models.EventAuthorisation)
|
||||
|
||||
|
||||
def send_admin_awaiting_approval_email(user, request, **kwargs):
|
||||
# Bit more controlled than just emailing all superusers
|
||||
for admin in models.Profile.admins():
|
||||
# Check we've ever emailed them before and if so, if cooldown has passed.
|
||||
if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now():
|
||||
context = {
|
||||
"request": request,
|
||||
"link_suffix": reverse("admin:RIGS_profile_changelist")
|
||||
+ 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(
|
||||
f"{context['number_of_users']} new users awaiting approval on RIGS",
|
||||
get_template("email/admin_awaiting_approval.txt").render(context),
|
||||
to=[admin.email],
|
||||
reply_to=[user.email],
|
||||
)
|
||||
css = finders.find("css/email.css")
|
||||
html = Premailer(
|
||||
get_template("email/admin_awaiting_approval.html").render(context),
|
||||
external_styles=css,
|
||||
allow_loading_external_files=True,
|
||||
).transform()
|
||||
email.attach_alternative(html, "text/html")
|
||||
email.send()
|
||||
|
||||
# Update last sent
|
||||
admin.last_emailed = timezone.now()
|
||||
admin.save()
|
||||
|
||||
|
||||
user_activated.connect(send_admin_awaiting_approval_email)
|
||||
|
||||
|
||||
def update_cache(sender, instance, created, **kwargs):
|
||||
cache.clear()
|
||||
|
||||
|
||||
for model in reversion.get_registered_models():
|
||||
post_save.connect(update_cache, sender=model)
|
||||
27
RIGS/static/config.rb
Normal file
27
RIGS/static/config.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
# Require any additional compass plugins here.
|
||||
require 'bootstrap-sass'
|
||||
|
||||
# Set this to the root of your project when deployed:
|
||||
http_path = "/static/"
|
||||
css_dir = "css"
|
||||
sass_dir = "scss"
|
||||
images_dir = "img"
|
||||
javascripts_dir = "js"
|
||||
fonts_dir = "fonts"
|
||||
|
||||
# You can select your preferred output style here (can be overridden via the command line):
|
||||
# output_style = :expanded or :nested or :compact or :compressed
|
||||
output_style = :compressed
|
||||
|
||||
# To enable relative paths to assets via compass helper functions. Uncomment:
|
||||
# relative_assets = true
|
||||
|
||||
# To disable debugging comments that display the original location of your selectors. Uncomment:
|
||||
# line_comments = false
|
||||
|
||||
|
||||
# If you prefer the indented syntax, you might want to regenerate this
|
||||
# project again passing --syntax sass, or you can uncomment this:
|
||||
# preferred_syntax = :sass
|
||||
# and then run:
|
||||
# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass
|
||||
27
RIGS/static/css/ajax-bootstrap-select.css
Executable file
27
RIGS/static/css/ajax-bootstrap-select.css
Executable file
@@ -0,0 +1,27 @@
|
||||
/*!
|
||||
* Ajax Bootstrap Select
|
||||
*
|
||||
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
|
||||
*
|
||||
* @version 1.3.1
|
||||
* @author Adam Heim - https://github.com/truckingsim
|
||||
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
|
||||
* @copyright 2015 Adam Heim
|
||||
* @license Released under the MIT license.
|
||||
*
|
||||
* Contributors:
|
||||
* Mark Carver - https://github.com/markcarver
|
||||
*
|
||||
* Last build: 2015-01-06 8:43:11 PM EST
|
||||
*/
|
||||
.bootstrap-select .status {
|
||||
background: #f0f0f0;
|
||||
clear: both;
|
||||
color: #999;
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
margin-bottom: -5px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user