mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-03-03 10:38:23 +00:00
Compare commits
37 Commits
data-inges
...
d55ec47b18
| Author | SHA1 | Date | |
|---|---|---|---|
| d55ec47b18 | |||
|
|
6550ed2318 | ||
|
|
c9759a6339 | ||
|
|
b637c4e452 | ||
|
|
8986b94b07 | ||
| 86c033ba97 | |||
| 52fd662340 | |||
| 9818ed995f | |||
| 5178614d71 | |||
| a7bf990666 | |||
|
a4a28a6130
|
|||
|
e3d8cf8978
|
|||
|
626779ef25
|
|||
| fa1dc31639 | |||
| d69543e309 | |||
|
|
a24e6d4495 | ||
|
|
fa5792914a | ||
| 0117091f3e | |||
|
37101d3340
|
|||
| de4bed92a4 | |||
|
|
3767923175 | ||
|
|
1d77cf95d3 | ||
|
|
1f21d0b265 | ||
| 7846a6d31e | |||
| d28b73a0b8 | |||
|
|
5c2e8b391c | ||
|
|
548bc1df81 | ||
|
|
c1d2bce8fb | ||
|
c71beab278
|
|||
|
259932a548
|
|||
|
7526485837
|
|||
| 39ed5aefb4 | |||
|
e7e760de2e
|
|||
| 9091197639 | |||
|
4f4baa62c1
|
|||
|
b9f8621e1a
|
|||
|
4b1dc37a7f
|
151
.github/workflows/combine-prs.yml
vendored
Normal file
151
.github/workflows/combine-prs.yml
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
name: 'Combine PRs'
|
||||
|
||||
# Controls when the action will run - in this case triggered manually
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branchPrefix:
|
||||
description: 'Branch prefix to find combinable PRs based on'
|
||||
required: true
|
||||
default: 'dependabot'
|
||||
mustBeGreen:
|
||||
description: 'Only combine PRs that are green (status is success)'
|
||||
required: true
|
||||
default: true
|
||||
combineBranchName:
|
||||
description: 'Name of the branch to combine PRs into'
|
||||
required: true
|
||||
default: 'combine-prs-branch'
|
||||
ignoreLabel:
|
||||
description: 'Exclude PRs with this label'
|
||||
required: true
|
||||
default: 'nocombine'
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "combine-prs"
|
||||
combine-prs:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
id: create-combined-pr
|
||||
name: Create Combined PR
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
let branchesAndPRStrings = [];
|
||||
let baseBranch = null;
|
||||
let baseBranchSHA = null;
|
||||
for (const pull of pulls) {
|
||||
const branch = pull['head']['ref'];
|
||||
console.log('Pull for branch: ' + branch);
|
||||
if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) {
|
||||
console.log('Branch matched prefix: ' + branch);
|
||||
let statusOK = true;
|
||||
if(${{ github.event.inputs.mustBeGreen }}) {
|
||||
console.log('Checking green status: ' + branch);
|
||||
const stateQuery = `query($owner: String!, $repo: String!, $pull_number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number:$pull_number) {
|
||||
commits(last: 1) {
|
||||
nodes {
|
||||
commit {
|
||||
statusCheckRollup {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
const vars = {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pull['number']
|
||||
};
|
||||
const result = await github.graphql(stateQuery, vars);
|
||||
const [{ commit }] = result.repository.pullRequest.commits.nodes;
|
||||
const state = commit.statusCheckRollup.state
|
||||
console.log('Validating status: ' + state);
|
||||
if(state != 'SUCCESS') {
|
||||
console.log('Discarding ' + branch + ' with status ' + state);
|
||||
statusOK = false;
|
||||
}
|
||||
}
|
||||
console.log('Checking labels: ' + branch);
|
||||
const labels = pull['labels'];
|
||||
for(const label of labels) {
|
||||
const labelName = label['name'];
|
||||
console.log('Checking label: ' + labelName);
|
||||
if(labelName == '${{ github.event.inputs.ignoreLabel }}') {
|
||||
console.log('Discarding ' + branch + ' with label ' + labelName);
|
||||
statusOK = false;
|
||||
}
|
||||
}
|
||||
if (statusOK) {
|
||||
console.log('Adding branch to array: ' + branch);
|
||||
const prString = '#' + pull['number'] + ' ' + pull['title'];
|
||||
branchesAndPRStrings.push({ branch, prString });
|
||||
baseBranch = pull['base']['ref'];
|
||||
baseBranchSHA = pull['base']['sha'];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (branchesAndPRStrings.length == 0) {
|
||||
core.setFailed('No PRs/branches matched criteria');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await github.rest.git.createRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}',
|
||||
sha: baseBranchSHA
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?');
|
||||
return;
|
||||
}
|
||||
|
||||
let combinedPRs = [];
|
||||
let mergeFailedPRs = [];
|
||||
for(const { branch, prString } of branchesAndPRStrings) {
|
||||
try {
|
||||
await github.rest.repos.merge({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
base: '${{ github.event.inputs.combineBranchName }}',
|
||||
head: branch,
|
||||
});
|
||||
console.log('Merged branch ' + branch);
|
||||
combinedPRs.push(prString);
|
||||
} catch (error) {
|
||||
console.log('Failed to merge branch ' + branch);
|
||||
mergeFailedPRs.push(prString);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Creating combined PR');
|
||||
const combinedPRsString = combinedPRs.join('\n');
|
||||
let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString;
|
||||
if(mergeFailedPRs.length > 0) {
|
||||
const mergeFailedPRsString = mergeFailedPRs.join('\n');
|
||||
body += '\n\n⚠️ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString
|
||||
}
|
||||
await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: 'Combined PR',
|
||||
head: '${{ github.event.inputs.combineBranchName }}',
|
||||
base: baseBranch,
|
||||
body: body
|
||||
});
|
||||
24
.github/workflows/django.yml
vendored
24
.github/workflows/django.yml
vendored
@@ -13,26 +13,20 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9.1
|
||||
- uses: actions/cache@v2
|
||||
id: pcache
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
python-version: 3.9
|
||||
cache: 'pipenv'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip pipenv
|
||||
python3 -m pip install --upgrade pip pipenv
|
||||
pipenv install -d
|
||||
# if: steps.pcache.outputs.cache-hit != 'true'
|
||||
- name: Cache Static Files
|
||||
id: static-cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: 'pipeline/built_assets'
|
||||
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
|
||||
@@ -43,9 +37,9 @@ jobs:
|
||||
- name: Basic Checks
|
||||
run: |
|
||||
pipenv run pycodestyle . --exclude=migrations,node_modules
|
||||
pipenv run python manage.py check
|
||||
pipenv run python manage.py makemigrations --check --dry-run
|
||||
pipenv run python manage.py collectstatic --noinput
|
||||
pipenv run python3 manage.py check
|
||||
pipenv run python3 manage.py makemigrations --check --dry-run
|
||||
pipenv run python3 manage.py collectstatic --noinput
|
||||
- name: Run Tests
|
||||
run: pipenv run pytest -n auto -vv --cov
|
||||
- uses: actions/upload-artifact@v2
|
||||
|
||||
13
Pipfile
13
Pipfile
@@ -11,7 +11,6 @@ asgiref = "~=3.3.1"
|
||||
beautifulsoup4 = "~=4.9.3"
|
||||
Brotli = "~=1.0.9"
|
||||
cachetools = "~=4.2.1"
|
||||
certifi = "~=2020.12.5"
|
||||
chardet = "~=4.0.0"
|
||||
configparser = "~=5.0.1"
|
||||
contextlib2 = "~=0.6.0.post1"
|
||||
@@ -26,18 +25,16 @@ django-ical = "~=1.7.1"
|
||||
django-recurrence = "~=1.10.3"
|
||||
django-registration-redux = "~=2.9"
|
||||
django-reversion = "~=3.0.9"
|
||||
django-toolbelt = "~=0.0.1"
|
||||
django-widget-tweaks = "~=1.4.8"
|
||||
django-htmlmin = "~=0.11.0"
|
||||
envparse = "~=0.2.0"
|
||||
envparse = "*"
|
||||
gunicorn = "~=20.0.4"
|
||||
icalendar = "~=4.0.7"
|
||||
idna = "~=2.10"
|
||||
lxml = "~=4.7.1"
|
||||
Markdown = "~=3.3.3"
|
||||
msgpack = "~=1.0.2"
|
||||
pep517 = "~=0.9.1"
|
||||
Pillow = "~=9.0.0"
|
||||
Pillow = "~=9.3.0"
|
||||
premailer = "~=3.7.0"
|
||||
progress = "~=1.5"
|
||||
psutil = "~=5.8.0"
|
||||
@@ -45,7 +42,7 @@ psycopg2 = "~=2.8.6"
|
||||
Pygments = "~=2.7.4"
|
||||
pyparsing = "~=2.4.7"
|
||||
PyPDF2 = "~=1.27.5"
|
||||
PyPOM = "~=2.2.0"
|
||||
PyPOM = "~=2.2.4"
|
||||
python-dateutil = "~=2.8.1"
|
||||
pytoml = "~=0.1.21"
|
||||
pytz = "~=2020.5"
|
||||
@@ -82,10 +79,10 @@ django-hcaptcha = "*"
|
||||
pikepdf = "*"
|
||||
django-queryable-properties = "*"
|
||||
django-mass-edit = "*"
|
||||
selenium = "~=3.141.0"
|
||||
|
||||
[dev-packages]
|
||||
selenium = "~=3.141.0"
|
||||
pycodestyle = "*"
|
||||
pycodestyle = "~=2.9.1"
|
||||
coveralls = "*"
|
||||
django-coverage-plugin = "*"
|
||||
pytest-cov = "*"
|
||||
|
||||
1324
Pipfile.lock
generated
1324
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -42,8 +42,9 @@ if not DEBUG:
|
||||
|
||||
INTERNAL_IPS = ['127.0.0.1']
|
||||
|
||||
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
|
||||
('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
|
||||
DOMAIN = env('DOMAIN', default='example.com')
|
||||
|
||||
ADMINS = [('IT Manager', f'it@{DOMAIN}'), ('Arona Jones', f'arona.jones@{DOMAIN}')]
|
||||
if DEBUG:
|
||||
ADMINS.append(('Testing Superuser', 'superuser@example.com'))
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ class EventChecklistForm(forms.ModelForm):
|
||||
for key in vehicles:
|
||||
pk = int(key.split('_')[1])
|
||||
driver_key = 'driver_' + str(pk)
|
||||
if(self.data[driver_key] == ''):
|
||||
if (self.data[driver_key] == ''):
|
||||
raise forms.ValidationError('Add a driver to vehicle ' + str(pk), code='vehicle_mismatch')
|
||||
else:
|
||||
try:
|
||||
|
||||
38
RIGS/management/commands/send_reminders.py
Normal file
38
RIGS/management/commands/send_reminders.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import premailer
|
||||
import datetime
|
||||
|
||||
from django.template.loader import get_template
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
|
||||
from RIGS import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Sends email reminders as required. Triggered daily through heroku-scheduler in production.'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
events = models.Event.objects.current_events().select_related('riskassessment')
|
||||
for event in events:
|
||||
earliest_time = event.earliest_time if isinstance(event.earliest_time, datetime.datetime) else timezone.make_aware(datetime.datetime.combine(event.earliest_time, datetime.time(00, 00)))
|
||||
# 48 hours = 172800 seconds
|
||||
if event.is_rig and not event.cancelled and not event.dry_hire and (earliest_time - timezone.now()).total_seconds() <= 172800 and not hasattr(event, 'riskassessment'):
|
||||
context = {
|
||||
"event": event,
|
||||
"url": "https://" + settings.DOMAIN + reverse('event_ra', kwargs={'pk': event.pk})
|
||||
}
|
||||
target = event.mic.email if event.mic else f"productions@{settings.DOMAIN}"
|
||||
msg = EmailMultiAlternatives(
|
||||
f"{event} - Risk Assessment Incomplete",
|
||||
get_template("email/ra_reminder.txt").render(context),
|
||||
to=[target],
|
||||
reply_to=[f"h.s.manager@{settings.DOMAIN}"],
|
||||
)
|
||||
css = finders.find('css/email.css')
|
||||
html = premailer.Premailer(get_template("email/ra_reminder.html").render(context), external_styles=css).transform()
|
||||
msg.attach_alternative(html, 'text/html')
|
||||
msg.send()
|
||||
18
RIGS/migrations/0045_alter_profile_is_approved.py
Normal file
18
RIGS/migrations/0045_alter_profile_is_approved.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
@@ -36,7 +36,7 @@ class Profile(AbstractUser):
|
||||
initials = models.CharField(max_length=5, null=True, blank=False)
|
||||
phone = models.CharField(max_length=13, blank=True, default='')
|
||||
api_key = models.CharField(max_length=40, blank=True, editable=False, default='')
|
||||
is_approved = models.BooleanField(default=False)
|
||||
is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.")
|
||||
# Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that...
|
||||
last_emailed = models.DateTimeField(blank=True, null=True)
|
||||
dark_theme = models.BooleanField(default=False)
|
||||
|
||||
@@ -58,13 +58,13 @@ def send_eventauthorisation_success_email(instance):
|
||||
|
||||
client_email = EmailMultiAlternatives(
|
||||
subject,
|
||||
get_template("eventauthorisation_client_success.txt").render(context),
|
||||
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("eventauthorisation_client_success.html").render(context),
|
||||
html = Premailer(get_template("email/eventauthorisation_client_success.html").render(context),
|
||||
external_styles=css).transform()
|
||||
client_email.attach_alternative(html, 'text/html')
|
||||
|
||||
@@ -82,7 +82,7 @@ def send_eventauthorisation_success_email(instance):
|
||||
|
||||
mic_email = EmailMessage(
|
||||
subject,
|
||||
get_template("eventauthorisation_mic_success.txt").render(context),
|
||||
get_template("email/eventauthorisation_mic_success.txt").render(context),
|
||||
to=[mic_email_address]
|
||||
)
|
||||
|
||||
@@ -117,12 +117,12 @@ def send_admin_awaiting_approval_email(user, request, **kwargs):
|
||||
|
||||
email = EmailMultiAlternatives(
|
||||
f"{context['number_of_users']} new users awaiting approval on RIGS",
|
||||
get_template("admin_awaiting_approval.txt").render(context),
|
||||
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("admin_awaiting_approval.html").render(context),
|
||||
html = Premailer(get_template("email/admin_awaiting_approval.html").render(context),
|
||||
external_styles=css).transform()
|
||||
email.attach_alternative(html, 'text/html')
|
||||
email.send()
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
bulletFontSize="10"/>
|
||||
</stylesheet>
|
||||
|
||||
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
|
||||
<template title="{{filename}}"> {# Note: page is 595x842 points (1 point=1/72in) #}
|
||||
<pageTemplate id="Headed" >
|
||||
<pageGraphics>
|
||||
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
|
||||
|
||||
5
RIGS/templates/email/eventauthorisation_mic_success.txt
Normal file
5
RIGS/templates/email/eventauthorisation_mic_success.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
|
||||
|
||||
Just to let you know your event N{{object.eventdisplay_id}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
|
||||
|
||||
The TEC Rig Information Gathering System
|
||||
16
RIGS/templates/email/ra_reminder.html
Normal file
16
RIGS/templates/email/ra_reminder.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends 'base_client_email.html' %}
|
||||
|
||||
{% block content %}
|
||||
<p>Hi {{event.mic.get_full_name|default_if_none:"Productions Manager"}},</p>
|
||||
|
||||
{% if event.mic %}
|
||||
<p>Just to let you know your event {{event.display_id}} <em>requires<em> a pre-event risk assessment completing prior to the event. Please do so as soon as possible.</p>
|
||||
{% else %}
|
||||
<p>This is a reminder that event {{event.display_id}} requires a MIC assigning and a risk assessment completing.</p>
|
||||
{% endif %}
|
||||
|
||||
<p>Fill it out here:</p>
|
||||
<a href="{{url}}" class="btn btn-info"><span class="fas fa-paperclip"></span> Create Risk Assessment</a>
|
||||
|
||||
<p>TEC PA & Lighting</p>
|
||||
{% endblock %}
|
||||
9
RIGS/templates/email/ra_reminder.txt
Normal file
9
RIGS/templates/email/ra_reminder.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
Hi {{event.mic.get_full_name|default_if_none:"Productions Manager"}},
|
||||
|
||||
{% if event.mic %}
|
||||
Just to let you know your event {{event.display_id}} requires a risk assessment completing prior to the event. Please do so as soon as possible.
|
||||
{% else %}
|
||||
This is a reminder that event {{event.display_id}} requires a MIC assigning and a risk assessment completing.
|
||||
{% endif %}
|
||||
|
||||
The TEC Rig Information Gathering System
|
||||
@@ -1,5 +0,0 @@
|
||||
Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}},
|
||||
|
||||
Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}.
|
||||
|
||||
The TEC Rig Information Gathering System
|
||||
@@ -5,21 +5,6 @@
|
||||
|
||||
{% block title %}Request Authorisation{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||
<script src="{% static 'js/popover.js' %}"></script>
|
||||
<script src="{% static 'js/clipboard.min.js' %}"></script>
|
||||
<script>
|
||||
var clipboard = new ClipboardJS('.btn');
|
||||
|
||||
clipboard.on('success', function(e) {
|
||||
$(e.trigger).popover('show');
|
||||
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
|
||||
e.clearSelection();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
@@ -33,11 +18,11 @@
|
||||
<dl class="dl-horizontal">
|
||||
{% if object.person.email %}
|
||||
<dt>Person Email</dt>
|
||||
<dd><span id="person-email">{{ object.person.email }}</span>{% button 'copy' id='#person-email' %}</dd>
|
||||
<dd><span id="person-email" class="pr-1">{{ object.person.email }}</span> {% button 'copy' id='#person-email' %}</dd>
|
||||
{% endif %}
|
||||
{% if object.organisation.email %}
|
||||
<dt>Organisation Email</dt>
|
||||
<dd><span id="org-email">{{ object.organisation.email }}</span>{% button 'copy' id='#org-email' %}</dd>
|
||||
<dd><span id="org-email" class="pr-1">{{ object.organisation.email }}</span> {% button 'copy' id='#org-email' %}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% else %}
|
||||
@@ -57,11 +42,20 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/tooltip.js' %}"></script>
|
||||
<script src="{% static 'js/popover.js' %}"></script>
|
||||
<script src="{% static 'js/clipboard.min.js' %}"></script>
|
||||
<script>
|
||||
$('#auth-request-form').on('submit', function () {
|
||||
$('#auth-request-form button').attr('disabled', true);
|
||||
});
|
||||
var clipboard = new ClipboardJS('.btn');
|
||||
|
||||
clipboard.on('success', function(e) {
|
||||
$(e.trigger).popover('show');
|
||||
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
|
||||
e.clearSelection();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -118,9 +118,9 @@ def orderby(request, field, attr):
|
||||
@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
|
||||
def get_field(obj, field, autoescape=True):
|
||||
value = getattr(obj, field)
|
||||
if(isinstance(value, bool)):
|
||||
if (isinstance(value, bool)):
|
||||
value = yesnoi(value, field in obj.inverted_fields)
|
||||
elif(isinstance(value, str)):
|
||||
elif (isinstance(value, str)):
|
||||
value = truncatewords(value, 20)
|
||||
return mark_safe(value)
|
||||
|
||||
@@ -144,7 +144,7 @@ def get_list(dictionary, key):
|
||||
|
||||
@register.filter
|
||||
def profile_by_index(value):
|
||||
if(value):
|
||||
if (value):
|
||||
return models.Profile.objects.get(pk=int(value))
|
||||
else:
|
||||
return ""
|
||||
@@ -216,6 +216,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
|
||||
return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style}
|
||||
elif type == 'submit':
|
||||
return {'submit': True, 'class': 'btn-primary', 'icon': 'fa-save', 'text': 'Save', 'id': id, 'style': style}
|
||||
elif type == 'today':
|
||||
return {'today': True, 'id': id}
|
||||
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}
|
||||
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ urlpatterns = [
|
||||
|
||||
path('event/<int:pk>/ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()),
|
||||
name='event_ra'),
|
||||
path('event/ra/<int:pk>/', permission_required_with_403('RIGS.view_riskassessment')(views.EventRiskAssessmentDetail.as_view()),
|
||||
path('event/ra/<int:pk>/', login_required(views.EventRiskAssessmentDetail.as_view()),
|
||||
name='ra_detail'),
|
||||
path('event/ra/<int:pk>/edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()),
|
||||
name='ra_edit'),
|
||||
@@ -87,7 +87,7 @@ urlpatterns = [
|
||||
|
||||
path('event/<int:pk>/checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()),
|
||||
name='event_ec'),
|
||||
path('event/checklist/<int:pk>/', permission_required_with_403('RIGS.view_eventchecklist')(views.EventChecklistDetail.as_view()),
|
||||
path('event/checklist/<int:pk>/', login_required(views.EventChecklistDetail.as_view()),
|
||||
name='ec_detail'),
|
||||
path('event/checklist/<int:pk>/edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()),
|
||||
name='ec_edit'),
|
||||
|
||||
@@ -342,12 +342,12 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
f"{self.object.display_id} | {self.object.name} - Event Authorisation Request",
|
||||
get_template("eventauthorisation_client_request.txt").render(context),
|
||||
get_template("email/eventauthorisation_client_request.txt").render(context),
|
||||
to=[email],
|
||||
reply_to=[self.request.user.email],
|
||||
)
|
||||
css = finders.find('css/email.css')
|
||||
html = premailer.Premailer(get_template("eventauthorisation_client_request.html").render(context),
|
||||
html = premailer.Premailer(get_template("email/eventauthorisation_client_request.html").render(context),
|
||||
external_styles=css).transform()
|
||||
msg.attach_alternative(html, 'text/html')
|
||||
|
||||
@@ -357,7 +357,7 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix
|
||||
|
||||
|
||||
class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
||||
template_name = "eventauthorisation_client_request.html"
|
||||
template_name = "email/eventauthorisation_client_request.html"
|
||||
model = models.Event
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.12 on 2022-05-26 09:22
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0024_alter_asset_salvage_value'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='asset',
|
||||
old_name='salvage_value',
|
||||
new_name='replacement_cost',
|
||||
),
|
||||
]
|
||||
24
assets/migrations/0026_auto_20220526_1623.py
Normal file
24
assets/migrations/0026_auto_20220526_1623.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.12 on 2022-05-26 15:23
|
||||
|
||||
import assets.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0025_rename_salvage_value_asset_replacement_cost'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='purchase_price',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, validators=[assets.models.validate_positive]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='replacement_cost',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True, validators=[assets.models.validate_positive]),
|
||||
),
|
||||
]
|
||||
18
assets/migrations/0027_asset_nickname.py
Normal file
18
assets/migrations/0027_asset_nickname.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.16 on 2022-12-11 00:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0026_auto_20220526_1623'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='nickname',
|
||||
field=models.CharField(blank=True, max_length=120),
|
||||
),
|
||||
]
|
||||
@@ -95,7 +95,7 @@ class AssetManager(models.Manager):
|
||||
def search(self, query=None):
|
||||
qs = self.get_queryset()
|
||||
if query is not None:
|
||||
or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query))
|
||||
or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query) | Q(nickname__icontains=query))
|
||||
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||
return qs
|
||||
|
||||
@@ -105,6 +105,11 @@ def get_available_asset_id(wanted_prefix=""):
|
||||
return 9000 if last_asset is None else wanted_prefix + str(last_asset.asset_id_number + 1)
|
||||
|
||||
|
||||
def validate_positive(value):
|
||||
if value < 0:
|
||||
raise ValidationError("A price cannot be negative")
|
||||
|
||||
|
||||
@reversion.register
|
||||
class Asset(models.Model, RevisionMixin):
|
||||
parent = models.ForeignKey(to='self', related_name='asset_parent',
|
||||
@@ -117,9 +122,10 @@ class Asset(models.Model, RevisionMixin):
|
||||
purchased_from = models.ForeignKey(to=Supplier, on_delete=models.SET_NULL, blank=True, null=True, related_name="assets")
|
||||
date_acquired = models.DateField()
|
||||
date_sold = models.DateField(blank=True, null=True)
|
||||
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10)
|
||||
salvage_value = models.DecimalField(null=True, decimal_places=2, max_digits=10)
|
||||
purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
||||
replacement_cost = models.DecimalField(null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
||||
comments = models.TextField(blank=True)
|
||||
nickname = models.CharField(max_length=120, blank=True)
|
||||
|
||||
# Audit
|
||||
last_audited_at = models.DateTimeField(blank=True, null=True)
|
||||
@@ -165,12 +171,6 @@ class Asset(models.Model, RevisionMixin):
|
||||
errdict["asset_id"] = [
|
||||
"An Asset ID can only consist of letters and numbers, with a final number"]
|
||||
|
||||
if self.purchase_price and self.purchase_price < 0:
|
||||
errdict["purchase_price"] = ["A price cannot be negative"]
|
||||
|
||||
if self.salvage_value and self.salvage_value < 0:
|
||||
errdict["salvage_value"] = ["A price cannot be negative"]
|
||||
|
||||
if self.is_cable:
|
||||
if not self.length or self.length <= 0:
|
||||
errdict["length"] = ["The length of a cable must be more than 0"]
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
date = new Date();
|
||||
}
|
||||
$('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'));
|
||||
return false;
|
||||
}
|
||||
function setFieldValue(ID, CSA) {
|
||||
$('#' + String(ID)).val(CSA);
|
||||
return false;
|
||||
}
|
||||
function checkIfCableHidden() {
|
||||
document.getElementById("cable-table").hidden = !document.getElementById("id_is_cable").checked;
|
||||
@@ -39,16 +41,16 @@
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.date_acquired col="col-6" %}
|
||||
<div class="col-sm-4">
|
||||
<button class="btn btn-info" onclick="setAcquired(true);" tabindex="-1">Today</button>
|
||||
<button class="btn btn-warning" onclick="setAcquired(false);" tabindex="-1">Unknown</button>
|
||||
<div class="col-sm-2">
|
||||
<button class="btn btn-info" onclick="return setAcquired(true);" tabindex="-1">Today</button>
|
||||
<button class="btn btn-warning" onclick="return setAcquired(false);" tabindex="-1">Unknown</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.date_sold col="col-6" %}
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.salvage_value col="col-6" prepend="£" %}
|
||||
{% include 'partials/form_field.html' with field=form.replacement_cost col="col-6" prepend="£" %}
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group form-row">
|
||||
@@ -64,16 +66,16 @@
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %}
|
||||
<div class="col-4">
|
||||
<button class="btn btn-danger" onclick="setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1" type="button">5{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-success" onclick="setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-info" onclick="setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1" type="button">20{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-danger" onclick="return setFieldValue('{{ form.length.id_for_label }}','5');" tabindex="-1" type="button">5{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-success" onclick="return setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ form.length.help_text }}</button>
|
||||
<button class="btn btn-info" onclick="return setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1" type="button">20{{ form.length.help_text }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
{% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text title='CSA' col="col-6" %}
|
||||
<div class="col-4">
|
||||
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1" type="button">1.5{{ form.csa.help_text }}</button>
|
||||
<button class="btn btn-secondary" onclick="setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button>
|
||||
<button class="btn btn-secondary" onclick="return setFieldValue('{{ form.csa.id_for_label }}', '1.5');" tabindex="-1" type="button">1.5{{ form.csa.help_text }}</button>
|
||||
<button class="btn btn-secondary" onclick="return setFieldValue('{{ form.csa.id_for_label }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
<label for="{{ form.description.id_for_label }}">Description</label>
|
||||
{% render_field form.description|add_class:'form-control' value=object.description %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.nickname.id_for_label }}">Nickname</label>
|
||||
{% render_field form.nickname|add_class:'form-control' value=object.nickname %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.category.id_for_label }}" >Category</label>
|
||||
{% render_field form.category|add_class:'form-control'%}
|
||||
@@ -45,7 +49,10 @@
|
||||
{% else %}
|
||||
<dt>Asset ID</dt>
|
||||
<dd>{{ object.asset_id }}</dd>
|
||||
|
||||
{% if object.nickname %}
|
||||
<dt>Nickname</dt>
|
||||
<dd>"{{ object.nickname }}"</dd>
|
||||
{% endif %}
|
||||
<dt>Description</dt>
|
||||
<dd>{{ object.description }}</dd>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load title_spaced from filters %}
|
||||
{% spaceless %}
|
||||
<label for="{{ field.id_for_label }}" {% if col %}class="col-2 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>
|
||||
<label for="{{ field.id_for_label }}" {% if col %}class="col-4 col-form-label"{% endif %}>{% if title %}{{ title }}{%else%}{{field.name|title_spaced}}{%endif%}</label>
|
||||
{% if append or prepend %}
|
||||
<div class="input-group {{col}}">
|
||||
{% if prepend %}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="form-control selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
|
||||
<select id="{{ form.purchased_from.id_for_label }}" name="{{ form.purchased_from.name }}" class="selectpicker" data-live-search="true" data-sourceurl="{% url 'api_secure' model='supplier' %}">
|
||||
{% if object.purchased_from %}
|
||||
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
|
||||
{% endif %}
|
||||
@@ -39,10 +39,10 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.salvage_value.id_for_label }}">Salvage Value</label>
|
||||
<label for="{{ form.salvage_value.id_for_label }}">Replacement Cost</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
||||
{% render_field form.salvage_value|add_class:'form-control' value=object.salvage_value %}
|
||||
{% render_field form.replacement_cost|add_class:'form-control' value=object.replacement_cost %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,8 +70,8 @@
|
||||
<dd>{% if object.purchased_from %}<a href="{{object.purchased_from.get_absolute_url}}">{{ object.purchased_from }}</a>{%else%}-{%endif%}</dd>
|
||||
<dt>Purchase Price</dt>
|
||||
<dd>£{{ object.purchase_price|default_if_none:'-' }}</dd>
|
||||
<dt>Salvage Value</dt>
|
||||
<dd>£{{ object.salvage_value|default_if_none:'-' }}</dd>
|
||||
<dt>Replacement Cost</dt>
|
||||
<dd>£{{ object.replacement_cost|default_if_none:'-' }}</dd>
|
||||
<dt>Date Acquired</dt>
|
||||
<dd>{{ object.date_acquired|default_if_none:'-' }}</dd>
|
||||
{% if object.date_sold %}
|
||||
|
||||
@@ -28,13 +28,13 @@ def cable_type(db):
|
||||
|
||||
@pytest.fixture
|
||||
def test_cable(db, category, status, cable_type):
|
||||
cable = models.Asset.objects.create(asset_id="9666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5", salvage_value=50)
|
||||
cable = models.Asset.objects.create(asset_id="9666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5", replacement_cost=50)
|
||||
yield cable
|
||||
cable.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_asset(db, category, status):
|
||||
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), salvage_value=100)
|
||||
asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100)
|
||||
yield asset
|
||||
asset.delete()
|
||||
|
||||
@@ -79,7 +79,7 @@ class AssetForm(FormPage):
|
||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
|
||||
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
|
||||
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
|
||||
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
|
||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||
'date_sold': (regions.DatePicker, (By.ID, 'id_date_sold')),
|
||||
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
||||
@@ -221,7 +221,7 @@ class AssetAuditList(AssetList):
|
||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
|
||||
'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')),
|
||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
||||
'status': (regions.SingleSelectPicker, (By.ID, 'id_status')),
|
||||
|
||||
@@ -102,7 +102,7 @@ def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, ca
|
||||
page.status = status.name
|
||||
page.serial_number = "MELON-MELON-MELON"
|
||||
page.comments = "You might need that"
|
||||
page.salvage_value = "666"
|
||||
page.replacement_cost = "666"
|
||||
page.is_cable = True
|
||||
|
||||
assert logged_in_browser.driver.find_element(By.ID, 'cable-table').is_displayed()
|
||||
@@ -179,7 +179,7 @@ class TestAssetForm(AutoLoginTest):
|
||||
self.page.comments = comments = "This is actually a sledgehammer, not a cable..."
|
||||
|
||||
self.page.purchase_price = "12.99"
|
||||
self.page.salvage_value = "99.12"
|
||||
self.page.replacement_cost = "99.12"
|
||||
self.page.date_acquired = acquired = datetime.date(2020, 5, 2)
|
||||
self.page.purchased_from_selector.toggle()
|
||||
self.assertTrue(self.page.purchased_from_selector.is_open)
|
||||
@@ -320,14 +320,14 @@ class TestAssetAudit(AutoLoginTest):
|
||||
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1,
|
||||
voltage_rating=40, num_pins=13)
|
||||
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status,
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1), salvage_value=10)
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||
models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status,
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1), salvage_value=10)
|
||||
category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||
models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category,
|
||||
date_acquired=datetime.date(2020, 2, 1), salvage_value=10)
|
||||
date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||
self.asset = models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status,
|
||||
category=self.category,
|
||||
date_acquired=datetime.date(2020, 2, 1), salvage_value=10)
|
||||
date_acquired=datetime.date(2020, 2, 1), replacement_cost=10)
|
||||
self.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
|
||||
self.wait = WebDriverWait(self.driver, 20)
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ def test_oembed(client, test_asset):
|
||||
|
||||
|
||||
def test_asset_create(admin_client):
|
||||
response = admin_client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'})
|
||||
response = admin_client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'replacement_cost': '-30'})
|
||||
assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||
assert_asset_form_errors(response)
|
||||
|
||||
@@ -99,7 +99,7 @@ def test_cable_create(admin_client):
|
||||
|
||||
def test_asset_edit(admin_client, test_asset):
|
||||
url = reverse('asset_update', kwargs={'pk': test_asset.asset_id})
|
||||
response = admin_client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""})
|
||||
response = admin_client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'replacement_cost': '-50', 'description': "", 'status': "", 'category': ""})
|
||||
assert_asset_form_errors(response)
|
||||
|
||||
|
||||
@@ -127,4 +127,4 @@ def assert_asset_form_errors(response):
|
||||
assertFormError(response, 'form', 'category', 'This field is required.')
|
||||
assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
||||
assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative')
|
||||
assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative')
|
||||
assertFormError(response, 'form', 'replacement_cost', 'A price cannot be negative')
|
||||
|
||||
@@ -28,6 +28,7 @@ def admin_user(admin_user):
|
||||
admin_user.last_name = "Test"
|
||||
admin_user.initials = "ETU"
|
||||
admin_user.is_approved = True
|
||||
admin_user.is_supervisor = True
|
||||
admin_user.save()
|
||||
return admin_user
|
||||
|
||||
|
||||
7105
package-lock.json
generated
7105
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,14 +27,14 @@
|
||||
"html5sortable": "^0.13.3",
|
||||
"jquery": "^3.6.0",
|
||||
"konami": "^1.6.3",
|
||||
"moment": "^2.29.2",
|
||||
"node-sass": "^7.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"node-sass": "^7.0.3",
|
||||
"popper.js": "^1.16.1",
|
||||
"postcss": "^8.4.5",
|
||||
"uglify-js": "^3.14.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browser-sync": "^2.27.7"
|
||||
"browser-sync": "^2.27.11"
|
||||
},
|
||||
"scripts": {
|
||||
"gulp": "gulp",
|
||||
|
||||
@@ -47,14 +47,16 @@ function initPicker(obj) {
|
||||
//log: 3,
|
||||
preprocessData: function (data) {
|
||||
var i, l = data.length, array = [];
|
||||
array.push({
|
||||
text: clearSelectionLabel,
|
||||
value: '',
|
||||
data:{
|
||||
update_url: '',
|
||||
subtext:''
|
||||
}
|
||||
});
|
||||
if (!obj.data('noclear')) {
|
||||
array.push({
|
||||
text: clearSelectionLabel,
|
||||
value: '',
|
||||
data:{
|
||||
update_url: '',
|
||||
subtext:''
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (l) {
|
||||
for(i = 0; i < l; i++){
|
||||
@@ -71,11 +73,13 @@ function initPicker(obj) {
|
||||
return array;
|
||||
}
|
||||
};
|
||||
|
||||
obj.prepend($("<option></option>")
|
||||
.attr("value",'')
|
||||
.text(clearSelectionLabel)
|
||||
.data('update_url','')); //Add "clear selection" option
|
||||
console.log(obj.data);
|
||||
if (!obj.data('noclear')) {
|
||||
obj.prepend($("<option></option>")
|
||||
.attr("value",'')
|
||||
.text(clearSelectionLabel)
|
||||
.data('update_url','')); //Add "clear selection" option
|
||||
}
|
||||
|
||||
|
||||
obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{% load nice_errors from filters %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-dismissable">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<div class="alert alert-danger mb-0">
|
||||
<dl>
|
||||
{% with form|nice_errors as qq %}
|
||||
{% for error_name,desc in qq.items %}
|
||||
<span class="row">
|
||||
<dt class="col-4">{{error_name}}</dt>
|
||||
<dd class="col-8">{{desc}}</dd>
|
||||
<dt class="col-3">{{error_name}}</dt>
|
||||
<dd class="col-9">{{desc}}</dd>
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<a href="{% url target pk %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%} {% if text == 'Print' %}target="_blank"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
|
||||
{% elif copy %}
|
||||
<button class="btn btn-secondary btn-sm mr-1" data-clipboard-target="{{id}}" data-content="Copied to clipboard!"><span class="fas fa-copy"></span></button>
|
||||
{% elif today %}
|
||||
<button class="btn btn-info col-sm-2" onclick="var date = new Date(); $('#{{id}}').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'))" tabindex="-1" type="button">Today</button>
|
||||
{% else %}
|
||||
<a href="{% url target %}" class="btn {{ class }}" {% if id %}id="{{id}}"{%endif%} {% if style %}style="{{style}}"{%endif%}><span class="fas {{ icon }} align-middle"></span> <span class="d-none d-sm-inline align-middle">{{ text }}</span></a>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{% load widget_tweaks %}
|
||||
{% include 'form_errors.html' %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-info">
|
||||
<p><strong>Please note:</strong> If it has been more than a year since you last logged in, your account will have been automatically deactivated. Contact <a href="mailto:it@nottinghamtec.co.uk">it@nottinghamtec.co.uk</a> for assistance.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-6 offset-sm-3 col-lg-4 offset-lg-4">
|
||||
<form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from PyRIGS.decorators import user_passes_test_with_403
|
||||
|
||||
|
||||
def has_perm_or_supervisor(perm, login_url=None, oembed_view=None):
|
||||
return user_passes_test_with_403(lambda u: (hasattr(u, 'is_supervisor') and u.is_supervisor) or u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)
|
||||
def is_supervisor(login_url=None, oembed_view=None):
|
||||
return user_passes_test_with_403(lambda u: (hasattr(u, 'is_supervisor') and u.is_supervisor))
|
||||
|
||||
@@ -143,6 +143,11 @@ class Command(BaseCommand):
|
||||
"Bin Diving",
|
||||
"Wiki Editing"]
|
||||
|
||||
descriptions = ["Physical training concentrates on mechanistic goals: training programs in this area develop specific motor skills, agility, strength or physical fitness, often with an intention of peaking at a particular time.",
|
||||
"In military use, training means gaining the physical ability to perform and survive in combat, and learn the many skills needed in a time of war. These include how to use a variety of weapons, outdoor survival skills, and how to survive being captured by the enemy, among many others. See military education and training.",
|
||||
"For psychological or physiological reasons, people who believe it may be beneficial to them can choose to practice relaxation training, or autogenic training, in an attempt to increase their ability to relax or deal with stress. While some studies have indicated relaxation training is useful for some medical conditions, autogenic training has limited results or has been the result of few studies.",
|
||||
"Some occupations are inherently hazardous, and require a minimum level of competence before the practitioners can perform the work at an acceptable level of safety to themselves or others in the vicinity. Occupational diving, rescue, firefighting and operation of certain types of machinery and vehicles may require assessment and certification of a minimum acceptable competence before the person is allowed to practice as a licensed instructor."]
|
||||
|
||||
for i, name in enumerate(names):
|
||||
category = random.choice(self.categories)
|
||||
previous_item = models.TrainingItem.objects.filter(category=category).last()
|
||||
@@ -150,7 +155,7 @@ class Command(BaseCommand):
|
||||
number = previous_item.reference_number + 1
|
||||
else:
|
||||
number = 0
|
||||
item = models.TrainingItem.objects.create(category=category, reference_number=number, description=name)
|
||||
item = models.TrainingItem.objects.create(category=category, reference_number=number, name=name, description=random.choice(descriptions))
|
||||
self.items.append(item)
|
||||
|
||||
def setup_levels(self):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.18 on 2023-02-19 14:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('training', '0005_auto_20220223_1535'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='trainingitem',
|
||||
old_name='description',
|
||||
new_name='name',
|
||||
),
|
||||
]
|
||||
18
training/migrations/0007_trainingitem_description.py
Normal file
18
training/migrations/0007_trainingitem_description.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.18 on 2023-02-19 14:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('training', '0006_rename_description_trainingitem_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trainingitem',
|
||||
name='description',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -85,7 +85,7 @@ class TrainingItemManager(QueryablePropertiesManager):
|
||||
def search(self, query=None):
|
||||
qs = self.get_queryset()
|
||||
if query is not None:
|
||||
or_lookup = (Q(description__icontains=query) | Q(display_id=query))
|
||||
or_lookup = (Q(name__icontains=query) | Q(description__icontains=query) | Q(display_id=query))
|
||||
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
|
||||
return qs
|
||||
|
||||
@@ -94,16 +94,13 @@ class TrainingItemManager(QueryablePropertiesManager):
|
||||
class TrainingItem(models.Model):
|
||||
reference_number = models.IntegerField()
|
||||
category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.CASCADE)
|
||||
description = models.CharField(max_length=50)
|
||||
name = models.CharField(max_length=50)
|
||||
description = models.TextField(blank=True)
|
||||
active = models.BooleanField(default=True)
|
||||
prerequisites = models.ManyToManyField('self', symmetrical=False, blank=True)
|
||||
|
||||
objects = TrainingItemManager()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return str(self)
|
||||
|
||||
@queryable_property
|
||||
def display_id(self):
|
||||
return f"{self.category.reference_number}.{self.reference_number}"
|
||||
@@ -121,7 +118,7 @@ class TrainingItem(models.Model):
|
||||
return models.Q()
|
||||
|
||||
def __str__(self):
|
||||
name = f"{self.display_id} {self.description}"
|
||||
name = f"{self.display_id} {self.name}"
|
||||
if not self.active:
|
||||
name += " (inactive)"
|
||||
return name
|
||||
@@ -149,7 +146,7 @@ class TrainingItemQualificationManager(QueryablePropertiesManager):
|
||||
def search(self, query=None):
|
||||
qs = self.get_queryset().select_related('item', 'supervisor', 'item__category')
|
||||
if query is not None:
|
||||
or_lookup = (Q(item__description__icontains=query) | Q(supervisor__first_name__icontains=query) | Q(supervisor__last_name__icontains=query) | Q(item__category__name__icontains=query) | Q(item__display_id=query))
|
||||
or_lookup = (Q(item__name__icontains=query) | Q(supervisor__first_name__icontains=query) | Q(supervisor__last_name__icontains=query) | Q(item__category__name__icontains=query) | Q(item__display_id=query))
|
||||
|
||||
try:
|
||||
or_lookup = Q(item__category__reference_number=int(query)) | or_lookup
|
||||
@@ -201,7 +198,7 @@ class TrainingItemQualification(models.Model, RevisionMixin):
|
||||
|
||||
@property
|
||||
def activity_feed_string(self):
|
||||
return f"{self.trainee} {self.get_depth_display().lower()} {self.get_depth_display()} in {self.item}"
|
||||
return f"{self.trainee} {self.get_depth_display().lower()} in {self.item}"
|
||||
|
||||
@classmethod
|
||||
def get_colour_from_depth(cls, depth):
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<a class="dropdown-item" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> Item List</a>
|
||||
</div>
|
||||
</li>
|
||||
{% if perms.training.add_trainingitemqualification or request.user.is_supervisor %}
|
||||
{% if request.user.is_supervisor %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'session_log' %}"><span class="fas fa-users"></span> Log Session</a></li>
|
||||
{% endif %}
|
||||
<li class="nav-item"><a class="nav-link" href="{% url 'training_activity_table' %}"><span class="fas fa-random"></span> Recent Changes</a></li>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
{% render_field form.date|add_class:'form-control'|attr:'type="date"' value=training_date %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<button class="btn btn-info col-sm-2" onclick="var date = new Date(); $('#id_date').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'))" tabindex="-1" type="button">Today</button>
|
||||
{% button 'today' id='id_date' %}
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<label for="id_notes" class="col-sm-2 col-form-label">Notes</label>
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
<div class="card-body">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for item in category.items.all %}
|
||||
<li class="list-group-item {% if not item.active%}text-warning{%endif%}">{{ item }}
|
||||
<li class="list-group-item {% if not item.active%}text-warning{%endif%}">{{ item }} <a href="{% url 'item_qualification' item.pk %}" class="btn btn-info float-right"><span class="fas fa-user"></span> Qualified Users</a>
|
||||
<br><small>{{ item.description }}</small>
|
||||
{% if item.prerequisites.exists %}
|
||||
<div class="ml-3 font-italic">
|
||||
<p class="text-info mb-0">Passed Out Prerequisites:</p>
|
||||
@@ -24,7 +25,6 @@
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'item_qualification' item.pk %}" class="btn btn-info"><span class="fas fa-user"></span> Qualified Users</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if request.user.is_supervisor or perms.training.add_traininglevelrequirement %}
|
||||
{% if request.user.is_supervisor %}
|
||||
<div class="col-sm-12 text-right pr-0">
|
||||
<a type="button" class="btn btn-success mb-3" href="{% url 'add_requirement' pk=object.pk %}" id="requirement_button">
|
||||
<span class="fas fa-plus"></span> Add New Requirement
|
||||
@@ -79,9 +79,9 @@
|
||||
{% endfor %}
|
||||
<tr><th colspan="3" class="text-center">{{object}}</th></tr>
|
||||
<tr>
|
||||
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor or perms.training.delete_traininglevelrequirement %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 0 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 1 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification u req.item 2 %} {% if request.user.is_supervisor %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% if request.user.is_supervisor or perms.training.add_trainingitemqualification %}
|
||||
{% if request.user.is_supervisor %}
|
||||
<a type="button" class="btn btn-success" href="{% url 'add_qualification' object.pk %}" id="add_record">
|
||||
<span class="fas fa-plus"></span> Add New Training Record
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
|
||||
<select name="supervisor" id="supervisor_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required>
|
||||
<select name="supervisor" id="supervisor_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required data-noclear="true">
|
||||
{% if supervisor %}
|
||||
<option value="{{form.supervisor.value}}" selected>{{ supervisor }}</option>
|
||||
{% endif %}
|
||||
|
||||
@@ -28,25 +28,26 @@
|
||||
{% include 'form_errors.html' %}
|
||||
{% csrf_token %}
|
||||
<h3>People</h3>
|
||||
<div class="form-group row">
|
||||
<div class="form-group row" id="supervisor_group">
|
||||
{% include 'partials/supervisor_field.html' %}
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="form-group row" id="trainees_group">
|
||||
<label for="trainees_id" class="col-sm-2">Select Attendees</label>
|
||||
<select multiple name="trainees" id="trainees_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials">
|
||||
<select multiple name="trainees" id="trainees_id" class="selectpicker col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" data-noclear="true">
|
||||
</select>
|
||||
</div>
|
||||
<h3>Training Items</h3>
|
||||
{% for depth in depths %}
|
||||
<div class="form-group row">
|
||||
<div class="form-group row" id="{{depth.0}}">
|
||||
<label for="selectpicker" class="col-sm-2 text-{% colour_from_depth depth.0 %} py-1">{{ depth.1 }} Items</label>
|
||||
<select multiple name="items_{{depth.0}}" id="items_{{depth.0}}_id" class="selectpicker col-sm-10 px-0" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}?fields=display_id,description&filters=active">
|
||||
<select multiple name="items_{{depth.0}}" id="items_{{depth.0}}_id" class="selectpicker col-sm-10 px-0" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}?fields=display_id,description&filters=active" data-noclear="true">
|
||||
</select>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<h3>Session Information</h3>
|
||||
<div class="form-group">
|
||||
{% include 'partials/form_field.html' with field=form.date %}
|
||||
<div class="form-group row">
|
||||
{% include 'partials/form_field.html' with field=form.date col='col-sm-6' %}
|
||||
{% button 'today' id='id_date' %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{% include 'partials/form_field.html' with field=form.notes %}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<th>Date</th>
|
||||
<th>Supervisor</th>
|
||||
<th>Notes</th>
|
||||
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
|
||||
{% if request.user.is_supervisor %}
|
||||
<th></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
@@ -67,7 +67,7 @@
|
||||
<td>{{ object.date }}</td>
|
||||
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
|
||||
<td>{{ object.notes }}</td>
|
||||
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
|
||||
{% if request.user.is_supervisor %}
|
||||
<td>{% button 'edit' 'edit_qualification' object.pk id="edit" %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
|
||||
@@ -30,6 +30,15 @@ def training_item(db):
|
||||
training_item.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def training_item_2(db):
|
||||
training_category = models.TrainingCategory.objects.create(reference_number=2, name="Sound")
|
||||
training_item = models.TrainingItem.objects.create(category=training_category, reference_number=1, description="Fundamentals of Audio")
|
||||
yield training_item
|
||||
training_category.delete()
|
||||
training_item.delete()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def level(db):
|
||||
level = models.TrainingLevel.objects.create(description="There is no description.", level=models.TrainingLevel.TECHNICIAN)
|
||||
|
||||
@@ -40,3 +40,42 @@ class AddQualification(FormPage):
|
||||
@property
|
||||
def success(self):
|
||||
return 'add' not in self.driver.current_url
|
||||
|
||||
|
||||
class SessionLog(FormPage):
|
||||
URL_TEMPLATE = 'training/session_log'
|
||||
|
||||
_supervisor_selector = (By.CSS_SELECTOR, 'div#supervisor_group>div.bootstrap-select')
|
||||
_trainees_selector = (By.CSS_SELECTOR, 'div#trainees_group>div.bootstrap-select')
|
||||
_training_started_selector = (By.XPATH, '//div[1]/div/div/form/div[4]/div')
|
||||
_training_complete_selector = (By.XPATH, '//div[1]/div/div/form/div[4]/div')
|
||||
_competency_assessed_selector = (By.XPATH, '//div[1]/div/div/form/div[5]/div')
|
||||
|
||||
form_items = {
|
||||
'date': (regions.DatePicker, (By.ID, 'id_date')),
|
||||
'notes': (regions.TextBox, (By.ID, 'id_notes')),
|
||||
}
|
||||
|
||||
@property
|
||||
def supervisor_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._supervisor_selector))
|
||||
|
||||
@property
|
||||
def trainees_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._trainees_selector))
|
||||
|
||||
@property
|
||||
def training_started_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._training_started_selector))
|
||||
|
||||
@property
|
||||
def training_complete_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._training_complete_selector))
|
||||
|
||||
@property
|
||||
def competency_assessed_selector(self):
|
||||
return regions.BootstrapSelectElement(self, self.find_element(*self._competency_assessed_selector))
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return 'log' not in self.driver.current_url
|
||||
|
||||
@@ -12,6 +12,15 @@ from training import models
|
||||
from training.tests import pages
|
||||
|
||||
|
||||
def select_super(page, supervisor):
|
||||
page.supervisor_selector.toggle()
|
||||
assert page.supervisor_selector.is_open
|
||||
page.supervisor_selector.search(supervisor.name[:-6])
|
||||
time.sleep(2) # Slow down for javascript
|
||||
assert page.supervisor_selector.options[0].selected
|
||||
page.supervisor_selector.toggle()
|
||||
|
||||
|
||||
def test_add_qualification(logged_in_browser, live_server, trainee, supervisor, training_item):
|
||||
page = pages.AddQualification(logged_in_browser.driver, live_server.url, pk=trainee.pk).open()
|
||||
# assert page.name in str(trainee)
|
||||
@@ -30,12 +39,7 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
|
||||
assert page.item_selector.options[0].selected
|
||||
page.item_selector.toggle()
|
||||
|
||||
page.supervisor_selector.toggle()
|
||||
assert page.supervisor_selector.is_open
|
||||
page.supervisor_selector.search(supervisor.name[:-6])
|
||||
time.sleep(2) # Slow down for javascript
|
||||
assert page.supervisor_selector.options[0].selected
|
||||
page.supervisor_selector.toggle()
|
||||
select_super(page, supervisor)
|
||||
|
||||
page.submit()
|
||||
assert page.success
|
||||
@@ -44,3 +48,32 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
|
||||
assert qualification.date == date
|
||||
assert qualification.notes == "A note"
|
||||
assert qualification.depth == models.TrainingItemQualification.STARTED
|
||||
|
||||
|
||||
def test_session_log(logged_in_browser, live_server, trainee, supervisor, training_item, training_item_2):
|
||||
page = pages.SessionLog(logged_in_browser.driver, live_server.url).open()
|
||||
|
||||
page.date = date = datetime.date(2001, 1, 10)
|
||||
page.notes = note = "A general note"
|
||||
|
||||
time.sleep(2) # Slow down for javascript
|
||||
|
||||
select_super(page, supervisor)
|
||||
|
||||
page.trainees_selector.toggle()
|
||||
assert page.trainees_selector.is_open
|
||||
page.trainees_selector.search(trainee.first_name)
|
||||
time.sleep(2) # Slow down for javascript
|
||||
page.trainees_selector.set_option(trainee.name, True)
|
||||
# assert page.trainees_selector.options[0].selected
|
||||
page.trainees_selector.toggle()
|
||||
|
||||
page.training_started_selector.toggle()
|
||||
assert page.training_started_selector.is_open
|
||||
page.training_started_selector.search(training_item.description[:-2])
|
||||
time.sleep(2) # Slow down for javascript
|
||||
# assert page.training_started_selector.options[0].selected
|
||||
page.training_started_selector.toggle()
|
||||
|
||||
page.submit()
|
||||
assert page.success
|
||||
|
||||
@@ -16,7 +16,7 @@ def test_add_qualification(admin_client, trainee, admin_user, training_item):
|
||||
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': trainee.pk, 'item': training_item.pk})
|
||||
assertFormError(response, 'form', 'date', 'Qualification date may not be in the future')
|
||||
assertFormError(response, 'form', 'supervisor', 'One may not supervise oneself...')
|
||||
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': admin_user.pk, 'item': training_item.pk})
|
||||
response = admin_client.post(url, {'date': date, 'trainee': admin_user.pk, 'supervisor': trainee.pk, 'item': training_item.pk})
|
||||
print(response.content)
|
||||
assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...')
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from training.decorators import has_perm_or_supervisor
|
||||
from training.decorators import is_supervisor
|
||||
|
||||
from training import views, models
|
||||
from versioning.views import VersionHistory
|
||||
@@ -11,23 +11,22 @@ urlpatterns = [
|
||||
path('item/<int:pk>/qualified_users/', login_required(views.ItemQualifications.as_view()), name='item_qualification'),
|
||||
|
||||
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
|
||||
path('trainee/<int:pk>/',
|
||||
has_perm_or_supervisor('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
||||
path('trainee/<int:pk>/', login_required(views.TraineeDetail.as_view()),
|
||||
name='trainee_detail'),
|
||||
path('trainee/<int:pk>/history', has_perm_or_supervisor('RIGS.view_profile')(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
|
||||
path('trainee/<int:pk>/add_qualification/', has_perm_or_supervisor('training.add_trainingitemqualification')(views.AddQualification.as_view()),
|
||||
path('trainee/<int:pk>/history', login_required(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
|
||||
path('trainee/<int:pk>/add_qualification/', is_supervisor()(views.AddQualification.as_view()),
|
||||
name='add_qualification'),
|
||||
path('trainee/edit_qualification/<int:pk>/', has_perm_or_supervisor('training.change_trainingitemqualification')(views.EditQualification.as_view()),
|
||||
path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),
|
||||
name='edit_qualification'),
|
||||
|
||||
path('levels/', login_required(views.LevelList.as_view()), name='level_list'),
|
||||
path('level/<int:pk>/', login_required(views.LevelDetail.as_view()), name='level_detail'),
|
||||
path('level/<int:pk>/user/<int:u>/', login_required(views.LevelDetail.as_view()), name='level_detail'),
|
||||
path('level/<int:pk>/add_requirement/', login_required(views.AddLevelRequirement.as_view()), name='add_requirement'),
|
||||
path('level/remove_requirement/<int:pk>/', login_required(views.RemoveRequirement.as_view()), name='remove_requirement'),
|
||||
path('level/<int:pk>/add_requirement/', is_supervisor()(views.AddLevelRequirement.as_view()), name='add_requirement'),
|
||||
path('level/remove_requirement/<int:pk>/', is_supervisor()(views.RemoveRequirement.as_view()), name='remove_requirement'),
|
||||
|
||||
path('trainee/<int:pk>/level/<int:level_pk>/confirm', login_required(views.ConfirmLevel.as_view()), name='confirm_level'),
|
||||
path('trainee/<int:pk>/level/<int:level_pk>/confirm', is_supervisor()(views.ConfirmLevel.as_view()), name='confirm_level'),
|
||||
path('trainee/<int:pk>/item_record', login_required(views.TraineeItemDetail.as_view()), name='trainee_item_detail'),
|
||||
|
||||
path('session_log', has_perm_or_supervisor('training.add_trainingitemqualification')(views.SessionLog.as_view()), name='session_log'),
|
||||
path('session_log', is_supervisor()(views.SessionLog.as_view()), name='session_log'),
|
||||
]
|
||||
|
||||
26
users/management/commands/usercleanup.py
Normal file
26
users/management/commands/usercleanup.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from datetime import datetime, timedelta
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils import timezone
|
||||
|
||||
from RIGS.models import Profile
|
||||
from training.models import TrainingLevel
|
||||
|
||||
|
||||
# This is triggered nightly by Heroku Scheduler
|
||||
class Command(BaseCommand):
|
||||
help = 'Performs perodic user maintenance tasks'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for person in Profile.objects.all():
|
||||
# Inactivate users that have not logged in for a year (or have never logged in)
|
||||
if person.last_login is None or (timezone.now() - person.last_login).days > 365:
|
||||
person.is_active = False
|
||||
person.is_approved = False
|
||||
person.save()
|
||||
# Ensure everyone with a supervisor level has the flag correctly set in the database
|
||||
if person.level_qualifications.exclude(confirmed_on=None).select_related('level') \
|
||||
.filter(level__level__gte=TrainingLevel.SUPERVISOR) \
|
||||
.exclude(level__department=TrainingLevel.HAULAGE) \
|
||||
.exclude(level__department__isnull=True).exists():
|
||||
person.is_supervisor = True
|
||||
person.save()
|
||||
@@ -183,7 +183,7 @@ class ModelComparison:
|
||||
def name(self):
|
||||
obj = self.new if self.new else self.old
|
||||
|
||||
if(hasattr(obj, 'activity_feed_string')):
|
||||
if (hasattr(obj, 'activity_feed_string')):
|
||||
return obj.activity_feed_string
|
||||
else:
|
||||
return str(obj)
|
||||
|
||||
Reference in New Issue
Block a user