Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
e138961496 Build(deps): Bump django from 3.2.13 to 3.2.15
Bumps [django](https://github.com/django/django) from 3.2.13 to 3.2.15.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.13...3.2.15)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-11 16:12:16 +00:00
61 changed files with 4387 additions and 4944 deletions

View File

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

View File

@@ -13,20 +13,26 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.9
cache: 'pipenv'
python-version: 3.9.1
- uses: actions/cache@v2
id: pcache
with:
path: ~/.local/share/virtualenvs
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
restore-keys: |
${{ runner.os }}-pipenv-
- name: Install Dependencies
run: |
python3 -m pip install --upgrade pip pipenv
python -m pip install --upgrade pip pipenv
pipenv install -d
# if: steps.pcache.outputs.cache-hit != 'true'
- name: Cache Static Files
id: static-cache
uses: actions/cache@v3
uses: actions/cache@v2
with:
path: 'pipeline/built_assets'
key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }}
@@ -37,9 +43,9 @@ jobs:
- name: Basic Checks
run: |
pipenv run pycodestyle . --exclude=migrations,node_modules
pipenv run python3 manage.py check
pipenv run python3 manage.py makemigrations --check --dry-run
pipenv run python3 manage.py collectstatic --noinput
pipenv run python manage.py check
pipenv run python manage.py makemigrations --check --dry-run
pipenv run python manage.py collectstatic --noinput
- name: Run Tests
run: pipenv run pytest -n auto -vv --cov
- uses: actions/upload-artifact@v2

13
Pipfile
View File

@@ -11,6 +11,7 @@ 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"
@@ -25,16 +26,18 @@ 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 = "*"
envparse = "~=0.2.0"
gunicorn = "~=20.0.4"
icalendar = "~=4.0.7"
idna = "~=2.10"
lxml = "~=4.7.1"
Markdown = "~=3.3.3"
msgpack = "~=1.0.2"
pep517 = "~=0.9.1"
Pillow = "~=9.3.0"
Pillow = "~=9.0.0"
premailer = "~=3.7.0"
progress = "~=1.5"
psutil = "~=5.8.0"
@@ -42,7 +45,7 @@ psycopg2 = "~=2.8.6"
Pygments = "~=2.7.4"
pyparsing = "~=2.4.7"
PyPDF2 = "~=1.27.5"
PyPOM = "~=2.2.4"
PyPOM = "~=2.2.0"
python-dateutil = "~=2.8.1"
pytoml = "~=0.1.21"
pytz = "~=2020.5"
@@ -79,10 +82,10 @@ django-hcaptcha = "*"
pikepdf = "*"
django-queryable-properties = "*"
django-mass-edit = "*"
selenium = "~=3.141.0"
[dev-packages]
pycodestyle = "~=2.9.1"
selenium = "~=3.141.0"
pycodestyle = "*"
coveralls = "*"
django-coverage-plugin = "*"
pytest-cov = "*"

1357
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,9 +42,8 @@ if not DEBUG:
INTERNAL_IPS = ['127.0.0.1']
DOMAIN = env('DOMAIN', default='example.com')
ADMINS = [('IT Manager', f'it@{DOMAIN}'), ('Arona Jones', f'arona.jones@{DOMAIN}')]
ADMINS = [('Tom Price', 'tomtom5152@gmail.com'), ('IT Manager', 'it@nottinghamtec.co.uk'),
('Arona Jones', 'arona.jones@nottinghamtec.co.uk')]
if DEBUG:
ADMINS.append(('Testing Superuser', 'superuser@example.com'))

View File

