mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-11 17:19:42 +00:00
Compare commits
14 Commits
data-inges
...
7846a6d31e
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||||
|
});
|
||||||
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)
|
initials = models.CharField(max_length=5, null=True, blank=False)
|
||||||
phone = models.CharField(max_length=13, blank=True, default='')
|
phone = models.CharField(max_length=13, blank=True, default='')
|
||||||
api_key = models.CharField(max_length=40, blank=True, editable=False, 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...
|
# 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)
|
last_emailed = models.DateTimeField(blank=True, null=True)
|
||||||
dark_theme = models.BooleanField(default=False)
|
dark_theme = models.BooleanField(default=False)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
bulletFontSize="10"/>
|
bulletFontSize="10"/>
|
||||||
</stylesheet>
|
</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" >
|
<pageTemplate id="Headed" >
|
||||||
<pageGraphics>
|
<pageGraphics>
|
||||||
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
|
<image file="static/imgs/paperwork/corner-tr-su.jpg" x="395" y="642" height="200" width="200"/>
|
||||||
|
|||||||
@@ -5,21 +5,6 @@
|
|||||||
|
|
||||||
{% block title %}Request Authorisation{% endblock %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
@@ -33,11 +18,11 @@
|
|||||||
<dl class="dl-horizontal">
|
<dl class="dl-horizontal">
|
||||||
{% if object.person.email %}
|
{% if object.person.email %}
|
||||||
<dt>Person Email</dt>
|
<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 %}
|
{% endif %}
|
||||||
{% if object.organisation.email %}
|
{% if object.organisation.email %}
|
||||||
<dt>Organisation Email</dt>
|
<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 %}
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -57,11 +42,20 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<script>
|
||||||
$('#auth-request-form').on('submit', function () {
|
$('#auth-request-form').on('submit', function () {
|
||||||
$('#auth-request-form button').attr('disabled', true);
|
$('#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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style}
|
||||||
elif type == 'submit':
|
elif type == 'submit':
|
||||||
return {'submit': True, 'class': 'btn-primary', 'icon': 'fa-save', 'text': 'Save', 'id': id, 'style': style}
|
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}
|
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.db.models import Q
|
|||||||
|
|
||||||
from assets import models
|
from assets import models
|
||||||
|
|
||||||
|
|
||||||
class AssetForm(forms.ModelForm):
|
class AssetForm(forms.ModelForm):
|
||||||
related_models = {
|
related_models = {
|
||||||
'asset': models.Asset,
|
'asset': models.Asset,
|
||||||
|
|||||||
@@ -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]),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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)
|
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
|
@reversion.register
|
||||||
class Asset(models.Model, RevisionMixin):
|
class Asset(models.Model, RevisionMixin):
|
||||||
parent = models.ForeignKey(to='self', related_name='asset_parent',
|
parent = models.ForeignKey(to='self', related_name='asset_parent',
|
||||||
@@ -117,8 +122,8 @@ class Asset(models.Model, RevisionMixin):
|
|||||||
purchased_from = models.ForeignKey(to=Supplier, on_delete=models.SET_NULL, blank=True, null=True, related_name="assets")
|
purchased_from = models.ForeignKey(to=Supplier, on_delete=models.SET_NULL, blank=True, null=True, related_name="assets")
|
||||||
date_acquired = models.DateField()
|
date_acquired = models.DateField()
|
||||||
date_sold = models.DateField(blank=True, null=True)
|
date_sold = models.DateField(blank=True, null=True)
|
||||||
purchase_price = models.DecimalField(blank=True, 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])
|
||||||
salvage_value = models.DecimalField(null=True, decimal_places=2, max_digits=10)
|
replacement_cost = models.DecimalField(null=True, decimal_places=2, max_digits=10, validators=[validate_positive])
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
|
|
||||||
# Audit
|
# Audit
|
||||||
@@ -165,12 +170,6 @@ class Asset(models.Model, RevisionMixin):
|
|||||||
errdict["asset_id"] = [
|
errdict["asset_id"] = [
|
||||||
"An Asset ID can only consist of letters and numbers, with a final number"]
|
"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 self.is_cable:
|
||||||
if not self.length or self.length <= 0:
|
if not self.length or self.length <= 0:
|
||||||
errdict["length"] = ["The length of a cable must be more than 0"]
|
errdict["length"] = ["The length of a cable must be more than 0"]
|
||||||
|
|||||||
@@ -9,9 +9,11 @@
|
|||||||
date = new Date();
|
date = new Date();
|
||||||
}
|
}
|
||||||
$('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'));
|
$('#id_date_acquired').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
function setFieldValue(ID, CSA) {
|
function setFieldValue(ID, CSA) {
|
||||||
$('#' + String(ID)).val(CSA);
|
$('#' + String(ID)).val(CSA);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
function checkIfCableHidden() {
|
function checkIfCableHidden() {
|
||||||
document.getElementById("cable-table").hidden = !document.getElementById("id_is_cable").checked;
|
document.getElementById("cable-table").hidden = !document.getElementById("id_is_cable").checked;
|
||||||
@@ -39,16 +41,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group form-row">
|
<div class="form-group form-row">
|
||||||
{% include 'partials/form_field.html' with field=form.date_acquired col="col-6" %}
|
{% include 'partials/form_field.html' with field=form.date_acquired col="col-6" %}
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-2">
|
||||||
<button class="btn btn-info" onclick="setAcquired(true);" tabindex="-1">Today</button>
|
<button class="btn btn-info" onclick="return setAcquired(true);" tabindex="-1">Today</button>
|
||||||
<button class="btn btn-warning" onclick="setAcquired(false);" tabindex="-1">Unknown</button>
|
<button class="btn btn-warning" onclick="return setAcquired(false);" tabindex="-1">Unknown</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group form-row">
|
<div class="form-group form-row">
|
||||||
{% include 'partials/form_field.html' with field=form.date_sold col="col-6" %}
|
{% include 'partials/form_field.html' with field=form.date_sold col="col-6" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group form-row">
|
<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>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="form-group form-row">
|
<div class="form-group form-row">
|
||||||
@@ -64,16 +66,16 @@
|
|||||||
<div class="form-group form-row">
|
<div class="form-group form-row">
|
||||||
{% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %}
|
{% include 'partials/form_field.html' with field=form.length append=form.length.help_text col="col-6" %}
|
||||||
<div class="col-4">
|
<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-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="setFieldValue('{{ form.length.id_for_label }}','10');" tabindex="-1" type="button">10{{ 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="setFieldValue('{{ form.length.id_for_label }}','20');" tabindex="-1" type="button">20{{ 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>
|
</div>
|
||||||
<div class="form-group form-row">
|
<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" %}
|
{% include 'partials/form_field.html' with field=form.csa append=form.csa.help_text title='CSA' col="col-6" %}
|
||||||
<div class="col-4">
|
<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="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="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 }}', '2.5');" tabindex="-1" type="button">2.5{{ form.csa.help_text }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
{% extends 'base_assets.html' %}
|
|
||||||
{% load widget_tweaks %}
|
|
||||||
{% load button from filters %}
|
|
||||||
{% load cache %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% if create %}
|
|
||||||
<form method="POST" action="{% url 'cable_test'%}">
|
|
||||||
{% elif edit %}
|
|
||||||
<form method="POST" action="{% url 'cable_test' object.id %}">
|
|
||||||
{% endif %}
|
|
||||||
{% include 'form_errors.html' %}
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="id" value="{{ object.id|default:0 }}" hidden="">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
{% for field in form %}
|
|
||||||
<div class="form-group">
|
|
||||||
{% include 'partials/form_field.html' with field=field %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="text-right">
|
|
||||||
{% button 'submit' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% load title_spaced from filters %}
|
{% load title_spaced from filters %}
|
||||||
{% spaceless %}
|
{% 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 %}
|
{% if append or prepend %}
|
||||||
<div class="input-group {{col}}">
|
<div class="input-group {{col}}">
|
||||||
{% if prepend %}
|
{% if prepend %}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
|
<label for="{{ form.purchased_from.id_for_label }}">Supplier</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<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 %}
|
{% 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>
|
<option value="{{form.purchased_from.value}}" selected="selected" data-update_url="{% url 'supplier_update' form.purchased_from.value %}">{{ object.purchased_from }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -39,10 +39,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<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">
|
||||||
<div class="input-group-prepend"><span class="input-group-text">£</span></div>
|
<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>
|
||||||
</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>
|
<dd>{% if object.purchased_from %}<a href="{{object.purchased_from.get_absolute_url}}">{{ object.purchased_from }}</a>{%else%}-{%endif%}</dd>
|
||||||
<dt>Purchase Price</dt>
|
<dt>Purchase Price</dt>
|
||||||
<dd>£{{ object.purchase_price|default_if_none:'-' }}</dd>
|
<dd>£{{ object.purchase_price|default_if_none:'-' }}</dd>
|
||||||
<dt>Salvage Value</dt>
|
<dt>Replacement Cost</dt>
|
||||||
<dd>£{{ object.salvage_value|default_if_none:'-' }}</dd>
|
<dd>£{{ object.replacement_cost|default_if_none:'-' }}</dd>
|
||||||
<dt>Date Acquired</dt>
|
<dt>Date Acquired</dt>
|
||||||
<dd>{{ object.date_acquired|default_if_none:'-' }}</dd>
|
<dd>{{ object.date_acquired|default_if_none:'-' }}</dd>
|
||||||
{% if object.date_sold %}
|
{% if object.date_sold %}
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
from . import models as am
|
|
||||||
|
|
||||||
|
|
||||||
class Test(models.Model):
|
|
||||||
item = models.ForeignKey(to=am.Asset, on_delete=models.CASCADE)
|
|
||||||
date = models.DateField()
|
|
||||||
tested_by = models.ForeignKey(to='RIGS.Profile', on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
|
|
||||||
class ElectricalTest(Test):
|
|
||||||
visual = models.BooleanField()
|
|
||||||
remarks = models.TextField()
|
|
||||||
|
|
||||||
|
|
||||||
class CableTest(ElectricalTest):
|
|
||||||
# Should contain X circuit tests, where X is determined by circuits as per cable type
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitTest(models.Model):
|
|
||||||
test = models.ForeignKey(to=CableTest, on_delete=models.CASCADE)
|
|
||||||
continuity = models.DecimalField(help_text='Ω')
|
|
||||||
insulation_resistance = models.DecimalField(help_text='MΩ')
|
|
||||||
|
|
||||||
|
|
||||||
class TestRequirement(models.Model):
|
|
||||||
item = models.ForeignKey(to=am.Asset, on_delete=models.CASCADE)
|
|
||||||
test_type = models.ForeignKey(to=Test, on_delete=models.CASCADE)
|
|
||||||
period = models.IntegerField() # X months
|
|
||||||
|
|
||||||
|
|
||||||
class CableTestForm(forms.ModelForm):
|
|
||||||
class Meta
|
|
||||||
model = CableTest
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitTest(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = Choice
|
|
||||||
exclude = ('test',)
|
|
||||||
@@ -28,13 +28,13 @@ def cable_type(db):
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_cable(db, category, status, cable_type):
|
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
|
yield cable
|
||||||
cable.delete()
|
cable.delete()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_asset(db, category, status):
|
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
|
yield asset
|
||||||
asset.delete()
|
asset.delete()
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class AssetForm(FormPage):
|
|||||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||||
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
|
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
|
||||||
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
|
'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_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||||
'date_sold': (regions.DatePicker, (By.ID, 'id_date_sold')),
|
'date_sold': (regions.DatePicker, (By.ID, 'id_date_sold')),
|
||||||
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
||||||
@@ -221,7 +221,7 @@ class AssetAuditList(AssetList):
|
|||||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||||
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
||||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
'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')),
|
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||||
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
'category': (regions.SingleSelectPicker, (By.ID, 'id_category')),
|
||||||
'status': (regions.SingleSelectPicker, (By.ID, 'id_status')),
|
'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.status = status.name
|
||||||
page.serial_number = "MELON-MELON-MELON"
|
page.serial_number = "MELON-MELON-MELON"
|
||||||
page.comments = "You might need that"
|
page.comments = "You might need that"
|
||||||
page.salvage_value = "666"
|
page.replacement_cost = "666"
|
||||||
page.is_cable = True
|
page.is_cable = True
|
||||||
|
|
||||||
assert logged_in_browser.driver.find_element(By.ID, 'cable-table').is_displayed()
|
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.comments = comments = "This is actually a sledgehammer, not a cable..."
|
||||||
|
|
||||||
self.page.purchase_price = "12.99"
|
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.date_acquired = acquired = datetime.date(2020, 5, 2)
|
||||||
self.page.purchased_from_selector.toggle()
|
self.page.purchased_from_selector.toggle()
|
||||||
self.assertTrue(self.page.purchased_from_selector.is_open)
|
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,
|
self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1,
|
||||||
voltage_rating=40, num_pins=13)
|
voltage_rating=40, num_pins=13)
|
||||||
models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status,
|
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,
|
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,
|
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,
|
self.asset = models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status,
|
||||||
category=self.category,
|
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.page = pages.AssetAuditList(self.driver, self.live_server_url).open()
|
||||||
self.wait = WebDriverWait(self.driver, 20)
|
self.wait = WebDriverWait(self.driver, 20)
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ def test_oembed(client, test_asset):
|
|||||||
|
|
||||||
|
|
||||||
def test_asset_create(admin_client):
|
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.')
|
assertFormError(response, 'form', 'asset_id', 'This field is required.')
|
||||||
assert_asset_form_errors(response)
|
assert_asset_form_errors(response)
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ def test_cable_create(admin_client):
|
|||||||
|
|
||||||
def test_asset_edit(admin_client, test_asset):
|
def test_asset_edit(admin_client, test_asset):
|
||||||
url = reverse('asset_update', kwargs={'pk': test_asset.asset_id})
|
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)
|
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', 'category', 'This field is required.')
|
||||||
assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired')
|
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', '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')
|
||||||
|
|||||||
@@ -43,6 +43,4 @@ urlpatterns = [
|
|||||||
(views.SupplierCreate.as_view()), name='supplier_create'),
|
(views.SupplierCreate.as_view()), name='supplier_create'),
|
||||||
path('supplier/<int:pk>/edit/', permission_required_with_403('assets.change_supplier')
|
path('supplier/<int:pk>/edit/', permission_required_with_403('assets.change_supplier')
|
||||||
(views.SupplierUpdate.as_view()), name='supplier_update'),
|
(views.SupplierUpdate.as_view()), name='supplier_update'),
|
||||||
|
|
||||||
path('testing/<int:pk>/cable_test/' views.AddCableTest.as_view(), name='cable_test'),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from z3c.rml import rml2pdf
|
|||||||
|
|
||||||
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \
|
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \
|
||||||
is_ajax, OEmbedView
|
is_ajax, OEmbedView
|
||||||
from assets import forms, models, testing
|
from assets import forms, models
|
||||||
|
|
||||||
|
|
||||||
class AssetList(LoginRequiredMixin, generic.ListView):
|
class AssetList(LoginRequiredMixin, generic.ListView):
|
||||||
@@ -430,8 +430,3 @@ class GenerateLabels(generic.View):
|
|||||||
response['Content-Disposition'] = f'filename="{name}"'
|
response['Content-Disposition'] = f'filename="{name}"'
|
||||||
response.write(merged.getvalue())
|
response.write(merged.getvalue())
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class AddCableTest(generic.CreateView):
|
|
||||||
model = testing.CableTest
|
|
||||||
template = 'cable_test_form.html'
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ def admin_user(admin_user):
|
|||||||
admin_user.last_name = "Test"
|
admin_user.last_name = "Test"
|
||||||
admin_user.initials = "ETU"
|
admin_user.initials = "ETU"
|
||||||
admin_user.is_approved = True
|
admin_user.is_approved = True
|
||||||
|
admin_user.is_supervisor = True
|
||||||
admin_user.save()
|
admin_user.save()
|
||||||
return admin_user
|
return admin_user
|
||||||
|
|
||||||
|
|||||||
53
package-lock.json
generated
53
package-lock.json
generated
@@ -30,7 +30,7 @@
|
|||||||
"html5sortable": "^0.13.3",
|
"html5sortable": "^0.13.3",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"konami": "^1.6.3",
|
"konami": "^1.6.3",
|
||||||
"moment": "^2.29.2",
|
"moment": "^2.29.4",
|
||||||
"node-sass": "^7.0.0",
|
"node-sass": "^7.0.0",
|
||||||
"popper.js": "^1.16.1",
|
"popper.js": "^1.16.1",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.5",
|
||||||
@@ -5322,9 +5322,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
|
||||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
"integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
},
|
},
|
||||||
@@ -5466,9 +5466,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/moment": {
|
"node_modules/moment": {
|
||||||
"version": "2.29.2",
|
"version": "2.29.4",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||||
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==",
|
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
@@ -7586,9 +7586,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-parser": {
|
"node_modules/socket.io-parser": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.3.tgz",
|
||||||
"integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==",
|
"integrity": "sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"component-emitter": "~1.3.0",
|
"component-emitter": "~1.3.0",
|
||||||
@@ -7639,14 +7639,17 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/socket.io/node_modules/socket.io-parser": {
|
"node_modules/socket.io/node_modules/socket.io-parser": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.2.tgz",
|
||||||
"integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==",
|
"integrity": "sha512-QFZBaZDNqZXeemwejc7D39jrq2eGK/qZuVDiMPKzZK1hLlNvjGilGt4ckfQZeVX4dGmuPzCytN9ZW1nQlEWjgA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"component-emitter": "1.2.1",
|
"component-emitter": "1.2.1",
|
||||||
"debug": "~4.1.0",
|
"debug": "~4.1.0",
|
||||||
"isarray": "2.0.1"
|
"isarray": "2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socks": {
|
"node_modules/socks": {
|
||||||
@@ -13292,9 +13295,9 @@
|
|||||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
|
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
|
||||||
},
|
},
|
||||||
"minimatch": {
|
"minimatch": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
|
||||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
"integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
@@ -13397,9 +13400,9 @@
|
|||||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||||
},
|
},
|
||||||
"moment": {
|
"moment": {
|
||||||
"version": "2.29.2",
|
"version": "2.29.4",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||||
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg=="
|
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||||
},
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -15053,9 +15056,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"socket.io-parser": {
|
"socket.io-parser": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.2.tgz",
|
||||||
"integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==",
|
"integrity": "sha512-QFZBaZDNqZXeemwejc7D39jrq2eGK/qZuVDiMPKzZK1hLlNvjGilGt4ckfQZeVX4dGmuPzCytN9ZW1nQlEWjgA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"component-emitter": "1.2.1",
|
"component-emitter": "1.2.1",
|
||||||
@@ -15102,9 +15105,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"socket.io-parser": {
|
"socket.io-parser": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.3.tgz",
|
||||||
"integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==",
|
"integrity": "sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"component-emitter": "~1.3.0",
|
"component-emitter": "~1.3.0",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
"html5sortable": "^0.13.3",
|
"html5sortable": "^0.13.3",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"konami": "^1.6.3",
|
"konami": "^1.6.3",
|
||||||
"moment": "^2.29.2",
|
"moment": "^2.29.4",
|
||||||
"node-sass": "^7.0.0",
|
"node-sass": "^7.0.0",
|
||||||
"popper.js": "^1.16.1",
|
"popper.js": "^1.16.1",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.5",
|
||||||
|
|||||||
@@ -47,14 +47,16 @@ function initPicker(obj) {
|
|||||||
//log: 3,
|
//log: 3,
|
||||||
preprocessData: function (data) {
|
preprocessData: function (data) {
|
||||||
var i, l = data.length, array = [];
|
var i, l = data.length, array = [];
|
||||||
array.push({
|
if (!obj.data('noclear')) {
|
||||||
text: clearSelectionLabel,
|
array.push({
|
||||||
value: '',
|
text: clearSelectionLabel,
|
||||||
data:{
|
value: '',
|
||||||
update_url: '',
|
data:{
|
||||||
subtext:''
|
update_url: '',
|
||||||
}
|
subtext:''
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (l) {
|
if (l) {
|
||||||
for(i = 0; i < l; i++){
|
for(i = 0; i < l; i++){
|
||||||
@@ -71,11 +73,13 @@ function initPicker(obj) {
|
|||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
console.log(obj.data);
|
||||||
obj.prepend($("<option></option>")
|
if (!obj.data('noclear')) {
|
||||||
.attr("value",'')
|
obj.prepend($("<option></option>")
|
||||||
.text(clearSelectionLabel)
|
.attr("value",'')
|
||||||
.data('update_url','')); //Add "clear selection" option
|
.text(clearSelectionLabel)
|
||||||
|
.data('update_url','')); //Add "clear selection" option
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker
|
obj.selectpicker().ajaxSelectPicker(options); //Initiaise selectPicker
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
{% load nice_errors from filters %}
|
{% load nice_errors from filters %}
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<div class="alert alert-danger alert-dismissable">
|
<div class="alert alert-danger mb-0">
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
|
||||||
<dl>
|
<dl>
|
||||||
{% with form|nice_errors as qq %}
|
{% with form|nice_errors as qq %}
|
||||||
{% for error_name,desc in qq.items %}
|
{% for error_name,desc in qq.items %}
|
||||||
<span class="row">
|
<span class="row">
|
||||||
<dt class="col-4">{{error_name}}</dt>
|
<dt class="col-3">{{error_name}}</dt>
|
||||||
<dd class="col-8">{{desc}}</dd>
|
<dd class="col-9">{{desc}}</dd>
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endwith %}
|
{% 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>
|
<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 %}
|
{% 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>
|
<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 %}
|
{% 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>
|
<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 %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
{% include 'form_errors.html' %}
|
{% 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">
|
<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 %}
|
<form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from PyRIGS.decorators import user_passes_test_with_403
|
from PyRIGS.decorators import user_passes_test_with_403
|
||||||
|
|
||||||
|
|
||||||
def has_perm_or_supervisor(perm, login_url=None, oembed_view=None):
|
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) or u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)
|
return user_passes_test_with_403(lambda u: (hasattr(u, 'is_supervisor') and u.is_supervisor))
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ class TrainingItemQualification(models.Model, RevisionMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def activity_feed_string(self):
|
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
|
@classmethod
|
||||||
def get_colour_from_depth(cls, depth):
|
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>
|
<a class="dropdown-item" href="{% url 'item_list' %}"><span class="fas fa-sitemap"></span> Item List</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</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>
|
<li class="nav-item"><a class="nav-link" href="{% url 'session_log' %}"><span class="fas fa-users"></span> Log Session</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="nav-item"><a class="nav-link" href="{% url 'training_activity_table' %}"><span class="fas fa-random"></span> Recent Changes</a></li>
|
<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 %}
|
{% render_field form.date|add_class:'form-control'|attr:'type="date"' value=training_date %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</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>
|
||||||
<div class="form-group form-row">
|
<div class="form-group form-row">
|
||||||
<label for="id_notes" class="col-sm-2 col-form-label">Notes</label>
|
<label for="id_notes" class="col-sm-2 col-form-label">Notes</label>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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">
|
<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">
|
<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
|
<span class="fas fa-plus"></span> Add New Requirement
|
||||||
@@ -79,9 +79,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr><th colspan="3" class="text-center">{{object}}</th></tr>
|
<tr><th colspan="3" class="text-center">{{object}}</th></tr>
|
||||||
<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.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 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 %}<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.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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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">
|
<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
|
<span class="fas fa-plus"></span> Add New Training Record
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
|
<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 %}
|
{% if supervisor %}
|
||||||
<option value="{{form.supervisor.value}}" selected>{{ supervisor }}</option>
|
<option value="{{form.supervisor.value}}" selected>{{ supervisor }}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -28,25 +28,26 @@
|
|||||||
{% include 'form_errors.html' %}
|
{% include 'form_errors.html' %}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<h3>People</h3>
|
<h3>People</h3>
|
||||||
<div class="form-group row">
|
<div class="form-group row" id="supervisor_group">
|
||||||
{% include 'partials/supervisor_field.html' %}
|
{% include 'partials/supervisor_field.html' %}
|
||||||
</div>
|
</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>
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<h3>Training Items</h3>
|
<h3>Training Items</h3>
|
||||||
{% for depth in depths %}
|
{% 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>
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<h3>Session Information</h3>
|
<h3>Session Information</h3>
|
||||||
<div class="form-group">
|
<div class="form-group row">
|
||||||
{% include 'partials/form_field.html' with field=form.date %}
|
{% include 'partials/form_field.html' with field=form.date col='col-sm-6' %}
|
||||||
|
{% button 'today' id='id_date' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{% include 'partials/form_field.html' with field=form.notes %}
|
{% include 'partials/form_field.html' with field=form.notes %}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th>Supervisor</th>
|
<th>Supervisor</th>
|
||||||
<th>Notes</th>
|
<th>Notes</th>
|
||||||
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
|
{% if request.user.is_supervisor %}
|
||||||
<th></th>
|
<th></th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<td>{{ object.date }}</td>
|
<td>{{ object.date }}</td>
|
||||||
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
|
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
|
||||||
<td>{{ object.notes }}</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>
|
<td>{% button 'edit' 'edit_qualification' object.pk id="edit" %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ def training_item(db):
|
|||||||
training_item.delete()
|
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
|
@pytest.fixture
|
||||||
def level(db):
|
def level(db):
|
||||||
level = models.TrainingLevel.objects.create(description="There is no description.", level=models.TrainingLevel.TECHNICIAN)
|
level = models.TrainingLevel.objects.create(description="There is no description.", level=models.TrainingLevel.TECHNICIAN)
|
||||||
|
|||||||
@@ -40,3 +40,42 @@ class AddQualification(FormPage):
|
|||||||
@property
|
@property
|
||||||
def success(self):
|
def success(self):
|
||||||
return 'add' not in self.driver.current_url
|
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
|
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):
|
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()
|
page = pages.AddQualification(logged_in_browser.driver, live_server.url, pk=trainee.pk).open()
|
||||||
# assert page.name in str(trainee)
|
# 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
|
assert page.item_selector.options[0].selected
|
||||||
page.item_selector.toggle()
|
page.item_selector.toggle()
|
||||||
|
|
||||||
page.supervisor_selector.toggle()
|
select_super(page, supervisor)
|
||||||
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()
|
page.submit()
|
||||||
assert page.success
|
assert page.success
|
||||||
@@ -44,3 +48,32 @@ def test_add_qualification(logged_in_browser, live_server, trainee, supervisor,
|
|||||||
assert qualification.date == date
|
assert qualification.date == date
|
||||||
assert qualification.notes == "A note"
|
assert qualification.notes == "A note"
|
||||||
assert qualification.depth == models.TrainingItemQualification.STARTED
|
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})
|
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', 'date', 'Qualification date may not be in the future')
|
||||||
assertFormError(response, 'form', 'supervisor', 'One may not supervise oneself...')
|
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)
|
print(response.content)
|
||||||
assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...')
|
assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...')
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from training.decorators import has_perm_or_supervisor
|
from training.decorators import is_supervisor
|
||||||
|
from PyRIGS.decorators import permission_required_with_403
|
||||||
|
|
||||||
from training import views, models
|
from training import views, models
|
||||||
from versioning.views import VersionHistory
|
from versioning.views import VersionHistory
|
||||||
@@ -12,22 +13,22 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
|
path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'),
|
||||||
path('trainee/<int:pk>/',
|
path('trainee/<int:pk>/',
|
||||||
has_perm_or_supervisor('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
permission_required_with_403('RIGS.view_profile')(views.TraineeDetail.as_view()),
|
||||||
name='trainee_detail'),
|
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>/history', permission_required_with_403('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>/add_qualification/', is_supervisor()(views.AddQualification.as_view()),
|
||||||
name='add_qualification'),
|
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'),
|
name='edit_qualification'),
|
||||||
|
|
||||||
path('levels/', login_required(views.LevelList.as_view()), name='level_list'),
|
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>/', 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>/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/<int:pk>/add_requirement/', is_supervisor()(views.AddLevelRequirement.as_view()), name='add_requirement'),
|
||||||
path('level/remove_requirement/<int:pk>/', login_required(views.RemoveRequirement.as_view()), name='remove_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('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()
|
||||||
Reference in New Issue
Block a user