@@ -321,60 +321,45 @@ class OEmbedView(generic.View):
return JsonResponse(data)
def get_info_string(user):
user_str = f"by {user.name} " if user else ""
time = timezone.now().strftime('%d/%m/%Y %H:%I')
return f"[Paperwork generated {user_str}on {time}"
def render_pdf_response(template, context, append_terms):
merger = PdfFileMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
if append_terms:
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(BytesIO(terms.read()))
merged = BytesIO()
merger.write(merged)
response = HttpResponse(content_type='application/pdf')
f = context['filename']
response['Content-Disposition'] = f'filename="{f}"'
response.write(merged.getvalue())
return response
class PrintView(generic.View):
append_terms = False
def get_context_data(self, **kwargs):
obj = get_object_or_404(self.model, pk=self.kwargs['pk'])
user_str = f"by {self.request.user.name} " if self.request.user is not None else ""
time = timezone.now().strftime('%d/%m/%Y %H:%I')
object_name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', obj.name)
context = {
'object': obj,
'current_user': self.request.user,
'object_name': object_name,
'info_string': get_info_string(self.request.user) + f"- {obj.current_version_id}]",
'info_string': f"[Paperwork generated {user_str}on {time} - {obj.current_version_id}]",
}
return context
def get(self, request, pk):
return render_pdf_response(get_template(self.template_name), self.get_context_data(), self.append_terms)
template = get_template(self.template_name)
merger = PdfFileMerger()
class PrintListView(generic.ListView):
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['current_user'] = self.request.user
context['info_string'] = get_info_string(self.request.user) + "]"
return context
context = self.get_context_data()
def get(self, request):
self.object_list = self.get_queryset()
return render_pdf_response(get_template(self.template_name), self.get_context_data(), False)
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
if self.append_terms:
terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL)
merger.append(BytesIO(terms.read()))
merged = BytesIO()
merger.write(merged)
response = HttpResponse(content_type='application/pdf')
f = context['filename']
response['Content-Disposition'] = f'filename="{f}"'
response.write(merged.getvalue())
return response

View File

@@ -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:

View File

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

View File

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

View File

@@ -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, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.")
is_approved = models.BooleanField(default=False)
# 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)

View File

@@ -58,13 +58,13 @@ def send_eventauthorisation_success_email(instance):
client_email = EmailMultiAlternatives(
subject,
get_template("email/eventauthorisation_client_success.txt").render(context),
get_template("eventauthorisation_client_success.txt").render(context),
to=[instance.email],
reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS],
)
css = finders.find('css/email.css')
html = Premailer(get_template("email/eventauthorisation_client_success.html").render(context),
html = Premailer(get_template("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("email/eventauthorisation_mic_success.txt").render(context),
get_template("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("email/admin_awaiting_approval.txt").render(context),
get_template("admin_awaiting_approval.txt").render(context),
to=[admin.email],
reply_to=[user.email],
)
css = finders.find('css/email.css')
html = Premailer(get_template("email/admin_awaiting_approval.html").render(context),
html = Premailer(get_template("admin_awaiting_approval.html").render(context),
external_styles=css).transform()
email.attach_alternative(html, 'text/html')
email.send()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -11,7 +11,6 @@
<initialize>
<color id="LightGray" RGB="#D3D3D3"/>
<color id="DarkGray" RGB="#707070"/>
<color id="Brand" RGB="#3853a4"/>
</initialize>
<paraStyle name="style.para" fontName="OpenSans" />
@@ -28,8 +27,6 @@
<paraStyle name="style.times" fontName="OpenSans" fontSize="10" />
<paraStyle name="style.head_titles" fontName="OpenSans-Bold" fontSize="10" />
<paraStyle name="style.head_numbers" fontName="OpenSans" fontSize="10" />
<paraStyle name="style.emheader" fontName="OpenSans" textColor="White" fontSize="12" backColor="Brand" leading="20" borderPadding="4"/>
<paraStyle name="style.breakbefore" parent="emheader" pageBreakBefore="1"/>
<blockTableStyle id="eventSpecifics">
<blockValign value="top"/>
@@ -87,7 +84,7 @@
bulletFontSize="10"/>
</stylesheet>
<template title="{{filename}}"> {# Note: page is 595x842 points (1 point=1/72in) #}
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
<pageTemplate id="Headed" >
<pageGraphics>
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,21 @@
{% block title %}Request Authorisation{% endblock %}
{% block js %}
<script src="{% static 'js/tooltip.js' %}"></script>
<script src="{% static 'js/popover.js' %}"></script>
<script src="{% static 'js/clipboard.min.js' %}"></script>
<script>
var clipboard = new ClipboardJS('.btn');
clipboard.on('success', function(e) {
$(e.trigger).popover('show');
window.setTimeout(function () {$(e.trigger).popover('hide')}, 3000);
e.clearSelection();
});
</script>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12">
@@ -18,11 +33,11 @@
<dl class="dl-horizontal">
{% if object.person.email %}
<dt>Person Email</dt>
<dd><span id="person-email" class="pr-1">{{ object.person.email }}</span> {% button 'copy' id='#person-email' %}</dd>
<dd><span id="person-email">{{ object.person.email }}</span>{% button 'copy' id='#person-email' %}</dd>
{% endif %}
{% if object.organisation.email %}
<dt>Organisation Email</dt>
<dd><span id="org-email" class="pr-1">{{ object.organisation.email }}</span> {% button 'copy' id='#org-email' %}</dd>
<dd><span id="org-email">{{ object.organisation.email }}</span>{% button 'copy' id='#org-email' %}</dd>
{% endif %}
</dl>
{% else %}
@@ -42,20 +57,11 @@
</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 %}

View File

@@ -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 ""
@@ -196,8 +196,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
text = "Edit"
elif type == 'print':
clazz += " btn-primary "
icon = "fa-download"
text = "Export"
icon = "fa-print"
text = "Print"
elif type == 'duplicate':
clazz += " btn-info "
icon = "fa-copy"
@@ -216,8 +216,6 @@ 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}

View File

@@ -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>/', login_required(views.EventRiskAssessmentDetail.as_view()),
path('event/ra/<int:pk>/', permission_required_with_403('RIGS.view_riskassessment')(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>/', login_required(views.EventChecklistDetail.as_view()),
path('event/checklist/<int:pk>/', permission_required_with_403('RIGS.view_eventchecklist')(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'),

View File

@@ -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("email/eventauthorisation_client_request.txt").render(context),
get_template("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("email/eventauthorisation_client_request.html").render(context),
html = premailer.Premailer(get_template("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 = "email/eventauthorisation_client_request.html"
template_name = "eventauthorisation_client_request.html"
model = models.Event
def render_to_response(self, context, **response_kwargs):

View File

@@ -105,7 +105,8 @@ class Command(BaseCommand):
for i in range(100):
prefix = random.choice(asset_prefixes)
asset_id = get_available_asset_id(wanted_prefix=prefix)
asset_id = str(get_available_asset_id(wanted_prefix=prefix))
asset_id = prefix + asset_id
asset = models.Asset(
asset_id=asset_id,
description=random.choice(asset_description),

View File

@@ -1,18 +0,0 @@
# 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),
),
]

View File

@@ -95,15 +95,14 @@ 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) | Q(nickname__icontains=query))
or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query))
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
def get_available_asset_id(wanted_prefix=""):
last_asset = Asset.objects.filter(asset_id_prefix=wanted_prefix).last()
last_asset_id = last_asset.asset_id_number if last_asset else 0
return wanted_prefix + str(last_asset_id + 1)
return 9000 if last_asset is None else wanted_prefix + str(last_asset.asset_id_number + 1)
def validate_positive(value):
@@ -126,7 +125,6 @@ class Asset(models.Model, RevisionMixin):
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)

View File

@@ -21,10 +21,6 @@
<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'%}
@@ -49,10 +45,7 @@
{% 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>

View File

@@ -168,7 +168,7 @@ class DuplicateMixin:
class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
def get_initial(self, *args, **kwargs):
initial = super().get_initial(*args, **kwargs)
initial["asset_id"] = models.get_available_asset_id()
initial["asset_id"] = models.get_available_asset_id(wanted_prefix=self.get_object().asset_id_prefix)
return initial
def get_context_data(self, **kwargs):

View File

@@ -28,7 +28,6 @@ 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

7173
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,14 +27,14 @@
"html5sortable": "^0.13.3",
"jquery": "^3.6.0",
"konami": "^1.6.3",
"moment": "^2.29.4",
"node-sass": "^7.0.3",
"moment": "^2.29.2",
"node-sass": "^7.0.0",
"popper.js": "^1.16.1",
"postcss": "^8.4.5",
"uglify-js": "^3.14.5"
},
"devDependencies": {
"browser-sync": "^2.27.11"
"browser-sync": "^2.27.7"
},
"scripts": {
"gulp": "gulp",

View File

@@ -47,16 +47,14 @@ function initPicker(obj) {
//log: 3,
preprocessData: function (data) {
var i, l = data.length, array = [];
if (!obj.data('noclear')) {
array.push({
text: clearSelectionLabel,
value: '',
data:{
update_url: '',
subtext:''
}
});
}
array.push({
text: clearSelectionLabel,
value: '',
data:{
update_url: '',
subtext:''
}
});
if (l) {
for(i = 0; i < l; i++){
@@ -73,13 +71,11 @@ function initPicker(obj) {
return array;
}
};
console.log(obj.data);
if (!obj.data('noclear')) {
obj.prepend($("<option></option>")
.attr("value",'')
.text(clearSelectionLabel)
.data('update_url','')); //Add "clear selection" option
}
obj.prepend($("<option></option>")
.attr("value",'')
.text(clearSelectionLabel)
.data('update_url','')); //Add "clear selection" option
obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker

View File

@@ -1,12 +1,13 @@
{% load nice_errors from filters %}
{% if form.errors %}
<div class="alert alert-danger mb-0">
<div class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<dl>
{% with form|nice_errors as qq %}
{% for error_name,desc in qq.items %}
<span class="row">
<dt class="col-3">{{error_name}}</dt>
<dd class="col-9">{{desc}}</dd>
<dt class="col-4">{{error_name}}</dt>
<dd class="col-8">{{desc}}</dd>
</span>
{% endfor %}
{% endwith %}

View File

@@ -4,8 +4,6 @@
<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 %}

View File

@@ -1,10 +1,5 @@
{% 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">

View File

@@ -1,5 +1,5 @@
from PyRIGS.decorators import user_passes_test_with_403
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))
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)

View File

@@ -143,15 +143,6 @@ 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.",
"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()
@@ -159,7 +150,7 @@ class Command(BaseCommand):
number = previous_item.reference_number + 1
else:
number = 0
item = models.TrainingItem.objects.create(category=category, reference_number=number, name=name, description=random.choice(descriptions) + random.choice(descriptions) + random.choice(descriptions))
item = models.TrainingItem.objects.create(category=category, reference_number=number, description=name)
self.items.append(item)
def setup_levels(self):

View File

@@ -1,18 +0,0 @@
# 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',
),
]

View File

@@ -1,18 +0,0 @@
# 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),
),
]

View File

@@ -85,7 +85,7 @@ class TrainingItemManager(QueryablePropertiesManager):
def search(self, query=None):
qs = self.get_queryset()
if query is not None:
or_lookup = (Q(name__icontains=query) | Q(description__icontains=query) | Q(display_id=query))
or_lookup = (Q(description__icontains=query) | Q(display_id=query))
qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
return qs
@@ -94,13 +94,16 @@ class TrainingItemManager(QueryablePropertiesManager):
class TrainingItem(models.Model):
reference_number = models.IntegerField()
category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.CASCADE)
name = models.CharField(max_length=50)
description = models.TextField(blank=True)
description = models.CharField(max_length=50)
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}"
@@ -118,7 +121,7 @@ class TrainingItem(models.Model):
return models.Q()
def __str__(self):
name = f"{self.display_id} {self.name}"
name = f"{self.display_id} {self.description}"
if not self.active:
name += " (inactive)"
return name
@@ -146,7 +149,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__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))
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))
try:
or_lookup = Q(item__category__reference_number=int(query)) | or_lookup
@@ -198,7 +201,7 @@ class TrainingItemQualification(models.Model, RevisionMixin):
@property
def activity_feed_string(self):
return f"{self.trainee} {self.get_depth_display().lower()} in {self.item}"
return f"{self.trainee} {self.get_depth_display().lower()} {self.get_depth_display()} in {self.item}"
@classmethod
def get_colour_from_depth(cls, depth):

View File

@@ -30,7 +30,7 @@
<a class="dropdown-item" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> Item List</a>
</div>
</li>
{% if request.user.is_supervisor %}
{% if perms.training.add_trainingitemqualification or 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>

View File

@@ -43,7 +43,7 @@
{% render_field form.date|add_class:'form-control'|attr:'type="date"' value=training_date %}
{% endwith %}
</div>
{% button 'today' id='id_date' %}
<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>
</div>
<div class="form-group form-row">
<label for="id_notes" class="col-sm-2 col-form-label">Notes</label>

View File

@@ -1,11 +1,6 @@
{% extends 'base_training.html' %}
{% load button from filters %}
{% block content %}
<div class="col-12 text-right py-2 pr-0">
{% button 'print' 'item_list_export' %}
</div>
<div id="accordion">
{% for category in categories %}
<div class="card">
@@ -18,11 +13,10 @@
<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 }} <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>
<li class="list-group-item {% if not item.active%}text-warning{%endif%}">{{ item }}
{% if item.prerequisites.exists %}
<div class="ml-3 font-italic">
<p class="text-info mb-0">Competency Assessment Prerequisites:</p>
<p class="text-info mb-0">Passed Out Prerequisites:</p>
<ul>
{% for p in item.prerequisites.all %}
<li>{{p}}</li>
@@ -30,6 +24,7 @@
</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>

View File

@@ -1,25 +0,0 @@
{% extends 'base_print.xml' %}
{% block content %}
<h1 style="page-head">TEC Training Item List</h1>
<spacer length="15" />
{% for category in categories %}
<h2 {% if not forloop.first %}style="breakbefore"{%else%}style="emheader"{%endif%}>{{category}}</h2>
<spacer length="10" />
{% for item in category.items.all %}
<h3>{{ item }}</h3>
<spacer length="4" />
<para>{{ item.description }}</para>
{% if item.prerequisites.exists %}
<h4>Competency Assessment Prerequisites:</h4>
<ul bulletFontSize="5">
{% for p in item.prerequisites.all %}
<li><para>{{p}}</para></li>
{% endfor %}
</ul>
{% endif %}
<spacer length="8" />
{% endfor %}
{% endfor %}
<namedString id="lastPage"><pageNumber/></namedString>
{% endblock %}

View File

@@ -44,7 +44,7 @@
{% endblock %}
{% block content %}
{% if request.user.is_supervisor %}
{% if request.user.is_supervisor or perms.training.add_traininglevelrequirement %}
<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 %}<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>
<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>
</tr>
</tbody>
</table>

View File

@@ -1,4 +1,4 @@
{% if request.user.is_supervisor %}
{% if request.user.is_supervisor or perms.training.add_trainingitemqualification %}
<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>

View File

@@ -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 data-noclear="true">
<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>
{% if supervisor %}
<option value="{{form.supervisor.value}}" selected>{{ supervisor }}</option>
{% endif %}

View File

@@ -28,26 +28,25 @@
{% include 'form_errors.html' %}
{% csrf_token %}
<h3>People</h3>
<div class="form-group row" id="supervisor_group">
<div class="form-group row">
{% include 'partials/supervisor_field.html' %}
</div>
<div class="form-group row" id="trainees_group">
<div class="form-group row">
<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" data-noclear="true">
<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>
</div>
<h3>Training Items</h3>
{% for depth in depths %}
<div class="form-group row" id="{{depth.0}}">
<div class="form-group row">
<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" data-noclear="true">
<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>
</div>
{% endfor %}
<h3>Session Information</h3>
<div class="form-group row">
{% include 'partials/form_field.html' with field=form.date col='col-sm-6' %}
{% button 'today' id='id_date' %}
<div class="form-group">
{% include 'partials/form_field.html' with field=form.date %}
</div>
<div class="form-group">
{% include 'partials/form_field.html' with field=form.notes %}

View File

@@ -54,7 +54,7 @@
<th>Date</th>
<th>Supervisor</th>
<th>Notes</th>
{% if request.user.is_supervisor %}
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
<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 %}
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
<td>{% button 'edit' 'edit_qualification' object.pk id="edit" %}</td>
{% endif %}
</tr>

View File

@@ -30,15 +30,6 @@ 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)

View File

@@ -40,42 +40,3 @@ 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

View File

@@ -12,15 +12,6 @@ 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)
@@ -39,7 +30,12 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
assert page.item_selector.options[0].selected
page.item_selector.toggle()
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()
page.submit()
assert page.success
@@ -48,32 +44,3 @@ 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

View File

@@ -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': admin_user.pk, 'supervisor': trainee.pk, 'item': training_item.pk})
response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': admin_user.pk, 'item': training_item.pk})
print(response.content)
assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...')

View File

@@ -1,33 +1,33 @@
from django.urls import path
from django.contrib.auth.decorators import login_required
from training.decorators import is_supervisor
from training.decorators import has_perm_or_supervisor
from training import views, models
from versioning.views import VersionHistory
urlpatterns = [
path('items/', login_required(views.ItemList.as_view()), name='item_list'),
path('items/export/', login_required(views.ItemListExport.as_view()), name='item_list_export'),
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>/', login_required(views.TraineeDetail.as_view()),
path('trainee/<int:pk>/',
has_perm_or_supervisor('RIGS.view_profile')(views.TraineeDetail.as_view()),
name='trainee_detail'),
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()),
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()),
name='add_qualification'),
path('trainee/edit_qualification/<int:pk>/', is_supervisor()(views.EditQualification.as_view()),
path('trainee/edit_qualification/<int:pk>/', has_perm_or_supervisor('training.change_trainingitemqualification')(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/', 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('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('trainee/<int:pk>/level/<int:level_pk>/confirm', is_supervisor()(views.ConfirmLevel.as_view()), name='confirm_level'),
path('trainee/<int:pk>/level/<int:level_pk>/confirm', login_required(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', is_supervisor()(views.SessionLog.as_view()), name='session_log'),
path('session_log', has_perm_or_supervisor('training.add_trainingitemqualification')(views.SessionLog.as_view()), name='session_log'),
]

View File

@@ -7,7 +7,7 @@ from django.db import transaction
from django.db.models import Q, Count
from django.db.utils import IntegrityError
from PyRIGS.views import is_ajax, ModalURLMixin, get_related, PrintListView
from PyRIGS.views import is_ajax, ModalURLMixin, get_related
from training import models, forms
from users import views
from reversion.views import RevisionMixin
@@ -24,20 +24,6 @@ class ItemList(generic.ListView):
return context
class ItemListExport(PrintListView):
model = models.TrainingItem
template_name = 'item_list.xml'
def get_queryset(self):
return self.model.objects.filter(active=True)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filename'] = "TrainingItemList.pdf"
context["categories"] = models.TrainingCategory.objects.all()
return context
class TraineeDetail(views.ProfileDetail):
template_name = "trainee_detail.html"
model = models.Trainee

View File

@@ -1,26 +0,0 @@
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()

View File

@@ -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)